// ==UserScript== // @name KJL后台-编缉器功能增强 // @namespace Violentmonkey Scripts // @match https://www.kujiale.com/vc/modeleditor/new* // @grant none // @version 1.9 // @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', '14'); if (window.__kjlDirectVersion === 14) return; window.__kjlDirectVersion = 14; window.__kjlDirectBound = true; // ---- 公式求值 ---- // ★ 复用站点自己的入口:任一 auto-input / formula-result-trigger 组件的 props 上都挂着 // calculateFormula: function (formula, valueType) { return s.formulaCenter.calculateFormula(...) } // 它内部 POST /editor/api/site/formula,返回 { error, value, valueType },异步约 80~160ms。 // 实测与「计算器按钮悬停显示的数值」完全一致(#W→600、#D-#FBH→382、0→0)。 // 注意:props 上的函数闭包指向模块级 formulaCenter,与具体行无关,抓到一次就能算任意表达式。 function getCalc() { if (window.__kjlCalcFn) return window.__kjlCalcFn; var all = document.querySelectorAll('.attr-panel *, .parameter-list *'); for (var i = 0; i < all.length; i++) { var el = all[i]; var kk = Object.getOwnPropertyNames(el).filter(function (x) { return x.indexOf('__reactInternalInstance') === 0; })[0]; if (!kk) continue; var ff = el[kk], gg = 0; while (ff && gg++ < 16) { var cs = [ff.memoizedProps, ff.alternate && ff.alternate.memoizedProps]; for (var j = 0; j < cs.length; j++) { var pp = cs[j]; if (pp && typeof pp.calculateFormula === 'function') { window.__kjlCalcFn = pp.calculateFormula; return window.__kjlCalcFn; } } ff = ff.return; } } return null; } // 取某行的 valueType(int/float/string/bool…),决定按什么类型算 function vtOf(id) { var el = id ? document.getElementById(id) : null; var li = el && el.closest ? el.closest('li.attr-list-item') : null; if (!li) return null; var all = [li].concat([].slice.call(li.querySelectorAll('*'))); for (var i = 0; i < all.length; i++) { var kk = Object.getOwnPropertyNames(all[i]).filter(function (x) { return x.indexOf('__reactInternalInstance') === 0; })[0]; if (!kk) continue; var ff = all[i][kk], gg = 0; while (ff && gg++ < 16) { var cs = [ff.memoizedProps, ff.alternate && ff.alternate.memoizedProps]; for (var j = 0; j < cs.length; j++) { var pp = cs[j]; if (pp && pp.valueType && ('eventKey' in pp)) return String(pp.valueType); } ff = ff.return; } } return null; } async function calcExpr(expr, vt) { var fn = getCalc(); if (!fn) return null; return await fn(String(expr), vt || 'float'); } // ★ 枚举行的选项表:props.options = [{ value:'0', label:'无孔', description:'0' }, …] // (下拉闭合时浮层根本不在 DOM 里,只能从 fiber props 拿) function optionsOf(rowId) { var el = rowId ? document.getElementById(rowId) : null; var li = el && el.closest ? el.closest('li.attr-list-item') : null; if (!li) return null; var all = [li].concat([].slice.call(li.querySelectorAll('*'))); for (var i = 0; i < all.length; i++) { var kk = Object.getOwnPropertyNames(all[i]).filter(function (x) { return x.indexOf('__reactInternalInstance') === 0; })[0]; if (!kk) continue; var ff = all[i][kk], gg = 0; while (ff && gg++ < 16) { var cs = [ff.memoizedProps, ff.alternate && ff.alternate.memoizedProps]; for (var j = 0; j < cs.length; j++) { var p = cs[j]; if (p && Array.isArray(p.options) && p.options.length && p.options[0] && typeof p.options[0] === 'object' && ('label' in p.options[0]) && ('value' in p.options[0])) return p.options; } ff = ff.return; } } return null; } // 展示名 → 选项值(下拉里直接选中的行,显示的是「有孔」,真值是 1) function optionValueOf(rowId, text) { var opts = optionsOf(rowId); if (!opts) return null; var t = String(text == null ? '' : text).trim(); if (!t) return null; for (var i = 0; i < opts.length; i++) { var o = opts[i]; if (!o) continue; if (String(o.label == null ? '' : o.label).trim() === t) { return { value: o.value, label: o.label }; } } return null; } // 算出来的值 → 展示名(找不到返回空串) function labelFor(rowId, val) { var opts = optionsOf(rowId); if (!opts || val == null || val === '') return ''; var s = String(val).trim(); for (var i = 0; i < opts.length; i++) { var o = opts[i]; if (o && String(o.value) === s) return String(o.label == null ? '' : o.label); } // 数值兜底:0 / 0.0 / "0.00" 视为同一个 var num = Number(s); if (s !== '' && !isNaN(num)) { for (var k2 = 0; k2 < opts.length; k2++) { var o2 = opts[k2]; if (!o2) continue; var sv = String(o2.value); if (sv !== '' && !isNaN(Number(sv)) && Number(sv) === num) return String(o2.label == null ? '' : o2.label); } } return ''; } // 从元素向上找属性值组件的 props(带快照校验) // ★ 找「属性值组件」的 props,并确认它确实属于 el 所在这一行。 // 三级判定(之前只用「value === 当前显示文本」,遇到枚举行会失效: // 显示文本是选项名「顶部」,props.value 却是 "0" → 再也写不进去): // ① 命中缓存里记过的 eventKey(同一行 eventKey 稳定) // ② props.value === 当前显示文本 // ③ 整条链上所有候选的 eventKey 完全一致 → 行身份无歧义 // 三条都拿不到就放弃写入,宁可不写也不写错行。 function propsOf(el, expect, rowId) { 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 list = [], 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)) { if (list.indexOf(p) < 0) list.push(p); } } f = f.return; } if (!list.length) return null; var ck = window.__kjlEkCache || (window.__kjlEkCache = {}); if (rowId && ck[rowId]) { for (var i1 = 0; i1 < list.length; i1++) { if (list[i1].eventKey === ck[rowId]) return list[i1]; } } if (expect != null) { for (var i2 = 0; i2 < list.length; i2++) { if (String(list[i2].value) === String(expect)) return list[i2]; } } var ek = list[0].eventKey, same = true; for (var i3 = 1; i3 < list.length; i3++) { if (list[i3].eventKey !== ek) { same = false; break; } } if (same) { if (rowId) ck[rowId] = ek; return list[0]; } return null; } // ★ 参数类型:valueType 就能区分这 8 种(参数弹窗里那个「参数类型」下拉) // float→浮点数 int→整数 string→字符串 boolean→布尔值 // booleanlist→多布尔 material→材质 profile/contour→轮廓 style→样式 var PTYPE = { float: '浮点数', double: '浮点数', number: '浮点数', int: '整数', integer: '整数', long: '整数', string: '字符串', text: '字符串', boolean: '布尔值', bool: '布尔值', booleanlist: '多布尔', boollist: '多布尔', boolarray: '多布尔', material: '材质', texture: '材质', profile: '轮廓', contour: '轮廓', outline: '轮廓', style: '样式' }; function ptypeOf(vt) { if (vt == null) return ''; return PTYPE[String(vt).toLowerCase()] || ''; } async function replyCalc(d, evtName) { var out2 = { reqId: d.reqId, ok: false, value: null, type: null, err: '', label: '', ptype: '' }; try { var vtUsed = d.vt || vtOf(d.id); out2.ptype = ptypeOf(vtUsed); var tree = await calcExpr(d.expr, vtUsed); if (tree && typeof tree === 'object') { if (tree.error) out2.err = 'formula error'; else { out2.ok = true; out2.value = tree.value; out2.type = tree.valueType; if (!out2.ptype) out2.ptype = ptypeOf(tree.valueType); // 枚举行:把数值翻成展示名(「无孔」= 0) try { out2.label = labelFor(d.id, tree.value); } catch (e2) { out2.label = ''; } } } else out2.err = 'no calc'; } catch (ex) { out2.err = String((ex && ex.message) || ex).slice(0, 80); } document.dispatchEvent(new CustomEvent(evtName, { detail: out2 })); } document.addEventListener('kjl-calc', function (e) { replyCalc((e && e.detail) || {}, 'kjl-calc-result'); }); // ---- 左栏「参数配置」的行 ---- // 值:行组件 props.data.value —— 站点已经把公式算好了(如 #CC_FX → 1) // 展示名:下拉行组件 props.options(React 元素:[{props:{value,children}}]); // 本行没选项表时,借用右栏同名属性行的(label id = attr-list-item-) function optLabel(opts, val) { if (!opts || !opts.length || val == null) return ''; var s = String(val).trim(); var getV = function (o) { return o && o.props ? o.props.value : (o ? o.value : null); }; var getL = function (o) { var l = o && o.props ? o.props.children : (o ? o.label : null); if (typeof l === 'string') return l; if (l && l.props && typeof l.props.children === 'string') return l.props.children; return l == null ? '' : String(l); }; for (var i = 0; i < opts.length; i++) { if (String(getV(opts[i])) === s) return getL(opts[i]); } var num = Number(s); if (s !== '' && !isNaN(num)) { for (var k = 0; k < opts.length; k++) { var ov = String(getV(opts[k])); if (ov !== '' && !isNaN(Number(ov)) && Number(ov) === num) return getL(opts[k]); } } return ''; } function leftInfo(ref) { if (!ref) return null; var rows = document.querySelectorAll('.param-row'); for (var i = 0; i < rows.length; i++) { var idEl = rows[i].querySelector('.param-row-item-id'); if (!idEl || String(idEl.textContent || '').trim() !== ref) continue; var cell = rows[i].querySelector('.param-row-item-value-column'); if (!cell) return null; var start = cell.querySelector('input') || cell.firstElementChild || cell; var kk = Object.getOwnPropertyNames(start).filter(function (x) { return x.indexOf('__reactInternalInstance') === 0; })[0]; if (!kk) return null; var data = null, opts = null; var f = start[kk], d = 0; while (f && d++ < 18) { var cs = [f.memoizedProps, f.alternate && f.alternate.memoizedProps]; for (var jj = 0; jj < cs.length; jj++) { var p = cs[jj]; if (!p) continue; if (!data && p.data && typeof p.data === 'object' && p.data.paramName) data = p.data; if (!opts && Array.isArray(p.options) && p.options.length) { var o0 = p.options[0]; if (o0 && o0.props && ('value' in o0.props)) opts = p.options; } } if (data && opts) break; f = f.return; } // 单元格里可能挂着我们自己的 ▾ 触发器,取文本要避开它 var lblEl = cell.querySelector('.pure-value') || cell.querySelector('.tui-select'); var cellTxt = String((lblEl || cell).textContent || '').trim(); // data.value 为空时(如「实际深」= #D,参数字段里没存值)退回单元格文本,文本可能是一条公式 var val = (data && data.value != null && String(data.value) !== '') ? String(data.value) : cellTxt; var label = ''; // ① 下拉行:单元格里显示的就是选中的展示名(最可靠,不用去翻选项表) if (cell.querySelector('.tui-select') && cellTxt && cellTxt !== val) label = cellTxt; // 多布尔:单元格显示的是「全否 / 左前倒角,右前倒角」这种摘要,正好当展示名 if (!label && data && data.valueType === 'booleanlist' && cellTxt && cellTxt !== val) label = cellTxt; // ② 本行组件的 options if (!label) label = optLabel(opts, val); // ③ 借右栏同名属性行的选项表(要求当前选中了部件) if (!label) label = optLabel(optionsOf('attr-list-item-' + ref), val); return { value: val, label: label, type: data ? data.valueType : null }; } return null; } document.addEventListener('kjl-left', async function (e) { var d = (e && e.detail) || {}; var o4 = { reqId: d.reqId, ok: false, value: '', label: '', ptype: '' }; try { var r = leftInfo(d.ref); if (r) { o4.ok = true; o4.ptype = ptypeOf(r.type); // ★ props.data.value 是「原始值」,可能是 #YG_HBL / #W 这种引用,得按本行 valueType 再算一次 var raw = String(r.value == null ? '' : r.value); var val = raw; if (/[#]/.test(raw)) { try { var t = await calcExpr(raw, r.type || 'float'); if (t && typeof t === 'object' && !t.error && t.value != null && t.value !== '') val = String(t.value); } catch (e2) { } } var label = r.label || ''; if (!label) { try { label = labelFor('attr-list-item-' + d.ref, val); } catch (e3) { label = ''; } } o4.value = val; o4.label = (label && label !== val) ? label : ''; } } catch (ex) { } document.dispatchEvent(new CustomEvent('kjl-left-result', { detail: o4 })); }); // ---- 多布尔(booleanlist)参数 ---- // 行的 props.onChange({ eventKey, value }) → rootState.parameterList.editParameterValue(...) // 值的编码是「二进制位」:bit i 对应 valueDisplayNames[i];"3"=0b0011=前两项,"0101"=5=第0、2项 function rowProps(ref) { if (!ref) return null; var rows = document.querySelectorAll('.param-row'); for (var i = 0; i < rows.length; i++) { var idEl = rows[i].querySelector('.param-row-item-id'); if (!idEl || String(idEl.textContent || '').trim() !== ref) continue; var cell = rows[i].querySelector('.param-row-item-value-column'); if (!cell) return null; var start = cell.querySelector('input') || cell.firstElementChild || cell; var kk = Object.getOwnPropertyNames(start).filter(function (x) { return x.indexOf('__reactInternalInstance') === 0; })[0]; if (!kk) return null; var f = start[kk], d = 0; while (f && d++ < 18) { var p = f.memoizedProps; if (p && p.data && p.data.paramName === ref) return p; f = f.return; } return null; } return null; } function multiInfo(ref) { var p = rowProps(ref); if (!p || !p.data) return null; var dd = p.data; return { eventKey: p.eventKey, value: String(dd.value == null ? '' : dd.value), labels: Array.isArray(dd.valueDisplayNames) ? dd.valueDisplayNames.slice() : [], name: dd.displayName || '', disabled: !!p.disabled }; } document.addEventListener('kjl-multi', function (e) { var d = (e && e.detail) || {}; var o5 = { reqId: d.reqId, ok: false, err: '' }; try { var r = multiInfo(d.ref); if (r) { o5.ok = true; o5.eventKey = r.eventKey; o5.value = r.value; o5.labels = r.labels; o5.name = r.name; o5.disabled = r.disabled; } else o5.err = 'no row'; } catch (ex) { o5.err = String((ex && ex.message) || ex).slice(0, 80); } document.dispatchEvent(new CustomEvent('kjl-multi-result', { detail: o5 })); }); document.addEventListener('kjl-multi-set', function (e) { var d = (e && e.detail) || {}; var o6 = { reqId: d.reqId, ok: false, err: '' }; try { var p = rowProps(d.ref); if (!p) o6.err = 'no row'; else if (typeof p.onChange !== 'function') o6.err = 'no onChange'; else if (p.disabled) o6.err = 'disabled'; else { p.onChange({ eventKey: p.eventKey, value: String(d.value) }); o6.ok = true; } } catch (ex) { o6.err = String((ex && ex.message) || ex).slice(0, 80); } document.dispatchEvent(new CustomEvent('kjl-multi-result', { detail: o6 })); }); // 哪些参数是多布尔(带选项) document.addEventListener('kjl-multi-refs', function (e) { var d = (e && e.detail) || {}; var o7 = { reqId: d.reqId, ok: true, refs: [] }; try { var rows = document.querySelectorAll('.param-row'); for (var i = 0; i < rows.length; i++) { var idEl = rows[i].querySelector('.param-row-item-id'); if (!idEl) continue; var ref = String(idEl.textContent || '').trim(); if (!ref) continue; var p = rowProps(ref); if (p && p.data && p.data.valueType === 'booleanlist' && Array.isArray(p.data.valueDisplayNames) && p.data.valueDisplayNames.length && !p.disabled) { o7.refs.push(ref); } } } catch (ex) { } document.dispatchEvent(new CustomEvent('kjl-multi-refs-result', { detail: o7 })); }); // 纯文字行:拿展示名换回选项值(「有孔」→ 1) document.addEventListener('kjl-opt', function (e) { var d = (e && e.detail) || {}; var out3 = { reqId: d.reqId, ok: false, value: null, label: '', ptype: '' }; try { try { out3.ptype = ptypeOf(vtOf(d.id)); } catch (e0) { } var o = optionValueOf(d.id, d.text); if (o) { out3.ok = true; out3.value = o.value; out3.label = o.label; } } catch (ex) { } document.dispatchEvent(new CustomEvent('kjl-opt-result', { detail: out3 })); }); // 兼容旧事件名 document.addEventListener('kjl-eval', function (e) { replyCalc((e && e.detail) || {}, 'kjl-eval-result'); }); 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, d.id) : null; if (p && !p.disabled) { p.onChange({ eventKey: p.eventKey, value: String(d.value) }); if (d.id) { window.__kjlEkCache = window.__kjlEkCache || {}; window.__kjlEkCache[d.id] = p.eventKey; // 行 → eventKey 记住,后续写入不再靠值匹配 } 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; window.__kjlEnsureAgent = function () { try { return ensureMainAgent(); } catch (e) { return false; } }; // 供其它 IIFE 复用 function ensureMainAgent() { var root = document.documentElement; if (root.getAttribute('data-kjl-direct') === '14') 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') === '14'; } 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 (!inPartAttr(srcLi) || !inPartAttr(targetEl)) 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') { var av = readRow(srcLi), bv = readRow(tgtLi); if (av == null || bv == null || av === bv) return null; var pairL = [srcLi, tgtLi].sort(function (a, b) { return (a.getBoundingClientRect().top - b.getBoundingClientRect().top); }); return { title: '交换值', rows: pairL.map(function (li) { return { name: rowNameOf(li), from: readRow(li), to: (li === srcLi ? bv : av) }; }) }; } // ② 按钮→按钮:交换两行的「同名参数引用名」 if (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 (!inPartAttr(under)) { mdrag.hitEl = null; mdrag.hitLi = null; setHit(null, null); showTip(null, null); return; } // ★ 按钮只允许落在「按钮」或「值控件」上;落在行/标签上不高亮、不响应 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; // 只管右栏属性行 if (!inPartAttr(li)) 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; }', // 标签可拖:抓手光标 + 不选中文字 // ⚠️ 必须带 [data-kjl-part="1"]:只给「部件属性」里的行。 // 否则尺寸/物理/系统/前端使用属性那些行的标签也是抓手 —— 看着能拖,其实拖不动。 '.attr-panel li[data-kjl-part="1"] .item-title { cursor: grab; -webkit-user-select: none; user-select: none; }', '.attr-panel li[data-kjl-part="1"] .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; } // ★ 元素是否落在「部件属性」分组里。 // 右栏是若干 li.attr-group-container:尺寸属性 / 物理属性 / 系统属性 / 部件属性 / 前端使用属性… // 引用名按钮和拖放**只允许在部件属性里**,别动尺寸、材质、业务属性那些行。 function inPartAttr(el) { if (!el || !el.closest) return false; var g = el.closest('li.attr-group-container'); if (!g) return false; var h = g.querySelector('.attr-group-header'); return !!(h && /部件属性/.test(h.textContent || '')); } // 行内真正的输入框(排除下拉浮层里的隐藏 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(); // ★ 兜底:清掉「部件属性」之外残留的按钮(分组增删 / 结构变化时可能出现) [].slice.call(document.querySelectorAll('.attr-panel .kjl-ref')).forEach(function (b) { if (inPartAttr(b)) return; var lb = b.parentElement; if (lb && lb.removeAttribute) lb.removeAttribute('data-kjl-ref'); b.remove(); }); // ★ 给「部件属性」里的行打标记(抓手光标 / 禁止选中只看这个标记) [].slice.call(document.querySelectorAll('.attr-panel li.attr-list-item')).forEach(function (one) { if (inPartAttr(one)) { if (one.getAttribute('data-kjl-part') !== '1') one.setAttribute('data-kjl-part', '1'); } else if (one.hasAttribute('data-kjl-part')) { one.removeAttribute('data-kjl-part'); } }); 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(); })(); // ============================================================ // KJL后台-属性控件悬停显示实际值 // 鼠标进入任一可编辑控件时,在鼠标上方显示: // 名称(小灰字)/ 实际值(蓝色大字)/ 引用名(小灰字) // 实际值来源:左栏「参数配置」的「当前值」列,遇到 #REF 递归解析(最多 5 层) // ============================================================ (function () { 'use strict'; var valueMap = null, valueMapAt = 0; function buildValueMap() { var now = Date.now(); if (valueMap && (now - valueMapAt) < 1500) return valueMap; 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 kids = rw.children; if (kids.length < 3) continue; var ref = (kids[1].textContent || '').trim(); if (!ref) continue; var vEl = kids[2]; var inp = vEl.querySelector ? vEl.querySelector('input') : null; var val = inp ? inp.value : (vEl.textContent || '').trim(); if (ref && val !== '' && !(ref in map)) map[ref] = val; } valueMap = map; valueMapAt = now; return map; } // 只有「纯引用」才递归解析:如 #YG_HBL → 18 ;公式(#CC_FX==0?1:0)原样返回 function resolveValue(raw, map, depth) { var s = String(raw == null ? '' : raw).trim(); if (!s || depth > 5) return s; var m = /^#([A-Za-z0-9_]+)$/.exec(s); if (!m) return s; var next = map[m[1]]; if (next == null || next === '') return s; if (String(next).trim() === s) return s; return resolveValue(next, map, depth + 1); } function tipStyle() { if (document.getElementById('kjl-value-tip-style')) return; var st = document.createElement('style'); st.id = 'kjl-value-tip-style'; st.textContent = [ '#kjl-value-tip { position: fixed; z-index: 2147483000; background: #fff;', ' border: 1px solid #d5dbe6; border-radius: 8px; padding: 8px 14px; pointer-events: none;', ' box-shadow: 0 10px 30px rgba(31,35,41,.22), 0 3px 10px rgba(31,35,41,.10);', ' font-size: 13px; color: #1f2329; line-height: 1.5; width: max-content; max-width: 460px;', ' font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }', '#kjl-value-tip .kjl-vt-label { font-size: 12px; color: #8f959e; margin-bottom: 2px; }', '#kjl-value-tip .kjl-vt-main { font-size: 15px; font-weight: 600; color: #2b6cff;', ' font-variant-numeric: tabular-nums; word-break: break-all; }', '#kjl-value-tip .kjl-vt-sub { font-size: 12px; color: #8f959e; margin-top: 3px; }', '#kjl-value-tip .kjl-vt-enum { font-size: 13px; font-weight: 600; color: #1f2329;', ' margin-right: 6px; }', // 参数类型前缀:首字,字号与后面文字一致,无边框,只保留灰色 '#kjl-value-tip .kjl-vt-type { display: inline-block; font-size: 13px; line-height: 1.4;', ' font-weight: 500; color: #86909c; margin-right: 5px; }' ].join('\n'); (document.head || document.documentElement).appendChild(st); } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : '"'; }); } function hideTip() { var el = document.getElementById('kjl-value-tip'); if (el) el.style.display = 'none'; } function showTip(data, cx, cy) { try { tipStyle(); var el = document.getElementById('kjl-value-tip'); if (!el) { el = document.createElement('div'); el.id = 'kjl-value-tip'; document.body.appendChild(el); } // tag:参数类型首字(浮/整/字/布/多/材/轮/样),小一号 + 方框,作为前缀 var tagTxt = data.tag ? String(data.tag).slice(0, 1) : ''; var key = tagTxt + '|' + (data.label || '') + '|' + (data.pre || '') + '|' + data.main + '|' + data.sub; if (el.__k !== key) { el.__k = key; el.innerHTML = (data.label ? '
' + esc(data.label) + '
' : '') + '
' + (tagTxt ? '' + esc(tagTxt) + '' : '') + (data.pre ? '' + esc(data.pre) + '' : '') + esc(data.main) + '
' + (data.sub ? '
' + esc(data.sub) + '
' : ''); } el.style.display = 'block'; var w = el.offsetWidth || 160, h = el.offsetHeight || 60; var vw = window.innerWidth, vh = window.innerHeight; var left = cx - w / 2; left = Math.max(8, Math.min(left, vw - w - 8)); var top = cy - h - 12; if (top < 8) top = cy + 20; // 上方放不下 → 下方 top = Math.max(8, Math.min(top, vh - h - 8)); el.style.left = Math.round(left) + 'px'; el.style.top = Math.round(top) + 'px'; } catch (e) { } } // 取行名 function rowName(li) { var lb = li.querySelector('.item-title') || li.querySelector('p'); if (!lb) return ''; return String(lb.getAttribute('title') || lb.textContent || '').replace(/#\S*/g, '').trim(); } // 取控件当前显示的值 function ctlValue(ctl, li) { var tag = ctl.tagName; if (tag === 'INPUT') return ctl.value; var t = li.querySelector('.attr-select-input-widget .title'); if (t) return t.textContent.trim(); var inp = [].slice.call(li.querySelectorAll('input')).filter(function (i) { return !(i.closest && i.closest('.attr-select-input-dropdown')); })[0]; return inp ? inp.value : (ctl.textContent || '').trim(); } function findCtl(t) { if (!t || !t.closest) return null; // ★ 鼠标在「计算器」按钮上时不要弹值提示:站点自己会在那里显示计算结果, // 我们再来一个就重叠了。顺带排除自建的引用名按钮(它也是「动作」而不是「值」)。 // ★ 左栏「参数配置」的当前值单元格(另一套 DOM:.param-row > .param-row-item-value-column) // 它的下拉浮层 .tui-select-dropdown 里是选项,同样不显示。 if (t.closest('.tui-select-dropdown')) return null; var lcell = t.closest('.param-row-item-value-column'); if (lcell) return lcell; // ★ 下拉浮层内部同样不显示:那里是选项列表(未选择/无孔/有孔 + 自定义参数 + 确定), // 鼠标扫过选项时弹值提示纯属干扰。 if (t.closest('.attr-select-input-dropdown') || t.closest('.formula-result-trigger-container') || t.closest('.formula-result-trigger') || t.closest('.kjl-ref')) return null; var c = t.closest('.attr-select-input') || t.closest('.auto-input-container') || t.closest('.multi-input-select-wrap'); if (c) return c.closest('.attr-panel') ? c : null; if (t.tagName === 'INPUT') { if (t.closest('.attr-select-input-dropdown')) return null; // 浮层内的输入框不算 return t.closest('.attr-panel') ? t : null; } return null; } // ===== 求值:复用站点的 calculateFormula(就是计算器按钮悬停看到的那个值)===== var evalSeq = 0, calcCache = {}, CALC_TTL = 6000; function ensureEvalAgent() { if (document.documentElement.getAttribute('data-kjl-direct') === '14') return true; if (typeof window.__kjlEnsureAgent === 'function') { try { window.__kjlEnsureAgent(); } catch (e) { } } return document.documentElement.getAttribute('data-kjl-direct') === '14'; } // 兜底:纯引用 → 左栏当前值(递归 3 层);否则原样返回 function refFallback(s) { var m0 = /^#([A-Za-z0-9_]+)$/.exec(s); if (m0) { var map = buildValueMap(); var cur = map[m0[1]]; for (var i = 0; i < 3 && cur; i++) { var m2 = /^#([A-Za-z0-9_]+)$/.exec(String(cur).trim()); if (!m2) break; var nx = map[m2[1]]; if (nx == null || nx === cur) break; cur = nx; } if (cur) return cur; } return s; } function fmtCalc(v, s) { if (typeof v === 'number') return String(Math.round(v * 1000) / 1000); if (typeof v === 'boolean') return v ? '是' : '否'; if (v === null || v === undefined || v === '') return refFallback(s); return String(v); } // 异步求值:cb(显示值, 原始内容, 枚举展示名) // 枚举展示名只在该行的 options 里能对上号时才有(「无孔」= 0) function evalValue(raw, cb, rowId) { var s = String(raw == null ? '' : raw).trim(); if (!s) { cb(s, s, ''); return; } // 含引用/数字/运算符 → 走站点的 calculateFormula // 纯文字(「有孔」「平铺」)→ 不能算,但可能是下拉项的显示名,去选项表反查它的值 var isFormula = /[#0-9+\-*/()<>?:=]/.test(s); var ckey = (isFormula ? 'c|' : 'o|') + (rowId || '') + '|' + s; var hit = calcCache[ckey]; if (hit && (Date.now() - hit.t) < CALC_TTL) { cb(hit.v, s, hit.l, hit.p || ''); return; } var fallback = function () { cb(isFormula ? refFallback(s) : s, s, ''); }; if (!ensureEvalAgent()) { fallback(); return; } var reqId = (isFormula ? 'c' : 'o') + (++evalSeq); var evt = isFormula ? 'kjl-calc-result' : 'kjl-opt-result'; var done = false; var h = function (e) { var d = e && e.detail; if (!d || d.reqId !== reqId) return; done = true; document.removeEventListener(evt, h); if (!d.ok) { fallback(); return; } var out, lab; var pt = d.ptype ? String(d.ptype) : ''; if (isFormula) { out = fmtCalc(d.value, s); lab = d.label ? String(d.label) : ''; } else { // 显示名 = 原文,值 = 选项里的 value(「有孔」→ 1) lab = String(d.label == null ? s : d.label); out = (d.value == null || d.value === '') ? lab : String(d.value); if (out === lab) lab = ''; // 值与名相同(平铺=平铺)只显示一次 } if (lab === out) lab = ''; calcCache[ckey] = { v: out, l: lab, p: pt, t: Date.now() }; cb(out, s, lab, pt); }; document.addEventListener(evt, h); try { document.dispatchEvent(new CustomEvent(isFormula ? 'kjl-calc' : 'kjl-opt', { detail: isFormula ? { expr: s, reqId: reqId, id: rowId || '' } : { text: s, reqId: reqId, id: rowId || '' } })); } catch (x) { document.removeEventListener(evt, h); fallback(); return; } // 主世界没回应时别一直等 setTimeout(function () { if (done) return; document.removeEventListener(evt, h); fallback(); }, 1800); } // ---- 左栏「参数配置」:行识别 + 取数(主世界 leftInfo)---- function leftRowOf(el) { if (!el || !el.closest) return null; var cell = el.closest('.param-row-item-value-column'); if (!cell) return null; var row = cell.closest('.param-row'); if (!row) return null; var idEl = row.querySelector('.param-row-item-id'); var ref = idEl ? String(idEl.textContent || '').trim() : ''; return ref ? { cell: cell, ref: ref } : null; } var leftSeq = 0, leftCache = {}; function evalLeft(ref, cb) { var hit = leftCache[ref]; if (hit && (Date.now() - hit.t) < CALC_TTL) { cb(hit.v, hit.l, hit.p || ''); return; } if (!ensureEvalAgent()) { cb('', ''); return; } var reqId = 'L' + (++leftSeq); var done = false; var h = function (e) { var d = e && e.detail; if (!d || d.reqId !== reqId) return; done = true; document.removeEventListener('kjl-left-result', h); if (!d.ok) { cb('', ''); return; } var val = String(d.value == null ? '' : d.value); var lab = d.label ? String(d.label) : ''; var pt = d.ptype ? String(d.ptype) : ''; if (lab === val) lab = ''; leftCache[ref] = { v: val, l: lab, p: pt, t: Date.now() }; cb(val, lab, pt); }; document.addEventListener('kjl-left-result', h); try { document.dispatchEvent(new CustomEvent('kjl-left', { detail: { ref: ref, reqId: reqId } })); } catch (x) { document.removeEventListener('kjl-left-result', h); cb('', ''); return; } setTimeout(function () { if (done) return; document.removeEventListener('kjl-left-result', h); cb('', ''); }, 1500); } var timer = null, tipGen = 0; document.addEventListener('mouseover', function (e) { var ctl = findCtl(e.target); if (!ctl) return; var lrow = leftRowOf(ctl); var li = lrow ? null : ctl.closest('li.attr-list-item'); if (!lrow && !li) return; // 拖拽进行中不显示,避免和拖拽预览浮层打架;自己开的多选弹层同样让路 if (document.documentElement.classList.contains('kjl-drag-active')) return; if (document.documentElement.classList.contains('kjl-mp-on')) { hideTip(); return; } var rect = ctl.getBoundingClientRect(); var cx = e.clientX || Math.round(rect.left + rect.width / 2); var cy = e.clientY || Math.round(rect.top); var gen = ++tipGen; // ★ 代际号:过期的 timer 直接放弃 if (timer) clearTimeout(timer); timer = setTimeout(function () { timer = null; if (gen !== tipGen) return; try { if (document.documentElement.classList.contains('kjl-drag-active')) return; if (lrow) { // 左栏:先秒出单元格文本,再换成「展示名 + 站点算好的值」 var lblEl2 = lrow.cell.querySelector('.pure-value') || lrow.cell.querySelector('.tui-select'); var cellTxt = String((lblEl2 || lrow.cell).textContent || '').trim(); if (cellTxt) showTip({ main: cellTxt }, cx, cy); evalLeft(lrow.ref, function (val, label, ptype) { if (gen !== tipGen) return; var el3 = document.getElementById('kjl-value-tip'); if (!el3 || el3.style.display === 'none') return; if (!val && !label) return; showTip({ tag: ptype, pre: label || '', main: val }, cx, cy); }); return; } var cur = ctlValue(ctl, li); var raw = String(cur == null ? '' : cur).trim(); if (!raw) { hideTip(); return; } var lbl = li.querySelector('.item-title'); var rowId = lbl && lbl.id; // 先按可同步得到的值秒出,等站点算完再替换(避免 hover 时要等 100ms 空白) // ★ 不显示行名:提示框只留「真实值 + 原始内容」 var quick = refFallback(raw); // 只显示「(展示名)+ 真实值」:原始内容(#XXX / 公式)行里本来就显示着,没必要重复 showTip({ main: quick }, cx, cy); evalValue(raw, function (real, _raw, enumLabel, ptype) { if (gen !== tipGen) return; // 已移开/已换行 var el2 = document.getElementById('kjl-value-tip'); if (!el2 || el2.style.display === 'none') return; // enumLabel:枚举行的展示名,显示在数值左边(「无孔 0」) showTip({ tag: ptype, pre: enumLabel || '', main: real }, cx, cy); }, rowId); } catch (x) { window.__kjlTipErr = String((x && x.stack) || x).slice(0, 300); } }, 90); }, true); document.addEventListener('mouseout', function (e) { // 只有「离开到非控件区域」才隐藏;移到另一个控件交给 mouseover if (findCtl(e.relatedTarget)) return; tipGen++; // 让挂起的 timer 失效 if (timer) { clearTimeout(timer); timer = null; } hideTip(); }, true); // 启动时就把主世界代理装上(否则首次 hover 时求值不可用) setTimeout(ensureEvalAgent, 600); setInterval(ensureEvalAgent, 3000); window.addEventListener('scroll', function () { tipGen++; hideTip(); }, true); window.addEventListener('blur', function () { tipGen++; hideTip(); }, true); window.addEventListener('blur', hideTip, true); })(); // ============================================================ // 10. 左栏「多布尔」参数:当前值单元格内嵌下拉式多选控件 // 外观直接复刻站点的 .tui-select(板件正面朝向那种框), // 点整框弹出勾选列表,勾一下立即写入。 // 值编码:十进制整数按位,bit i <-> valueDisplayNames[i] // ============================================================ (function () { 'use strict'; if (!/\/vc\/modeleditor\//.test(location.pathname)) return; var multiSet = {}, multiCount = 0; var seq = 0; var pop = null, popRef = '', popName = '', popLabels = [], popNum = 0; var anchor = null; function ensureAgent() { if (document.documentElement.getAttribute('data-kjl-direct')) return true; if (typeof window.__kjlEnsureAgent === 'function') { try { window.__kjlEnsureAgent(); } catch (e) { } } return !!document.documentElement.getAttribute('data-kjl-direct'); } function waitFor(name, reqId, cb, msec) { var done = false; var h = function (e) { var d = e && e.detail; if (!d || d.reqId !== reqId) return; done = true; document.removeEventListener(name, h); cb(d); }; document.addEventListener(name, h); setTimeout(function () { if (done) return; document.removeEventListener(name, h); cb(null); }, msec || 1500); } function loadRefs(cb) { var id = null; if (!ensureAgent()) { cb(null); return; } id = 'mr' + (++seq); waitFor('kjl-multi-refs-result', id, cb, 1200); try { document.dispatchEvent(new CustomEvent('kjl-multi-refs', { detail: { reqId: id } })); } catch (e) { cb(null); } } function loadOne(ref, cb) { var id = null; if (!ensureAgent()) { cb(null); return; } id = 'm1' + (++seq); waitFor('kjl-multi-result', id, cb, 1200); try { document.dispatchEvent(new CustomEvent('kjl-multi', { detail: { ref: ref, reqId: id } })); } catch (e) { cb(null); } } function writeBits(ref, num, cb) { var id = null; if (!ensureAgent()) { cb(false); return; } id = 'm2' + (++seq); waitFor('kjl-multi-result', id, function (d) { cb(!!(d && d.ok)); }, 1500); try { document.dispatchEvent(new CustomEvent('kjl-multi-set', { detail: { ref: ref, value: String(num), reqId: id } })); } catch (e) { cb(false); } } function numOf(v) { var s = String(v == null ? '' : v).trim(); var n = parseInt(s, 10); return isNaN(n) ? 0 : n; } // ---------- 样式 ---------- function style() { if (document.getElementById('kjl-multi-style')) return; var st = document.createElement('style'); st.id = 'kjl-multi-style'; st.textContent = [ // 只把站点自己的 .pure-value 藏起来,外观交给复刻的 .tui-select '.param-row-item-value-column.kjl-mp-cell > .pure-value { display: none !important; }', '.kjl-mp-select { width: 100%; cursor: pointer; }', '.kjl-mp-select .tui-select-selection-selected-value { overflow: hidden;', ' text-overflow: ellipsis; white-space: nowrap; }', '.kjl-mp-select.kjl-mp-open .tui-select-selection { border-color: #1a7af8 !important; }', // 弹层:配色照抄站点原生下拉(白底 + 蓝框 + 选中项黄底蓝字) '#kjl-multi-pop { position: fixed; z-index: 2147483000; background: #fff;', ' border: 2px solid #0062ff; border-radius: 2px; box-shadow: 0 4px 16px rgba(3, 9, 17, .08);', ' padding: 4px 0; min-width: 150px; max-height: 280px; overflow: auto;', ' font-size: 16px; line-height: 22px; color: rgba(3, 9, 17, .6); }', '#kjl-multi-pop .kjl-mp-item { display: flex; align-items: center; height: 22px; padding: 0 4px;', ' cursor: pointer; white-space: nowrap; }', '#kjl-multi-pop .kjl-mp-item:hover { background: rgba(26, 122, 248, .08); }', '#kjl-multi-pop .kjl-mp-item.on { background: rgba(251, 234, 0, .61); color: #1a7af8; }', '#kjl-multi-pop .kjl-mp-box { flex: none; width: 14px; height: 14px; margin-right: 6px;', ' border: 1px solid rgba(3, 9, 17, .3); border-radius: 2px; position: relative; background: #fff; }', '#kjl-multi-pop .kjl-mp-item.on .kjl-mp-box { background: #1a7af8; border-color: #1a7af8; }', '#kjl-multi-pop .kjl-mp-item.on .kjl-mp-box::after { content: ""; position: absolute; left: 4px; top: 1px;', ' width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); }', '#kjl-multi-pop .kjl-mp-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; }' ].join('\n'); (document.head || document.documentElement).appendChild(st); } // ---------- 复刻站点下拉框 ---------- function buildBox(ref) { var box = document.createElement('div'); box.className = 'tui-select parameter-row-value-column-option-input small-select tui-select-xs tui-select-xs-isChrome kjl-mp-select'; box.setAttribute('data-ref', ref); box.innerHTML = '
' + '
' + '
' + '
' + '' + '' + '
'; box.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); }); box.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openPop(this.getAttribute('data-ref'), this); }); return box; } function syncBox(box, cell) { var pv = cell.querySelector('.pure-value'); var txt = pv ? String(pv.textContent || '').trim() : ''; var v = box.querySelector('.kjl-mp-value'); if (v && v.textContent !== txt) v.textContent = txt; if (pvid(pv) !== box.getAttribute('data-pv')) { box.setAttribute('data-pv', pvid(pv)); box.title = txt; } } function pvid(pv) { if (!pv) return ''; return ( (pv.textContent || '') + '|' + (pv.getAttribute('title') || '') ).trim(); } // ---------- 弹层 ---------- function closePop() { if (anchor) { anchor.classList.remove('kjl-mp-open'); anchor = null; } popRef = ''; if (pop) pop.style.display = 'none'; try { document.documentElement.classList.remove('kjl-mp-on'); } catch (e) { } } function position() { if (!pop || !anchor) return; var r = anchor.getBoundingClientRect(); var w = pop.offsetWidth || 160, h = pop.offsetHeight || 120; var left = Math.min(r.left, window.innerWidth - w - 8); if (left < 8) left = 8; var top = r.bottom + 4; if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - h - 4); pop.style.left = Math.round(left) + 'px'; pop.style.top = Math.round(top) + 'px'; } function paint() { if (!pop) return; var items = pop.querySelectorAll('.kjl-mp-item'); for (var i = 0; i < items.length; i++) { var idx = parseInt(items[i].getAttribute('data-i'), 10); items[i].className = 'kjl-mp-item' + (((popNum >> idx) & 1) ? ' on' : ''); } } function renderPop() { style(); if (!pop) { pop = document.createElement('div'); pop.id = 'kjl-multi-pop'; document.body.appendChild(pop); } pop.innerHTML = ''; for (var i = 0; i < popLabels.length; i++) { (function (idx, lab) { var it = document.createElement('div'); it.className = 'kjl-mp-item' + (((popNum >> idx) & 1) ? ' on' : ''); it.setAttribute('data-i', String(idx)); var box = document.createElement('span'); box.className = 'kjl-mp-box'; var txt = document.createElement('span'); txt.className = 'kjl-mp-txt'; txt.textContent = lab; it.appendChild(box); it.appendChild(txt); it.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); }); it.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); toggle(idx); }); pop.appendChild(it); })(i, popLabels[i]); } pop.style.display = 'block'; try { document.documentElement.classList.add('kjl-mp-on'); } catch (e) { } // 打开瞬间把还挂着的值提示收掉(鼠标没移动不会再触发 mouseover) try { var t0 = document.getElementById('kjl-value-tip'); if (t0) t0.style.display = 'none'; } catch (e) { } position(); } // 站点改完值后,把框上的摘要文字同步过来(否则要等下一轮 scan,最长 2s) function refreshBox() { var box = anchor; if (!box || !box.closest) return; var cell = box.closest('.param-row-item-value-column'); if (cell) syncBox(box, cell); } function toggle(idx) { var bit = 1 << idx; var prev = popNum; popNum = (popNum & bit) ? (popNum & ~bit) : (popNum | bit); paint(); writeBits(popRef, popNum, function (ok) { if (ok) { setTimeout(refreshBox, 200); setTimeout(refreshBox, 700); return; } popNum = prev; paint(); }); } function openPop(ref, box) { if (popRef === ref && pop && pop.style.display === 'block') { closePop(); return; } closePop(); popRef = ref; anchor = box; if (anchor) anchor.classList.add('kjl-mp-open'); loadOne(ref, function (d) { if (!d || !d.ok || popRef !== ref) return; popName = d.name || ''; popLabels = d.labels || []; popNum = numOf(d.value); renderPop(); }); } // ---------- 单元格上挂控件 ---------- function decorate() { if (!multiCount) return; style(); var rows = document.querySelectorAll('.param-row'); for (var i = 0; i < rows.length; i++) { var idEl = rows[i].querySelector('.param-row-id, .param-row-item-id'); if (!idEl) continue; var ref = String(idEl.textContent || '').trim(); if (!ref) continue; var cell = rows[i].querySelector('.param-row-item-value-column'); if (!cell) continue; var box = cell.querySelector('.kjl-mp-select'); if (multiSet[ref]) { cell.classList.add('kjl-mp-cell'); if (!box) { box = buildBox(ref); cell.appendChild(box); } else if (box.getAttribute('data-ref') !== ref) { box.setAttribute('data-ref', ref); } syncBox(box, cell); if (anchor === box && pop && pop.style.display === 'block') position(); } else { cell.classList.remove('kjl-mp-cell'); if (box) box.remove(); } } } var busy = false; function scan() { if (busy) return; if (document.documentElement.classList.contains('kjl-drag-active')) return; busy = true; loadRefs(function (d) { busy = false; if (!d || !d.ok) return; multiSet = {}; multiCount = 0; (d.refs || []).forEach(function (r) { multiSet[r] = 1; multiCount++; }); decorate(); }); } // ---------- 全局关闭 ---------- document.addEventListener('mousedown', function (e) { if (!pop || pop.style.display === 'none') return; var t = e.target; if (t && t.closest && (t.closest('#kjl-multi-pop') || t.closest('.kjl-mp-select'))) return; closePop(); }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' || e.keyCode === 27) closePop(); }, true); window.addEventListener('scroll', function () { closePop(); }, true); window.addEventListener('resize', function () { closePop(); }, true); setTimeout(scan, 1200); setInterval(scan, 2000); })(); // ============================================================ // 11. 结构导航浮动面板:「收起」图标换成图钉(钉住 / 悬停临时显示) // 站点对同一个 elementListPanel 会渲染两个实例: // ① folded 实例 → 只有 .floating-panel-trigger(右上角那个小图标) // ② not-folded 实例 → 标题行(.tui-tabs-bar) + .mola-floating-panel-content(真正的面板) // 图钉放在 ② 的标题行右侧 a.fold-btn 位置;① 那个触发器负责悬停唤出。 // ============================================================ (function () { 'use strict'; if (!/\/vc\/modeleditor\//.test(location.pathname)) return; var PIN_KEY = 'kjl.navPanel.pinned'; var pinned = true; try { if (localStorage.getItem(PIN_KEY) === '0') pinned = false; } catch (e) { } var hideTimer = null; // 图钉:钉住 = 实心,未钉住 = 带内圈(用户提供的两个图标) var PIN_PATH_ON = 'M768 128v85.333333h-42.666667v256l85.333334 128v85.333334h-256v298.666666h-85.333334v-298.666666H213.333333v-85.333334l85.333334-128V213.333333H256V128h512z'; var PIN_PATH_OFF = 'M768 128v85.333333h-42.666667v256l85.333334 128v85.333334h-256v298.666666h-85.333334v-298.666666H213.333333v-85.333334l85.333334-128V213.333333H256V128h512zM384 213.333333v281.856L315.904 597.333333h392.192L640 495.189333V213.333333H384z'; function pinSvg(on) { return ''; } function style() { if (document.getElementById('kjl-nav-style')) return; var st = document.createElement('style'); st.id = 'kjl-nav-style'; st.textContent = [ // 图钉挂在「面板」下而不是 header 下:header 有拖动窗口的 mousedown, // 挂在里面点不动(按下去会拖窗口)。right/top 由 JS 按收起按钮实际位置算。 '.kjl-nav-pin { position: absolute; z-index: 10;', ' display: inline-flex; align-items: center; justify-content: center;', ' width: 18px; height: 22px; border-radius: 3px; cursor: pointer;', ' color: #8f959e; opacity: .9; }', '.kjl-nav-pin:hover { background: rgba(26, 122, 248, .12); color: #1a7af8; opacity: 1; }', '.kjl-nav-pin.on { color: #1a7af8; opacity: 1; }', // 收起:只藏内容区,标题行(图钉所在)保留 '.mola-floating-panel.kjl-nav-off .mola-floating-panel-content { display: none !important; }', '.mola-floating-panel.kjl-nav-off { height: auto !important; min-height: 30px; }' ].join('\n'); (document.head || document.documentElement).appendChild(st); } // 找出「面板本体」和「折叠触发器」两个实例 function panels() { var list = [].slice.call(document.querySelectorAll('.mola-floating-panel')); var full = null, trig = null; for (var i = 0; i < list.length; i++) { var p = list[i]; if (p.querySelector('.element-list-panel') || p.querySelector('.tui-tabs-bar')) full = p; else if (p.querySelector('.floating-panel-trigger')) { if (!trig) trig = p; } // 清掉误加到别的实例上的类 if (p !== full && p.classList.contains('kjl-nav-off')) p.classList.remove('kjl-nav-off'); } return { full: full, trig: trig }; } function foldAnchor(full) { if (!full) return null; var a = full.querySelector('a.fold-btn'); if (a) return a; var icon = full.querySelector('i.param-xiangshangshouqi'); return icon ? icon.parentElement : null; } function setHidden(full, hide) { if (!full) return; if (hide && pinned) return; if (hide) full.classList.add('kjl-nav-off'); else full.classList.remove('kjl-nav-off'); } function paintPin(pin) { if (!pin) return; pin.innerHTML = pinSvg(pinned); pin.className = 'kjl-nav-pin' + (pinned ? ' on' : ''); pin.title = pinned ? '已钉住:面板常显(点击取消钉住)' : '未钉住:鼠标移出即收起(点击钉住)'; } function togglePin(full, pin) { pinned = !pinned; try { localStorage.setItem(PIN_KEY, pinned ? '1' : '0'); } catch (e) { } paintPin(pin); setHidden(full, false); } function ensurePin(full) { if (!full) return; var a = foldAnchor(full); if (!a) return; if (full.querySelector('.kjl-nav-pin')) return; style(); var pin = document.createElement('span'); pin.className = 'kjl-nav-pin' + (pinned ? ' on' : ''); paintPin(pin); pin.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); }, true); pin.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); togglePin(full, pin); }, true); // ★ 必须挂在「面板」下(不能放进 header):header 上有拖动窗口的 mousedown, // 放里面点图钉会被它先接住 → 变成拖窗口。 full.appendChild(pin); placePin(pin, full, a); } // 图钉贴在收起按钮左边(收起按钮是 position:absolute; right:12px,相对 header) function placePin(pin, full, a) { try { if (!pin || !full || !a) return; var pr = full.getBoundingClientRect(); var ar = a.getBoundingClientRect(); if (!pr.width || !ar.width) return; pin.style.right = Math.round(pr.right - ar.left + 2) + 'px'; pin.style.top = Math.round(ar.top - pr.top + (ar.height - 22) / 2) + 'px'; } catch (e) { } } function bindHover(full, trig) { if (full && full.getAttribute('data-kjl-nav-bound') !== '1') { full.setAttribute('data-kjl-nav-bound', '1'); full.addEventListener('mouseenter', function () { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } setHidden(full, false); }); full.addEventListener('mouseleave', function () { if (pinned) return; setHidden(full, true); }); } if (trig && trig.getAttribute('data-kjl-nav-tr') !== '1') { trig.setAttribute('data-kjl-nav-tr', '1'); var tr = trig.querySelector('.floating-panel-trigger'); if (!tr) return; tr.addEventListener('mouseenter', function () { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } if (full) full.classList.remove('kjl-nav-off'); }); tr.addEventListener('mouseleave', function () { if (pinned || !full) return; if (hideTimer) clearTimeout(hideTimer); hideTimer = setTimeout(function () { hideTimer = null; if (pinned) return; // 0.1s 内鼠标已经进到面板里就别收 if (full.matches && full.matches(':hover')) return; full.classList.add('kjl-nav-off'); }, 100); }); } } function scan() { var p = panels(); if (!p.full) return; style(); bindHover(p.full, p.trig); ensurePin(p.full); var pinEl = p.full.querySelector('.kjl-nav-pin'); if (pinEl) placePin(pinEl, p.full, foldAnchor(p.full)); // 钉住 → 常显;未钉住 → 默认就是收起态(刷新后也一样),只有鼠标在面板/触发器上时才展开 if (pinned) { if (p.full.classList.contains('kjl-nav-off')) p.full.classList.remove('kjl-nav-off'); } else { var hoveringPanel = p.full.matches && p.full.matches(':hover'); var trigEl = p.trig && p.trig.querySelector('.floating-panel-trigger'); var hoveringTrig = !!(trigEl && trigEl.matches && trigEl.matches(':hover')); if (!hoveringPanel && !hoveringTrig && !p.full.classList.contains('kjl-nav-off')) { p.full.classList.add('kjl-nav-off'); } } } setTimeout(scan, 1200); setInterval(scan, 1200); })();