// ==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 += ''; }); html += '
名称原值新值
' + tipEsc(r.name) + '' + tipEsc(r.from) + '' + tipEsc(r.to) + '
'; 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(); })();