// ==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 += '| ' + tipEsc(r.name) +
' | ' + tipEsc(r.from) +
' | ' + tipEsc(r.to) + ' |
';
});
html += '
';
el.innerHTML = html;
}
el.style.display = 'block';
var w = el.offsetWidth || 220, h = el.offsetHeight || 90;
var vw = window.innerWidth, vh = window.innerHeight;
var left, top;
if (typeof cx === 'number' && typeof cy === 'number') {
// 水平:以鼠标为中心,夹在视口内
left = cx - w / 2;
left = Math.max(8, Math.min(left, vw - w - 8));
// 垂直:优先放到鼠标上方;上方放不下就放下方
top = cy - h - 14;
if (top < 8) top = cy + 20;
top = Math.max(8, Math.min(top, vh - h - 8));
} else {
// 没给鼠标坐标时退回「源行左侧」
var rc = srcLi.getBoundingClientRect();
left = rc.left - w - 12;
if (left < 4) left = rc.right + 12;
top = Math.max(4, rc.top + rc.height / 2 - h / 2);
}
el.style.left = Math.round(left) + 'px';
el.style.top = Math.round(top) + 'px';
} catch (e) { }
}
// ===== 鼠标自实现拖拽(替代不可靠的 HTML5 draggable)=====
var mdrag = null, suppressClick = false;
function clearHits() {
var box = partContainer();
if (!box) return;
[].slice.call(box.querySelectorAll('.kjl-drop-hit,.kjl-drop-hit-el')).forEach(function (x) {
x.classList.remove('kjl-drop-hit');
x.classList.remove('kjl-drop-hit-el');
});
}
function setHit(el, li) {
clearHits();
if (!el) return;
var hasClosest = el.closest ? true : false;
// 落在图标按钮上 → 高亮按钮本身
var btn = hasClosest ? el.closest('.kjl-ref') : null;
if (btn) { btn.classList.add('kjl-drop-hit-el'); return; }
// 落在值控件上(下拉/输入框)→ 高亮控件本身
var ctl = hasClosest ? el.closest('.attr-select-input,.auto-input-container,.auto-input') : null;
if (ctl) { ctl.classList.add('kjl-drop-hit-el'); return; }
// 其它(标签/整行)→ 高亮整行
if (li) li.classList.add('kjl-drop-hit');
}
function onDragMove(e) {
if (!mdrag) return;
if (!mdrag.active) {
if (Math.abs(e.clientX - mdrag.x0) + Math.abs(e.clientY - mdrag.y0) < 5) return;
mdrag.active = true;
if (mdrag.li) mdrag.li.classList.add('kjl-dragging');
// ★ 拖拽中:全局抓手光标 + 禁止选中文本,并清掉 mousedown 已产生的选区
try { document.documentElement.classList.add('kjl-drag-active'); } catch (x) { }
try { var s0 = window.getSelection(); if (s0 && s0.removeAllRanges) s0.removeAllRanges(); } catch (x) { }
}
e.preventDefault();
var under = document.elementFromPoint(e.clientX, e.clientY);
var li = rowOf(under);
// ★ 落点必须在「部件属性」分组内,出了这个分组一律不高亮、不响应
if (!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);
})();