// ==UserScript==
// @name KJL后台-编缉器功能增强
// @namespace Violentmonkey Scripts
// @match https://www.kujiale.com/vc/modeleditor/new*
// @grant none
// @version 1.8.1
// @author -
// @description 2025/4/20 18:21:03
// ==/UserScript==
// KJL后台-编缉器隐藏网格和高亮隐藏参数
(function() {
'use strict';
// 目标选择器
const gridButtonSelector = 'div.tuipro-bottombar-group:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
const hideGridOptionSelector = 'li.visibility-menu-content-item:nth-child(1)';
const highlightButtonSelector = 'button.tui-switch:nth-child(2)';
const checkButtonSelector = 'button.tui-switch:nth-child(2)';
// 执行操作的函数
function performActions() {
// 隐藏网格
let gridButton = document.querySelector(gridButtonSelector);
if (gridButton) {
gridButton.click();
let hideGridOption = document.querySelector(hideGridOptionSelector);
if (hideGridOption) {
hideGridOption.click();
gridButton.click(); // 可能是为了关闭菜单?
}
} else {
console.log("未能找到网格按钮");
}
// 高亮隐藏变量
let highlightButton = document.querySelector(highlightButtonSelector);
if (highlightButton) {
highlightButton.click();
} else {
console.log("未能找到高亮开关按钮");
}
}
// 创建一个MutationObserver实例
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // 确保是元素节点
let checkButton = node.querySelector(checkButtonSelector);
if (checkButton) {
performActions();
observer.disconnect(); // 执行一次后停止观察
}
}
});
});
});
// 配置观察选项
const config = { childList: true, subtree: true };
// 开始观察整个文档
observer.observe(document.body, config);
// 初始检查,以防目标元素已经存在
let checkButton = document.querySelector(checkButtonSelector);
if (checkButton) {
performActions();
observer.disconnect(); // 如果已经找到,停止观察
}
})();
// KJL后台-编辑器参数提示框优化
(function() {
'use strict';
// Function to reorder the first two children of the target element
function reorderChildren(target) {
if (target && target.children.length > 1) {
// Store the first child
var firstChild = target.children[0];
// Store the second child
var secondChild = target.children[1];
// Remove the first child
target.removeChild(firstChild);
// Insert the first child after the second child
target.insertBefore(firstChild, secondChild.nextSibling);
}
}
// Create a new MutationObserver instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// Check if addedNodes is not empty and the first added node is a div with the class 'auto-complete-select-list'
if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList && mutation.addedNodes[0].classList.contains('auto-complete-select-list')) {
// Call the reorder function with the added node
reorderChildren(mutation.addedNodes[0]);
}
});
});
// Start the observer
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
// KJL后台-编缉器属性栏添加更新商品名称
(function() {
'use strict';
// 添加样式到文档
var style = document.createElement('style');
document.head.appendChild(style);
style.type = 'text/css';
style.appendChild(document.createTextNode(`
/* 可视化按钮样式 */
.auto-update-button {
width: 40px;
height: 20px;
background-color: #ccc;
border-radius: 10px;
position: relative;
cursor: pointer;
transition: background-color 0.3s;
border: none;
outline: none;
margin-left: 5px;
}
.auto-update-button::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
background-color: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.auto-update-button.active {
background-color: #4caf50;
}
.auto-update-button.active::before {
transform: translateX(20px);
}
/* 文本标签样式 */
.mode-label {
font-size: 16px;
margin-left: 10px;
margin-right: 5px;
cursor: pointer; /* 手动模式时显示手形光标 */
color: #007bff;
transition: color 0.3s;
}
.mode-label:hover {
color: #fd00fd;
}
/* 按钮容器样式 */
.button-container {
display: flex;
margin-left: 10px;
align-items: center; /* 垂直居中对齐 */
justify-content: flex-start; /* 水平左对齐 */
height: 30px; /* 固定高度 */
gap: 5px; /* 按钮之间的间距 */
}
`));
// 设置默认更新方式
let autoUpdate = false;
// 设置检测间隔时间(毫秒)
const checkInterval = 100;
// 添加自动/手动切换按钮和文本标签
function addButtons() {
const sidebarHeader = document.querySelector('.sidebar-container .sidebar-header');
if (sidebarHeader && !document.querySelector('.button-container')) {
// 创建按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('button-container');
// 创建文本标签
const modeLabel = document.createElement('span');
modeLabel.classList.add('mode-label');
modeLabel.textContent = autoUpdate ? '自动更新' : '手动更新';
// 根据模式设置文本标签的样式和点击事件
if (!autoUpdate) {
modeLabel.classList.add('manual'); // 手动模式时添加样式
modeLabel.addEventListener('click', updateName); // 绑定点击事件
}
// 创建自动/手动切换按钮
const autoUpdateButton = document.createElement('button');
autoUpdateButton.classList.add('auto-update-button');
autoUpdateButton.title = autoUpdate ? '自动更新已开启' : '自动更新已关闭'; // 鼠标悬停提示
// 根据 autoUpdate 状态设置初始样式
if (autoUpdate) {
autoUpdateButton.classList.add('active');
}
// 为按钮添加点击事件
autoUpdateButton.addEventListener('click', function() {
autoUpdate = !autoUpdate;
autoUpdateButton.classList.toggle('active');
autoUpdateButton.title = autoUpdate ? '自动更新已开启' : '自动更新已关闭'; // 更新鼠标悬停提示
modeLabel.textContent = autoUpdate ? '自动更新' : '手动更新'; // 更新文本标签
// 切换文本标签的样式和点击事件
if (autoUpdate) {
modeLabel.classList.remove('manual');
modeLabel.removeEventListener('click', updateName); // 移除点击事件
} else {
modeLabel.classList.add('manual');
modeLabel.addEventListener('click', updateName); // 绑定点击事件
}
});
// 创建“查打详情”按钮
const modelDetail = document.createElement('span');
modelDetail.classList.add('mode-label');
modelDetail.textContent = '详情';
modelDetail.addEventListener('click', function() {
const productNameDiv = document.querySelector('button.tui-btn--primary:nth-child(1)');
if (productNameDiv) {
productNameDiv.click();
}
});
// 创建“打开模型”按钮
const openModel = document.createElement('span');
openModel.classList.add('mode-label');
openModel.textContent = '打开';
openModel.addEventListener('click', function() {
const openModelIcon = document.querySelector('.option-button > button:nth-child(2)');
if (openModelIcon) {
openModelIcon.click();
}
});
// 将文本标签和按钮添加到容器中
buttonContainer.appendChild(autoUpdateButton);
buttonContainer.appendChild(modeLabel);
buttonContainer.appendChild(modelDetail);
buttonContainer.appendChild(openModel);
// 将按钮容器插入到.sidebar-header的前面
sidebarHeader.parentNode.insertBefore(buttonContainer, sidebarHeader);
}
}
// 手动更新名称
function updateName() {
const nameLinkDiv = document.querySelector('.tui-text-overflow-multiple-line');
const nameContent = document.querySelector('.name-editor p.name-content');
const changeButton = document.querySelector('.name-change-button');
if (nameLinkDiv && nameContent && changeButton) {
const nameLinkText = nameLinkDiv.textContent.trim();
const nameContentText = nameContent.textContent.trim();
if (nameLinkText !== nameContentText) {
changeButton.click();
// 等待.name-editor > input出现
setTimeout(() => {
const input = document.querySelector('.name-editor > input');
if (input) {
input.value = nameLinkText;
// 使输入框失去焦点
input.blur();
}
}, 100); // 延迟100毫秒确保输入框已经加载
}
}
}
// 自动更新名称
function autoCheckAndUpdateName() {
const nameLinkDiv = document.querySelector('.tui-text-overflow-multiple-line');
const nameContent = document.querySelector('.name-editor p.name-content');
const changeButton = document.querySelector('.name-change-button');
// 确保所有元素都已加载
if (nameLinkDiv && nameContent && changeButton && autoUpdate == true) {
// 获取两个元素的文本内容
const nameLinkText = nameLinkDiv.textContent.trim();
const nameContentText = nameContent.textContent.trim();
// 比较文本内容
if (nameLinkText !== nameContentText) {
// 文本不同,点击.name-change-button
changeButton.click();
// 等待.name-editor > input出现
setTimeout(() => {
const input = document.querySelector('.name-editor > input');
if (input) {
// 更新.name-editor > input内的文本
input.value = nameLinkText;
// 使输入框失去焦点
input.blur();
}
}, 100); // 延迟500毫秒确保输入框已经加载
}
}
}
// 检测并添加按钮
function checkAndAddButton() {
const nameEditor = document.querySelector('.name-editor');
if (nameEditor && nameEditor.offsetParent !== null) {
addButtons();
autoCheckAndUpdateName();
}
}
// 使用setInterval来周期性检测
setInterval(checkAndAddButton, checkInterval);
})();
// KJL后台-编缉器部件栏添加关闭所有干涉
(function() {
'use strict';
function createToggleButton(targetElement) {
// 检查按钮是否已经存在,以避免重复添加
if (targetElement.nextElementSibling && targetElement.nextElementSibling.classList.contains('ikKMNI')) {
return;
}
// 创建按钮
var button = document.createElement('button');
button.setAttribute('type', 'button');
button.classList.add('ikKMNI');
button.style.marginLeft= '5px';
button.style.marginLeft= '10px';
button.style.fontSize = '12px';
button.style.cursor = 'pointer'; // 设置光标为手形
button.textContent = '关闭所有干涉';
button.addEventListener('mouseover', function() {
this.style.color = '#fd00fd';
this.style.setProperty('color', '#fd00fd', 'important');
});
button.addEventListener('mouseout', function() {
this.style.removeProperty('color');
});
// 按钮点击事件处理函数
button.addEventListener('click', async function() {
var items = document.querySelectorAll('ul.tui-tree-new:nth-child(2) > li > span:nth-child(2) > span:nth-child(2) > div:nth-child(1) > div:nth-child(1) > span:nth-child(1) > span:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2)');
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (item) {
item.click();
console.log('点击第 ' + (index + 1) + ' 项,共 ' + items.length + ' 项');
// 等待 specificElement1 出现
const specificElement1 = await waitElement('.element-attr-groups > li:nth-child(11) > li:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(10) > div:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)', 500);
if (specificElement1) {
specificElement1.click();
console.log('点击 => 忽略内部干涉', specificElement1);
// 等待 specificElement2 出现
const specificElement2 = await waitElement('.select-input-options > li.option-item:nth-child(2)', 500);
if (specificElement2) {
specificElement2.click();
console.log('选择 => 是', specificElement2);
}
}
}
}
});
function waitElement(selector, timeout) {
return new Promise(function(resolve) {
const startTime = Date.now();
const interval = setInterval(function() {
const element = document.querySelector(selector);
if (element || (Date.now() - startTime > timeout)) {
clearInterval(interval);
resolve(element);
}
}, 100);
});
}
// 在目标元素后插入切换按钮
targetElement.parentNode.insertBefore(button, targetElement.nextSibling);
}
// MutationObserver配置
var observer = new MutationObserver(function(mutationsList) {
mutationsList.forEach(function(mutation) {
if (mutation.type === 'childList') {
// 检查目标元素是否出现或变化
var targetElement = document.querySelector('.tui-tabs-bar-tab-content-flex');
if (targetElement) {
// 在目标元素旁边创建切换按钮
createToggleButton(targetElement);
}
}
});
});
// 观察器选项(监控子节点列表变化和子树变化)
var config = { childList: true, subtree: true };
// 开始监视body的变化
observer.observe(document.body, config);
})();
// KJL后台-编辑器部件栏添加分类隐藏
(function() {
'use strict';
// 添加样式到文档
var style = document.createElement('style');
document.head.appendChild(style);
style.type = 'text/css';
style.appendChild(document.createTextNode(`
/* 轮廓线的线宽与颜色 */
.profile-canvas .canvas-items-container .line {
stroke-width: 10;
stroke: rgba(0,0,0,.65);
}
/* 轮廓线选中后的线宽与颜色 */
.profile-canvas .canvas-items-container .line.selected {
stroke-width: 10;
stroke: #1a7af8;
}
/* 隐藏按钮的样式 */
.fwwEgA {
margin-left: 10px;
cursor: pointer;
color: #1a7af8;
}
`));
var count = 0;
// 创建隐藏按钮的函数
function createHideButtonForAll() {
// 获取所有的ul.tui-tree-new元素
var treeNewElements = document.querySelectorAll('ul.tui-tree-new');
treeNewElements.forEach(function(treeNew) {
// 查找特定的子元素位置
var targetElement = treeNew.querySelector('li:nth-child(1) > span:nth-child(1) > span:nth-child(2) > div:nth-child(1) > div:nth-child(1) > span:nth-child(1) > span:nth-child(1)');
if (targetElement) {
// 检查按钮是否已经存在,以避免重复添加
if (targetElement.nextElementSibling && targetElement.nextElementSibling.classList.contains('fwwEgA')) {
return;
}
// 创建全部隐藏按钮
var link = document.createElement('a');
link.setAttribute('role', 'button');
link.setAttribute('aria-disabled', 'false');
link.setAttribute('class', 'fwwEgA');
link.textContent = '隐藏';
// 创建隐藏布尔按钮
var booleanLink = document.createElement('a');
booleanLink.setAttribute('role', 'button');
booleanLink.setAttribute('aria-disabled', 'false');
booleanLink.setAttribute('class', 'fwwEgA');
booleanLink.textContent = '布尔';
// 添加点击事件监听器以隐藏布尔元素
booleanLink.addEventListener('click', function() {
var hideButtons = treeNew.querySelectorAll('.param-iconfont.param-yixianshibuermoxing-20px, .param-iconfont.param-yincangbuermoxing-20px');
hideButtons.forEach(function(button) {
button.click();
});
});
// 添加点击事件监听器以隐藏文本
link.addEventListener('click', function() {
var hideButtons = treeNew.querySelectorAll('.param-iconfont.param-eye-on, .param-iconfont.param-a-duixiang_yincang_20pxbeifen2');
hideButtons.forEach(function(button) {
button.click();
});
});
// 在目标元素后插入隐藏布尔按钮和全部隐藏按钮
targetElement.parentNode.insertBefore(link, targetElement.nextSibling);
if(count==0){
targetElement.parentNode.insertBefore(booleanLink, targetElement.nextSibling);
count++;
}
}
});
}
// MutationObserver配置
var observer = new MutationObserver(function(mutationsList) {
mutationsList.forEach(function(mutation) {
if (mutation.type === 'childList') {
// 当子节点列表变化时,为所有ul.tui-tree-new添加按钮
createHideButtonForAll();
}
});
});
// 观察器选项(监控子节点列表变化和子树变化)
var config = { childList: true, subtree: true };
// 开始监视body的变化
observer.observe(document.body, config);
// 初始化时也为所有ul.tui-tree-new添加按钮
createHideButtonForAll();
})();
// KJL后台-编缉器属性项添加锁定值
(function() {
'use strict';
// 添加样式到文档
var style = document.createElement('style');
document.head.appendChild(style);
style.type = 'text/css';
style.appendChild(document.createTextNode(`
/* 右栏 属性 下拉框 */
div.attr-select-input div.attr-select-input-dropdown {
height: auto !important;
background: #fff !important;
border: 1px solid #1a7af8;
}
div.attr-select-input div.attr-select-input-dropdown ul.select-input-options a.lock {
position: absolute;
margin-top: -26px !important;
margin-left: 205px;
}
.attr-select-input .attr-select-input-dropdown .select-input-options {
height: auto !important;
max-height:1000px !important;
}
.attr-select-input .attr-select-input-dropdown .select-input-options .option-item {
width: 190px !important;
}
.attr-select-input .attr-select-input-dropdown .select-input-options .option-item .option-label .description {
width: 30px !important;
}
`));
function addLockAfterOptionItems() {
const dropdowns = document.querySelectorAll('.attr-select-input-dropdown');
dropdowns.forEach(dropdown => {
const optionItems = dropdown.querySelectorAll('.option-item');
optionItems.forEach((item, index) => {
if (index > 0) {
let nextSibling = item.nextElementSibling;
let hasLock = false;
while (nextSibling) {
if (nextSibling.classList.contains('lock')) {
hasLock = true;
break;
}
nextSibling = nextSibling.nextElementSibling;
}
if (!hasLock) {
const lockElement = document.createElement('a');
lockElement.textContent = '锁定';
lockElement.className = 'lock';
item.insertAdjacentElement('afterend', lockElement);
lockElement.addEventListener('click', function() {
const descriptionElement = item.querySelector('.description') || item.querySelector('.name');
const descriptionContent = descriptionElement.textContent;
const inputElement = document.querySelector('.custom-input > div:nth-child(1) > div:nth-child(1) > input:nth-child(1)');
if (inputElement) {
// 触发完整事件序列
const triggerEvents = ['focus', 'input', 'change', 'blur'];
// 使用原型方法设置值
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
let finalValue;
if (descriptionContent === 'true' || descriptionContent === 'false') {
finalValue = descriptionContent;
} else {
finalValue = '1*' + descriptionContent;
}
valueSetter.call(inputElement, finalValue);
// 触发框架事件
if (inputElement.__vue__) {
inputElement.__vue__.$emit('input', '1*' + descriptionContent);
}
// 派发完整事件
triggerEvents.forEach(eventType => {
const event = new Event(eventType, {
bubbles: true,
cancelable: true
});
inputElement.dispatchEvent(event);
});
// 保持焦点控制
inputElement.focus();
// 新增确认按钮点击(加在inputElement处理完成后)
setTimeout(() => {
document.querySelectorAll('.custom-input-btn').forEach(btn => {
if (getComputedStyle(btn).display !== 'none') {
btn.dispatchEvent(new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
}));
}
});
}, 150);
}
});
}
}
});
});
}
// 使用 MutationObserver 监听 DOM 变化
const observer = new MutationObserver(function(mutationsList) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
addLockAfterOptionItems();
}
}
});
// 开始观察整个文档的 body,监听子节点变化
observer.observe(document.body, {
childList: true,
subtree: true
});
// ===== 锁定浮层:输入框有文本且处于焦点时,回车 = 点「确定」 =====
// 浮层结构:div.auto-input-container.custom-input > ... > input 右侧 button.custom-input-btn「确定」
(function bindEnterToConfirm() {
if (window.__kjlLockEnterBound) return; // 防重复绑定
window.__kjlLockEnterBound = true;
function isVisible(el) {
if (!el) return false;
var s = getComputedStyle(el);
return s.display !== 'none' && s.visibility !== 'hidden' && el.offsetParent !== null;
}
function clickConfirm() {
var btns = [].slice.call(document.querySelectorAll('.custom-input-btn'));
var btn = btns.filter(isVisible)[0];
if (!btn) return false;
btn.dispatchEvent(new MouseEvent('click', { view: window, bubbles: true, cancelable: true }));
return true;
}
// 参数补全候选框是否展开(站点常驻该节点,只在展开时可见)
function acOpen() {
var els = document.querySelectorAll('.auto-complete-select-list');
for (var i = 0; i < els.length; i++) {
var el = els[i];
var s = getComputedStyle(el);
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) continue;
// ★ 该浮层是 position:fixed 且按需创建 —— offsetParent 恒为 null,只能看实际尺寸
var b = el.getBoundingClientRect();
if (b.width > 10 && b.height > 10) return true;
}
return false;
}
document.addEventListener('keydown', function (e) {
var isEnter = e.key === 'Enter' || e.keyCode === 13;
if (!isEnter) return;
var input = e.target;
if (!input || input.tagName !== 'INPUT') return;
if (!input.closest || !input.closest('.custom-input')) return; // 只认锁定浮层的输入框
// ★ 候选框展开时 Enter 归站点(把第一条候选填进输入框),绝不能点「确定」
if (acOpen()) return;
if (document.activeElement !== input) return; // 必须处于焦点
if (!input.value || !input.value.trim()) return; // 必须有文本
e.preventDefault();
e.stopPropagation();
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
clickConfirm();
}, true);
})();
// ===== 下拉浮层弹出后:输入框自动聚焦,光标移到末尾 =====
// 目的:打开属性下拉 → 直接开始输入 → 回车确定(配合上面的「回车=确定」)
(function autoFocusCustomInput() {
if (window.__kjlLockAutoFocus) return; // 防重复绑定
window.__kjlLockAutoFocus = true;
function focusEnd(input) {
if (!input || !input.isConnected) return;
try { input.focus({ preventScroll: true }); } catch (e) { try { input.focus(); } catch (e2) { } }
var n = (input.value || '').length;
try { input.setSelectionRange(n, n); } catch (e) { }
}
var lastSig = '';
setInterval(function () {
var dd = document.querySelector('.attr-select-input-dropdown');
if (!dd || dd.offsetParent === null || getComputedStyle(dd).display === 'none') { lastSig = ''; return; }
var input = dd.querySelector('.custom-input input');
if (!input || input.offsetParent === null) { lastSig = ''; return; }
if (lastSig === 'open') return;
lastSig = 'open';
// 站点可能在本帧之后重新渲染输入框 → 分三次补聚焦,确保焦点与光标位置都落定
[0, 80, 200].forEach(function (d) { setTimeout(function () { focusEnd(input); }, d); });
}, 120);
})();
// 在页面卸载时停止观察
window.addEventListener('unload', function() {
observer.disconnect();
});
})();
// KJL后台-编辑器属性行快捷按钮
// 尺寸属性:宽度→W(#W) 深度→D(#D) 高度→H(#H)
// 物理属性:位置X→-W/W(-#W/#W) 位置Y→-D/D(-#D/#D) 位置Z→-H/H(-#H/#H)
// 旋转X/Y/Z→-90/+90(在当前值上 ±90)
// 仅当输入框可编辑(未 disabled / 未 readOnly)时才执行。
(function () {
'use strict';
var SPEC = [
{ label: '宽度', kind: 'set', btns: [['W', '#W']] },
{ label: '深度', kind: 'set', btns: [['D', '#D']] },
{ label: '高度', kind: 'set', btns: [['H', '#H']] },
{ label: '位置X', kind: 'set', btns: [['-W', '-#W'], ['W', '#W']] },
{ label: '位置Y', kind: 'set', btns: [['-D', '-#D'], ['D', '#D']] },
{ label: '位置Z', kind: 'set', btns: [['-H', '-#H'], ['H', '#H']] },
{ label: '旋转X', kind: 'num', btns: [['-90', -90], ['+90', 90]] },
{ label: '旋转Y', kind: 'num', btns: [['-90', -90], ['+90', 90]] },
{ label: '旋转Z', kind: 'num', btns: [['-90', -90], ['+90', 90]] }
];
function injectStyle() {
if (document.getElementById('kjl-qb-style')) return;
var st = document.createElement('style');
st.id = 'kjl-qb-style';
st.textContent = [
'.attr-panel .kjl-qb { display: inline-flex; align-items: center; justify-content: center;',
' box-sizing: border-box; width: 30px; height: 20px; padding: 0; margin-left: 4px;',
' font-size: 11px; line-height: 18px; border: 1px solid #c9cdd4; border-radius: 3px;',
' background: #fff; color: #4e5969; cursor: pointer; user-select: none;',
' vertical-align: middle; font-family: inherit; font-weight: normal; white-space: nowrap; }',
'.attr-panel .kjl-qb:hover { border-color: #1a7af8; color: #1a7af8; background: #f2f8ff; }',
'.attr-panel .kjl-qb:active { background: #e3f0ff; }',
'.attr-panel .kjl-qb.kjl-off { opacity: .35; cursor: not-allowed; }',
'.attr-panel .kjl-qb.kjl-off:hover { border-color: #c9cdd4; color: #4e5969; background: #fff; }'
].join('\n');
(document.head || document.documentElement).appendChild(st);
}
// 标签所在容器里的输入框(.item-title → li;子行 p → .input-n-content)
function getInput(labelEl) {
// 尺寸属性行:.item-title 的直接父级是 li;物理属性子行:p 被
包着,同行的输入框在 .input-n-content 里
var row = labelEl.closest ? (labelEl.closest('.input-n-content') || labelEl.closest('li')) : null;
return row ? row.querySelector('input') : null;
}
function editable(input) {
return !!input && !input.disabled && !input.readOnly;
}
function setValue(input, val) {
var setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
try { input.focus({ preventScroll: true }); } catch (e) { try { input.focus(); } catch (e2) { } }
setter.call(input, val);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
try { input.blur(); } catch (e) { }
}
function addBtns(spec, labelEl) {
if (labelEl.getAttribute('data-kjl-qb') === '1') { // 已加过 → 只刷新禁用态
var cur = getInput(labelEl);
[].slice.call(labelEl.querySelectorAll('.kjl-qb')).forEach(function (b) {
b.classList.toggle('kjl-off', !editable(cur));
});
return;
}
labelEl.setAttribute('data-kjl-qb', '1');
var inp0 = getInput(labelEl);
spec.btns.forEach(function (pair) {
var b = document.createElement('span');
b.className = 'kjl-qb' + (editable(inp0) ? '' : ' kjl-off');
b.textContent = pair[0];
b.title = spec.kind === 'set' ? ('设为 ' + pair[1]) : ('当前值 ' + pair[1]);
b.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var input = getInput(labelEl);
if (!editable(input)) return; // 不可修改 → 不执行
if (spec.kind === 'set') {
setValue(input, pair[1]);
} else {
var v = parseFloat(input.value);
if (isNaN(v)) v = 0;
setValue(input, String(Math.round((v + pair[1]) * 1000) / 1000));
}
});
labelEl.appendChild(b);
});
}
function scan() {
injectStyle();
var cands = document.querySelectorAll('.attr-panel .item-title, .attr-panel .input-n-content p');
for (var i = 0; i < cands.length; i++) {
var el = cands[i];
var txt = (el.textContent || '').replace(/\s+/g, '');
if (!txt) continue;
for (var j = 0; j < SPEC.length; j++) {
if (txt === SPEC[j].label) { addBtns(SPEC[j], el); break; }
}
}
}
// ★ 性能:只在左/右栏内部有变动时才重扫;防抖 + 空闲执行 + 低频兜底
var timer = null;
function run() { timer = null; try { scan(); } catch (e) { } }
function schedule() {
if (timer) return;
timer = setTimeout(function () {
timer = null;
if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 800 });
else run();
}, 450);
}
// ★ 性能:只观察右栏/左栏容器本身(挂在 body+subtree 上会被整页 DOM 变动频繁唤醒)
var bound = [];
function bindObserver() {
var targets = [document.querySelector('.attr-panel'), document.querySelector('.parameter-list')];
targets.forEach(function (el) {
if (!el || bound.indexOf(el) >= 0) return;
try { new MutationObserver(schedule).observe(el, { childList: true, subtree: true }); bound.push(el); } catch (e) { }
});
}
bindObserver();
setInterval(function () {
// 容器被重建时重新绑定,并低频兜底扫描
bindObserver();
try { scan(); } catch (e) { }
}, 2000);
schedule();
})();
// KJL后台-部件属性:一键填入「引用名」
// 两类行都支持:
// ① 普通输入框 → 直接写 "#引用名"
// ② 下拉框里带输入框(.attr-select-input 内含 .custom-input input)→ 先点开下拉 → 写入 → 点「确定」
// 注意:这类下拉的浮层常驻行内 DOM(只是隐藏),所以必须用「排除浮层内 input」的方式找真正的输入框。
(function () {
'use strict';
/* ===== 拖拽:按钮之间对调 / 按钮到值控件赋值 ===== */
function readRow(li) {
var inp = directInput(li);
if (inp && !inp.disabled && !inp.readOnly) return inp.value;
var ctl = selectCtl(li);
if (ctl) return ctlText(ctl);
return null;
}
// ===== 直调 React 组件的 onChange:跳过「展开下拉 → 填值 → 点确定」=====
// 原理:属性行的下拉控件是 React 组件,props 里直接有写入点:
// props.onChange({ eventKey: '4/14/xxx', value: '#REF' })
// 实测 0.2ms 完成并正常重渲染。
// ★ 必须带 eventKey(从当前 fiber 取):缺它组件内部会抛
// "Cannot read properties of undefined (reading 'split')"
function reactPropsOf(el) {
if (!el) return null;
var keys;
try { keys = Object.getOwnPropertyNames(el); } catch (e) { return null; }
var k = null;
for (var i = 0; i < keys.length; i++) {
if (keys[i].indexOf('__reactInternalInstance') === 0) { k = keys[i]; break; }
}
if (!k) return null;
var f = el[k], guard = 0;
while (f && guard++ < 16) {
var cands = [f.memoizedProps, f.alternate && f.alternate.memoizedProps];
for (var j = 0; j < cands.length; j++) {
var p = cands[j];
if (p && typeof p.onChange === 'function' && p.eventKey && ('value' in p)) return p;
}
f = f.return;
}
return null;
}
// 油猴脚本跑在隔离世界,看不到 React 挂在 DOM 上的 __reactInternalInstance$,
// 所以把「取 fiber + 调 onChange」这一段注入到页面主世界,用 DOM 自定义事件通信。
function kjlMainAgent() {
document.documentElement.setAttribute('data-kjl-direct', '1');
if (window.__kjlDirectBound) return;
window.__kjlDirectBound = true;
function propsOf(el, expect) {
if (!el) return null;
var keys = Object.getOwnPropertyNames(el), k = null;
for (var i = 0; i < keys.length; i++) {
if (keys[i].indexOf('__reactInternalInstance') === 0) { k = keys[i]; break; }
}
if (!k) return null;
var f = el[k], guard = 0;
while (f && guard++ < 16) {
var cands = [f.memoizedProps, f.alternate && f.alternate.memoizedProps];
for (var j = 0; j < cands.length; j++) {
var p = cands[j];
if (p && typeof p.onChange === 'function' && p.eventKey && ('value' in p)) {
// ★ 校验快照是否属于「当前这一行」:React 双缓冲下 memoizedProps 可能陈旧,
// 不校验就会把值写到别的参数上(表现为"写错行")
if (expect == null || String(p.value) === String(expect)) return p;
}
}
f = f.return;
}
return null;
}
document.addEventListener('kjl-direct-set', function (e) {
var d = (e && e.detail) || {};
var ok = false, err = '';
try {
var el = d.id ? document.getElementById(d.id) : null;
var li = el && el.closest ? el.closest('li.attr-list-item') : null;
// ★ 元素选择:下拉控件 → 行内 input(跳过浮层里的)→ .item-content
// 输入框行只能从
往上层找才能命中属性值组件(从 .item-content 往上找不到)
var ctl = null;
if (li) {
ctl = li.querySelector('.attr-select-input');
if (!ctl) {
var ins = li.querySelectorAll('input');
for (var ti = 0; ti < ins.length; ti++) {
if (!(ins[ti].closest && ins[ti].closest('.attr-select-input-dropdown'))) { ctl = ins[ti]; break; }
}
}
if (!ctl) ctl = li.querySelector('.item-content');
}
// 读该行当前显示值,用于校验 props 快照
var shown = null;
if (li) {
var tw = li.querySelector('.attr-select-input-widget .title');
if (tw) shown = tw.textContent.trim();
else {
var ins2 = li.querySelectorAll('input');
for (var ii = 0; ii < ins2.length; ii++) {
if (!(ins2[ii].closest && ins2[ii].closest('.attr-select-input-dropdown'))) { shown = ins2[ii].value; break; }
}
}
}
var p = ctl ? propsOf(ctl, shown) : null;
if (p && !p.disabled) { p.onChange({ eventKey: p.eventKey, value: String(d.value) }); ok = true; }
else err = p ? 'disabled' : 'no props';
} catch (ex) { err = String(ex && ex.message || ex).slice(0, 80); }
document.dispatchEvent(new CustomEvent('kjl-direct-result', { detail: { reqId: d.reqId, ok: ok, err: err } }));
});
}
var kjlReqSeq = 0;
function ensureMainAgent() {
var root = document.documentElement;
if (root.getAttribute('data-kjl-direct') === '1') return true;
try {
var old = document.getElementById('kjl-direct-agent');
if (old) old.remove();
var s = document.createElement('script');
s.id = 'kjl-direct-agent';
s.textContent = '(' + kjlMainAgent.toString() + ')();';
root.appendChild(s);
} catch (e) { return false; }
return root.getAttribute('data-kjl-direct') === '1';
}
function setViaReact(li, val) {
try {
var label = li.querySelector('.item-title');
var id = label && label.id;
if (!id) return false;
if (!ensureMainAgent()) return false;
var reqId = 'r' + (++kjlReqSeq);
var res = null;
var h = function (e) {
var d = e && e.detail;
if (d && d.reqId === reqId) res = d;
};
document.addEventListener('kjl-direct-result', h);
document.dispatchEvent(new CustomEvent('kjl-direct-set', { detail: { id: id, value: val, reqId: reqId } }));
document.removeEventListener('kjl-direct-result', h);
return !!(res && res.ok);
} catch (e) { return false; }
}
function writeRow(li, val, onDone) {
var inp = directInput(li);
if (inp) {
if (inp.disabled || inp.readOnly) return false;
// ★ 输入框行同样优先走 React onChange(数据层写入,不碰 DOM input);
// 取不到 fiber 时才回退「改 input.value + 派发 input/change」
if (setViaReact(li, val)) {
if (onDone) setTimeout(onDone, 80);
return true;
}
setVal(inp, val);
if (onDone) setTimeout(onDone, 60);
return true;
}
var ctl = selectCtl(li);
if (ctl) {
// ★ 优先直调 React onChange(毫秒级);取不到 fiber 时回退到下拉流程
if (setViaReact(li, val)) {
if (onDone) setTimeout(onDone, 80);
return true;
}
fillViaSelect(li, ctl, val);
if (onDone) setTimeout(onDone, 900); // 等「展开→填值→确定」走完
return true;
}
return false;
}
function rowOf(el) { return el && el.closest ? el.closest('li.attr-list-item') : null; }
function clearHit(box) {
if (!box) return;
[].slice.call(box.querySelectorAll('.kjl-drop-hit')).forEach(function (li) { li.classList.remove('kjl-drop-hit'); });
}
// 取某行「同名参数」的引用名(label.id 形如 attr-list-item-KC1)
function labelRefOf(li) {
try {
var lb = li.querySelector('.item-title') || li.querySelector('p');
var id = lb && lb.id;
if (!id) return '';
var pre = 'attr-list-item-';
return String(id).indexOf(pre) === 0 ? String(id).slice(pre.length) : '';
} catch (e) { return ''; }
}
function doDrop(srcLi, targetEl, srcKind) {
// ★ 用 id 重新取「当前」那一行:行会被站点重渲染,闭包里存的 li 可能是陈旧节点
if (window.__kjlDragSrcId) {
var live = document.getElementById(window.__kjlDragSrcId);
if (live && live.closest) live = live.closest('li.attr-list-item');
if (live) srcLi = live;
}
if (!srcLi) return;
// 【标签文本拖动】拖到另一行 → 交换两行当前值(不区分落点类型)
if (srcKind === 'label') {
var tl = rowOf(targetEl);
if (!tl || tl === srcLi) return;
var av = readRow(srcLi), bv = readRow(tl);
if (av == null || bv == null || av === bv) return;
writeRow(srcLi, bv);
setTimeout(function () { writeRow(tl, av); }, 150);
return;
}
// 【按钮拖动】只允许落在「按钮」或「值控件」上;落在行/标签/空白处一律忽略
var tgtBtn = targetEl && targetEl.closest ? targetEl.closest('.kjl-ref') : null;
var inValue = targetEl && targetEl.closest
? targetEl.closest('.auto-input-container,.attr-select-input,.attr-select-input-widget,.auto-input') : null;
if (!tgtBtn && !inValue) return;
var tgtLi = tgtBtn ? rowOf(tgtBtn) : rowOf(targetEl);
if (!tgtLi || tgtLi === srcLi) return;
if (tgtBtn && !inValue) {
// ① 按钮 → 按钮:取两行各自的「同名参数引用名」,互相设置(不是对调现有值)
var ra = labelRefOf(srcLi), rb = labelRefOf(tgtLi);
if (!ra || !rb || ra === rb) return;
writeRow(srcLi, '#' + rb);
setTimeout(function () { writeRow(tgtLi, '#' + ra); }, 150);
} else {
// ② 按钮 → 值控件:把源行当前值写进去
var srcVal = readRow(srcLi);
if (srcVal == null) return;
writeRow(tgtLi, srcVal);
}
}
// ===== 拖拽预览浮层(显示源行左侧)=====
function tipEsc(s) {
return String(s == null ? '' : s).replace(/[&<>"]/g, function (ch) {
return ch === '&' ? '&' : ch === '<' ? '<' : ch === '>' ? '>' : '"';
});
}
function rowNameOf(li) {
try {
var lb = li.querySelector('.item-title') || li.querySelector('p');
var t = lb ? (lb.getAttribute('title') || lb.textContent || '') : '';
return String(t).replace(/#\S*/g, '').trim();
} catch (e) { return ''; }
}
function buildTip(under, srcLi, kind) {
try {
if (!under || !srcLi) return null;
var tgtBtn = under.closest ? under.closest('.kjl-ref') : null;
var inValue = under.closest ? under.closest('.auto-input-container,.attr-select-input,.attr-select-input-widget,.auto-input') : null;
var tgtLi = tgtBtn ? rowOf(tgtBtn) : rowOf(under);
if (!tgtLi || tgtLi === srcLi) return null;
// 交换:标签拖拽 / 按钮→按钮 → 两行,新值 = 对方引用名
if (kind === 'label' || (tgtBtn && !inValue)) {
var ra = labelRefOf(srcLi), rb = labelRefOf(tgtLi);
if (!ra || !rb || ra === rb) return null;
var pair = [srcLi, tgtLi].sort(function (a, b) {
return (a.getBoundingClientRect().top - b.getBoundingClientRect().top);
});
return {
title: '交换值',
rows: pair.map(function (li) {
return { name: rowNameOf(li), from: readRow(li), to: '#' + (li === srcLi ? rb : ra) };
})
};
}
// 按钮→控件:目标行 原值 → 源行当前值
var sv = readRow(srcLi);
if (sv == null) return null;
return { title: '赋值', rows: [{ name: rowNameOf(tgtLi), from: readRow(tgtLi), to: sv }] };
} catch (e) { return null; }
}
function showTip(srcLi, data, cx, cy) {
try {
var el = document.getElementById('kjl-drag-tip');
if (!data) { if (el) el.style.display = 'none'; return; }
if (!el) {
el = document.createElement('div');
el.id = 'kjl-drag-tip';
document.body.appendChild(el);
}
var key = data.title + '|' + data.rows.map(function (r) { return r.name + '=' + r.from + '>' + r.to; }).join(';');
if (el.__kjlTipKey !== key) {
el.__kjlTipKey = key;
var html = '
' + tipEsc(data.title) + '
';
html += '| 名称 | 原值 | 新值 |
';
data.rows.forEach(function (r) {
html += '| ' + tipEsc(r.name) +
' | ' + tipEsc(r.from) +
' | ' + tipEsc(r.to) + ' |
';
});
html += '
';
el.innerHTML = html;
}
el.style.display = 'block';
var w = el.offsetWidth || 220, h = el.offsetHeight || 90;
var vw = window.innerWidth, vh = window.innerHeight;
var left, top;
if (typeof cx === 'number' && typeof cy === 'number') {
// 水平:以鼠标为中心,夹在视口内
left = cx - w / 2;
left = Math.max(8, Math.min(left, vw - w - 8));
// 垂直:优先放到鼠标上方;上方放不下就放下方
top = cy - h - 14;
if (top < 8) top = cy + 20;
top = Math.max(8, Math.min(top, vh - h - 8));
} else {
// 没给鼠标坐标时退回「源行左侧」
var rc = srcLi.getBoundingClientRect();
left = rc.left - w - 12;
if (left < 4) left = rc.right + 12;
top = Math.max(4, rc.top + rc.height / 2 - h / 2);
}
el.style.left = Math.round(left) + 'px';
el.style.top = Math.round(top) + 'px';
} catch (e) { }
}
// ===== 鼠标自实现拖拽(替代不可靠的 HTML5 draggable)=====
var mdrag = null, suppressClick = false;
function clearHits() {
var box = partContainer();
if (!box) return;
[].slice.call(box.querySelectorAll('.kjl-drop-hit,.kjl-drop-hit-el')).forEach(function (x) {
x.classList.remove('kjl-drop-hit');
x.classList.remove('kjl-drop-hit-el');
});
}
function setHit(el, li) {
clearHits();
if (!el) return;
var hasClosest = el.closest ? true : false;
// 落在图标按钮上 → 高亮按钮本身
var btn = hasClosest ? el.closest('.kjl-ref') : null;
if (btn) { btn.classList.add('kjl-drop-hit-el'); return; }
// 落在值控件上(下拉/输入框)→ 高亮控件本身
var ctl = hasClosest ? el.closest('.attr-select-input,.auto-input-container,.auto-input') : null;
if (ctl) { ctl.classList.add('kjl-drop-hit-el'); return; }
// 其它(标签/整行)→ 高亮整行
if (li) li.classList.add('kjl-drop-hit');
}
function onDragMove(e) {
if (!mdrag) return;
if (!mdrag.active) {
if (Math.abs(e.clientX - mdrag.x0) + Math.abs(e.clientY - mdrag.y0) < 5) return;
mdrag.active = true;
if (mdrag.li) mdrag.li.classList.add('kjl-dragging');
// ★ 拖拽中:全局抓手光标 + 禁止选中文本,并清掉 mousedown 已产生的选区
try { document.documentElement.classList.add('kjl-drag-active'); } catch (x) { }
try { var s0 = window.getSelection(); if (s0 && s0.removeAllRanges) s0.removeAllRanges(); } catch (x) { }
}
e.preventDefault();
var under = document.elementFromPoint(e.clientX, e.clientY);
var li = rowOf(under);
// ★ 按钮只允许落在「按钮」或「值控件」上;落在行/标签上不高亮、不响应
if (mdrag.kind === 'button') {
var dropOk = !!(under && under.closest && (under.closest('.kjl-ref') ||
under.closest('.attr-select-input,.auto-input-container,.auto-input')));
if (!dropOk) {
mdrag.hitEl = null;
mdrag.hitLi = null;
setHit(null, null);
showTip(null, null);
return;
}
}
mdrag.hitEl = (li && li !== mdrag.li) ? under : null;
mdrag.hitLi = (li && li !== mdrag.li) ? li : null;
setHit(mdrag.hitEl, mdrag.hitLi);
// 预览浮层(锚在源行左侧)
showTip(mdrag.li, mdrag.hitEl ? buildTip(mdrag.hitEl, mdrag.li, mdrag.kind) : null, e.clientX, e.clientY);
}
function onDragUp(e) {
if (!mdrag) return;
var d = mdrag;
mdrag = null;
document.removeEventListener('mousemove', onDragMove, true);
document.removeEventListener('mouseup', onDragUp, true);
if (d.li) d.li.classList.remove('kjl-dragging');
try { document.documentElement.classList.remove('kjl-drag-active'); } catch (x) { }
setHit(null);
showTip(null, null);
if (!d.active) return; // 只是点击 → 交给 click 处理器
var under = d.hitEl || document.elementFromPoint(e.clientX, e.clientY);
if (!under) return;
suppressClick = true; // 拖完别再触发「填引用名」
// 只拦落点元素上的这一次 click,避免站点改选中(挂 document 会误伤后续操作)
var kill = function (ev) { ev.preventDefault(); ev.stopPropagation(); ev.stopImmediatePropagation(); };
var kt = (d.hitEl && d.hitEl.addEventListener) ? d.hitEl : null;
if (kt) {
kt.addEventListener('click', kill, true);
setTimeout(function () { try { kt.removeEventListener('click', kill, true); } catch (x) { } }, 500);
}
window.__kjlDragSrcId = d.labelId;
window.__kjlDragSrc = d.li;
doDrop(d.li, under, d.kind);
window.__kjlDragSrcId = '';
window.__kjlDragSrc = null;
e.preventDefault();
e.stopPropagation();
}
function startMouseDrag(e, li, labelId, kind) {
if (e.button !== 0) return;
mdrag = { li: li, labelId: labelId || '', kind: kind || 'button', x0: e.clientX, y0: e.clientY, active: false, hitEl: null, hitLi: null };
document.addEventListener('mousemove', onDragMove, true);
document.addEventListener('mouseup', onDragUp, true);
e.stopPropagation(); // 不惊动站点自己的 mousedown
}
// ★ document 级 mousedown 委托
// 容器级委托会因站点重渲染替换容器而失效(表现为拖拽偶发无响应),所以挂到 document 上。
var dndDelegated = false;
function bindDnd() {
if (dndDelegated) return;
dndDelegated = true;
document.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
var t = e.target;
if (!t || !t.closest) return;
var li = t.closest('li.attr-list-item');
if (!li || !li.closest('.attr-panel')) return; // 只管右栏属性行
var btn = t.closest('.kjl-ref');
if (btn) {
var lb0 = li.querySelector('.item-title');
startMouseDrag(e, li, (lb0 && lb0.id) || '', 'button');
return;
}
var labelEl = t.closest('.item-title');
if (labelEl) startMouseDrag(e, li, labelEl.id || '', 'label');
}, true);
}
function dndStyle() {
if (document.getElementById('kjl-dnd-style')) return;
var st = document.createElement('style');
st.id = 'kjl-dnd-style';
st.textContent = [
'.attr-panel .kjl-ref { cursor: grab; }',
'.attr-panel .kjl-drop-hit-el { outline: 2px solid #1a7af8; outline-offset: 1px; border-radius: 3px; }',
// 拖拽预览浮层(美化版)
'#kjl-drag-tip { position: fixed; z-index: 2147483000; background: #fff; border: 1px solid #d5dbe6;',
' border-radius: 8px; box-shadow: 0 10px 30px rgba(31,35,41,.22), 0 3px 10px rgba(31,35,41,.10);',
' padding: 12px 16px 10px 16px; pointer-events: none; font-size: 13px; line-height: 1.5; color: #1f2329;',
' font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; width: max-content;',
' max-width: 460px; min-width: 180px; }',
'#kjl-drag-tip .kjl-tip-title { font-size: 13px; font-weight: 600; color: #1f2329; margin: 0 0 8px;',
' padding-bottom: 7px; border-bottom: 1px solid #f0f2f5; letter-spacing: .2px; }',
'#kjl-drag-tip table { border-collapse: collapse; }',
'#kjl-drag-tip th { font-size: 12px; font-weight: 500; color: #8f959e; text-align: left;',
' padding: 0 16px 6px 0; white-space: nowrap; }',
'#kjl-drag-tip th:last-child, #kjl-drag-tip td:last-child { padding-right: 0; }',
'#kjl-drag-tip td { font-size: 13px; padding: 5px 16px 5px 0; white-space: nowrap;',
' border-top: 1px solid #f5f7fa; font-variant-numeric: tabular-nums; }',
'#kjl-drag-tip td.kjl-tip-name { color: #1f2329; }',
'#kjl-drag-tip td.kjl-tip-from { color: #8f959e; }',
'#kjl-drag-tip td.kjl-tip-to { color: #2b6cff; font-weight: 600; }',
'#kjl-drag-tip tr:first-child td { border-top: none; padding-top: 2px; }',
// 按钮高亮:实底反色(图标是 currentColor,会自动变白)
'.attr-panel .kjl-ref.kjl-drop-hit-el { background: #1a7af8; color: #fff; border-color: #1a7af8;',
' outline-offset: 0; box-shadow: 0 0 0 3px rgba(26,122,248,.32); border-radius: 3px; }',
// 标签可拖:抓手光标 + 不选中文字
'.attr-panel .item-title { cursor: grab; -webkit-user-select: none; user-select: none; }',
'.attr-panel .item-title:active { cursor: grabbing; }',
// 拖拽进行中:全局抓手 + 禁止选中(避免出现图三那种文字被刷蓝)
'html.kjl-drag-active, html.kjl-drag-active * { cursor: grabbing !important;',
' -webkit-user-select: none !important; user-select: none !important; }',
'.attr-panel .kjl-ref:active { cursor: grabbing; }',
'.attr-panel li.kjl-drop-hit { position: relative; }',
'.attr-panel li.kjl-drop-hit::after { content: ""; position: absolute; left: 0; right: 0; top: 0; bottom: 0;',
' border: 2px solid #1a7af8; border-radius: 3px; pointer-events: none; }',
'.attr-panel li.kjl-dragging { opacity: .5; }'
].join('\n');
(document.head || document.documentElement).appendChild(st);
}
// 左栏「参数配置」:名称 → 引用名
var refCache = null, refCacheAt = 0;
function buildRefMap(force) {
var now = Date.now();
if (!force && refCache && (now - refCacheAt) < 1500) return refCache; // ★ 缓存 1.5s,避免每次扫描都重查左栏
var map = {};
var rows = document.querySelectorAll('.parameter-list .param-row');
for (var i = 0; i < rows.length; i++) {
var rw = rows[i];
if (rw.classList && rw.classList.contains('param-row-title')) continue;
var n = rw.querySelector('.param-row-item-name');
var d = rw.querySelector('.param-row-item-id');
if (!n || !d) continue;
var name = (n.textContent || '').replace(/\s+/g, '');
var ref = (d.textContent || '').trim();
if (name && ref && !map[name]) map[name] = ref;
}
refCache = map; refCacheAt = now;
return map;
}
function partContainer() {
var heads = document.querySelectorAll('.attr-panel .attr-group-header');
for (var i = 0; i < heads.length; i++) {
if (/部件属性/.test(heads[i].textContent || '')) {
return heads[i].closest ? heads[i].closest('li.attr-group-container') : null;
}
}
return null;
}
// 行内真正的输入框(排除下拉浮层里的隐藏 input)
function directInput(li) {
var ins = li.querySelectorAll('input');
for (var i = 0; i < ins.length; i++) {
if (!(ins[i].closest && ins[i].closest('.attr-select-input-dropdown'))) return ins[i];
}
return null;
}
// 下拉控件(只有内部带 .custom-input 的才值得加按钮)
function selectCtl(li) {
// 注意:自定义输入框(.custom-input)是下拉展开后才渲染的,这里不能预先判断
return li.querySelector('.attr-select-input');
}
function ctlText(ctl) {
var t = ctl && ctl.querySelector('.attr-select-input-widget .title');
return (t ? t.textContent : (ctl ? ctl.textContent : '')) || '';
}
function setVal(input, val, noBlur) {
var setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
try { input.focus({ preventScroll: true }); } catch (e) { try { input.focus(); } catch (e2) { } }
setter.call(input, val);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
if (!noBlur) { try { input.blur(); } catch (e) { } }
}
// 打开/收起下拉:站点监听的是 mousedown/mouseup 序列,单纯 .click() 无效
function openDropdown(ctl) {
// 实测:只对 widget 派发一次 click 才能正确切换;叠加 mousedown/mouseup 会「开了又关」
var w = ctl.querySelector('.attr-select-input-widget') || ctl;
try { w.click(); } catch (e) { }
}
// 下拉型:展开 → 写入 custom-input → 点「确定」
function fillViaSelect(li, ctl, val) {
var dd0 = li.querySelector('.attr-select-input-dropdown');
var alreadyOpen = !!(dd0 && dd0.offsetParent !== null);
if (!alreadyOpen) openDropdown(ctl);
var tries = 0;
var timer = setInterval(function () {
tries++;
var dd = li.querySelector('.attr-select-input-dropdown');
var inp = null;
if (dd) {
// ★ 不用 offsetParent 判断可见:下拉浮层是 position:fixed,offsetParent 恒为 null → 永远判成"没展开"
var ci = dd.querySelector('.custom-input input');
if (ci) {
var cs = getComputedStyle(ci), cr = ci.getBoundingClientRect();
if (cs.display !== 'none' && cs.visibility !== 'hidden' && cr.width > 4 && cr.height > 4) inp = ci;
}
}
if (!inp) {
if (tries > 15) { // ~1.2s 仍无自定义输入 → 该下拉不支持
clearInterval(timer);
openDropdown(ctl); // 收起
var label = li.querySelector('.item-title') || li.querySelector('p');
if (label) {
label.setAttribute('data-kjl-noref', '1'); // 标记,避免以后重复尝试
var bb = label.querySelector('.kjl-ref');
if (bb) bb.remove(); // 移除按钮,避免误导
}
}
return;
}
clearInterval(timer);
setVal(inp, val, true); // 不要 blur,否则浮层会关
setTimeout(function () {
var cands = [].slice.call(li.querySelectorAll('.custom-input-btn'))
.concat([].slice.call(document.querySelectorAll('.custom-input-btn')));
var btn = cands.filter(function (b) {
var s = getComputedStyle(b), r = b.getBoundingClientRect();
return s.display !== 'none' && s.visibility !== 'hidden' && r.width > 4 && r.height > 4;
})[0];
if (btn) btn.dispatchEvent(new MouseEvent('click', { view: window, bubbles: true, cancelable: true }));
}, 150);
}, 80);
}
function scan() {
dndStyle();
bindDnd();
var box = partContainer();
if (!box) return;
var map = buildRefMap();
var lis = box.querySelectorAll('li.attr-list-item');
for (var i = 0; i < lis.length; i++) {
var li = lis[i];
var label = li.querySelector('.item-title') || li.querySelector('p');
if (!label) continue;
if (label.getAttribute('data-kjl-drag-src') !== '1') {
label.setAttribute('data-kjl-drag-src', '1');
label.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
// 点在图标按钮上时交给按钮自己处理
if (e.target && e.target.closest && e.target.closest('.kjl-ref')) return;
var lb = e.currentTarget;
var row = lb && lb.closest ? lb.closest('li.attr-list-item') : null;
if (!row) return;
startMouseDrag(e, row, (lb && lb.id) || '', 'label');
});
}
// ★ 带「引用」组合框的行(.multi-input-select-wrap,如「材质」「倒角情况」)用不到这个按钮,
// 并且要清掉此前已加的(组合框的值由下拉选择,不需要填引用名)
if (li.querySelector('.multi-input-select-wrap')) {
var eb = label.querySelector('.kjl-ref');
if (eb) eb.remove();
continue;
}
if (label.getAttribute('data-kjl-ref') === '1') continue; // 已加过
if (label.getAttribute('data-kjl-noref') === '1') continue; // 该下拉不支持自定义输入(已自动排除)
var inp = directInput(li);
var ctl = selectCtl(li);
if (!inp && !ctl) continue; // 无可写控件
var name = (label.textContent || '').replace(/\s+/g, '');
var ref = map[name];
var tip = '';
if (!ref) {
// 兜底:左栏无同名参数时,用当前值的引用名反查
var cur = inp ? (inp.value || '') : ctlText(ctl);
var mm = /^#(.+)$/.exec((cur || '').trim());
if (mm) {
for (var k in map) {
if (map[k] === mm[1]) { name = k; ref = mm[1]; tip = '(左栏无同名参数,按当前引用名)'; break; }
}
}
}
if (!name || !ref) continue;
label.setAttribute('data-kjl-ref', '1');
var b = document.createElement('span');
b.className = 'kjl-qb kjl-ref';
b.innerHTML = '
';
b.title = '设同名参数值';
(function (li, name, labelId) {
b.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var m = buildRefMap(true);
if (!m[name]) return;
var val = '#' + m[name];
var i2 = directInput(li);
if (i2) {
if (i2.disabled || i2.readOnly) return; // 可修改时才执行
// ★ 输入框行也优先走 React onChange(数据层写入,不碰 DOM input)
if (setViaReact(li, val)) return;
setVal(i2, val);
return;
}
var c2 = selectCtl(li);
if (c2) {
// ★ 优先直调 React onChange(毫秒级,不弹下拉);失败才走展开→填值→确定
if (setViaReact(li, val)) return;
fillViaSelect(li, c2, val);
}
});
// 拖拽的 mousedown 由容器级委托统一处理(见 bindDnd),这里不再绑节点级监听
b.addEventListener('click', function (e) {
if (suppressClick) { suppressClick = false; e.preventDefault(); e.stopPropagation(); }
}, true);
})(li, name, label.id || '');
label.appendChild(b);
}
}
// ★ 性能:只在左/右栏内部有变动时才重扫;防抖 + 空闲执行 + 低频兜底
var timer = null;
function run() { timer = null; try { scan(); } catch (e) { } }
function schedule() {
if (timer) return;
timer = setTimeout(function () {
timer = null;
if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 800 });
else run();
}, 450);
}
// ★ 性能:只观察右栏/左栏容器本身(挂在 body+subtree 上会被整页 DOM 变动频繁唤醒)
var bound = [];
function bindObserver() {
var targets = [document.querySelector('.attr-panel'), document.querySelector('.parameter-list')];
targets.forEach(function (el) {
if (!el || bound.indexOf(el) >= 0) return;
try { new MutationObserver(schedule).observe(el, { childList: true, subtree: true }); bound.push(el); } catch (e) { }
});
}
bindObserver();
setInterval(function () {
// 容器被重建时重新绑定,并低频兜底扫描
bindObserver();
try { scan(); } catch (e) { }
}, 2000);
schedule();
})();