您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
登录极客版页面后,自动开启助手插件,显示设备与规则的关系,方便查看规则与设备的对应关系,支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置规则列表布局样式等功能。
当前为
// ==UserScript== // @name 米家中枢极客版助手 // @namespace http://tampermonkey.net/ // @version v0.9.1 // @description 登录极客版页面后,自动开启助手插件,显示设备与规则的关系,方便查看规则与设备的对应关系,支持快捷键折叠/展开,关闭,适应画布布局,设备高亮,日志高亮,自动适应画布、设置规则列表布局样式等功能。 // @author 王丰,sk163 // @license MIT // @match http://*/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== /** * v0.8.8更新: * 1. 更改了插件名称 * 2、增加了快捷键,Ctrl/Command+E折叠/展开,Ctrl/Command+Q关闭,Ctrl/Command+B适应画布布局 * 3、自动画布布局修改为仅在初次进入规则编排页面或激活编排页面时触发 * 4、极客版登录后自动启动插件,无需再点击设备列表激活 * 5、修正了一些已知问题 * * v0.8.9更新: * 1、修改了日志高亮逻辑,优化了执行效率,提升了性能,减少无用的循环 * 2、参考了米家自动化极客版样式优化(感谢原作者:lqs1848,https://greasyfork.org/zh-CN/scripts/495833),增加了规则列表样式设置选项,可选择每行显示1-5条规则 * 3、增加了自动折叠窗口的选项 * 4、修正了一些已知问题 * * v0.8.10更新: * 1、修复了窗口过小的问题 * 2、取消了规则列表最小宽度限制 * * v0.8.11更新: * 1、规则列表换行显示 * * v0.8.12更新: * 1、增加了快捷键Ctrl+W,关闭当前的画布 * 2、将快捷键说明增加到了原生的【使用指南】中 * * v0.8.13更新: * 1、修复了一行显示多列时,由于规则名称过长导致显示错位的问题 * * v0.8.14更新: * 1、增加刷新助手按钮,同时增加了快捷键Ctrl+R。查看所有的快捷键,可在极客版画布的【使用指南】中进行查看。 * 2、优化了自动化规则列表在一行多列下的显示效果。 * 3、修正了一些已知问题 * * v0.8.15更新: * 1、修复自动化列表在多列显示时,筛选格式混乱问题(作者:lqs1848) * 2、修复自动化列表在多列显示时,第一个自动化显示错位问题(作者:lqs1848) * * v0.8.16更新: * 1、新增变量按照名称排序(作者:Derstood) * * v0.8.17更新: * 1、修正文本变量排序(作者:Derstood) * * v0.9.0更新: * 1、适配v1.6.0极客版 * 注意:仅兼容极客版v1.6.0版本 * * v0.9.1更新: * 1、增加了对变量视图的支持(变量类型、变量名称、变量值的筛选、变量排序、对应规则编排中的变量卡片高亮显示等) * 2、调整了选项布局 * 3、支持了对表格文本的选择,方便复制 * 注意:仅兼容极客版v1.6.0版本 */ (async () => { const callAPI = (api, params) => { return new Promise(res => editor.gateway.callAPI(api, params, res)); }; let scriptTitle=GM_info.script.name; let scriptVersion=GM_info.script.version; let isInit = false; let selectCardIds = ''; let defaultColor='#43ad7f7f' let defaultWindowWidth=1200; let defaultWindowHeight=600; let defaultRuleStyle='4'; let minWindowWidth=450; let minWindowHeight=100; let enableEnhancedDisplayLog=GM_getValue("enableEnhancedDisplayLog"); let enableAutoFitContent=GM_getValue("enableAutoFitContent"); let enableAutoCollapseCheck=GM_getValue("enableAutoCollapseCheck"); let backgroundColor = GM_getValue("backgroundColor") ; let windowWidth = GM_getValue("windowWidth"); let windowHeight = GM_getValue("windowHeight"); let ruleStyle = GM_getValue("ruleStyle"); if (enableEnhancedDisplayLog === undefined || enableEnhancedDisplayLog === null || enableEnhancedDisplayLog === "") { enableEnhancedDisplayLog = true; } if (enableAutoFitContent === undefined || enableAutoFitContent === null || enableAutoFitContent === "") { enableAutoFitContent = true; } if (enableAutoCollapseCheck === undefined || enableAutoCollapseCheck === null || enableAutoCollapseCheck === "") { enableAutoCollapseCheck = false; } if (backgroundColor === undefined || backgroundColor === null || backgroundColor === "") { backgroundColor = defaultColor; } if (windowWidth === undefined || windowWidth === null || windowWidth === "" || isNaN(windowWidth)) { windowWidth = defaultWindowWidth; } else { windowWidth = parseInt(windowWidth, 10) < minWindowWidth ? minWindowWidth : parseInt(windowWidth, 10); } if (windowHeight === undefined || windowHeight === null || windowHeight === "" || isNaN(windowHeight)) { windowHeight = defaultWindowHeight; } else { windowHeight = parseInt(windowHeight, 10) < minWindowHeight ? minWindowHeight : parseInt(windowHeight, 10) } if (ruleStyle === undefined || ruleStyle === null || ruleStyle === "") { ruleStyle = defaultRuleStyle; } const executeScript = async () => { if (document.getElementById('device-rule-map')||isInit===true) { return; } if (typeof editor === 'undefined' || typeof editor.gateway === 'undefined' || typeof editor.gateway.callAPI === 'undefined') { console.error('editor.gateway.callAPI 方法未定义。请确保在正确的环境中运行此脚本。'); return; } try { isInit=true; const devListResponse = await callAPI('getDevList'); const devMap = devListResponse.devList; const roomNames = Array.from(new Set(Object.values(devMap).map(device => device.roomName))); let varRuleMap = {}; let devRuleMap = {}; let varMap = {}; const scopes = (await callAPI('getVarScopeList',{})).scopes; for(const scope of scopes){ const vars = await callAPI('getVarList',{scope:scope}); Object.entries(vars).forEach(([vid, v]) => { varRuleMap[vid]=[]; varMap[vid]={name:v.userData.name,scope:(scope==="global"?"全局":"规则内"),type:(v.type==="string"?"文本":"数值"),value:v.value} }); } const ruleList = await callAPI('getGraphList'); for (const rule of ruleList) { const content = await callAPI('getGraph', {id: rule.id}); const dids = new Set(content.nodes.map(n => n.props?.did).filter(did => did !== undefined)); const devCards = new Set(content.nodes.map(n => { return (n.props && n.id) ? {did: n.props.did, oriId: n.id} : undefined; }).filter(card => card !== undefined)); dids.forEach(did => { devRuleMap[did] = devRuleMap[did] ?? []; const cardIds = Array.from(devCards) .filter(card => card.did === did) .map(card => card.oriId).join(','); const tempDevRule = { ruleId: rule.id, cardIds: cardIds, totalCardNum: devCards.size }; devRuleMap[did].push(tempDevRule); }); const varCards = new Set(content.nodes.map(node => { return (node.props && node.id && node.props.scope) ? {vid: node.props.id, cid: node.id} : undefined; }).filter(card => card !== undefined)); const varids = new Set(content.nodes.filter(node => node.props && node.props.scope).map(node => node.props.id)); varids.forEach(vid=>{ varRuleMap[vid] = varRuleMap[vid] ?? []; const cardIds = Array.from(varCards) .filter(card => card.vid === vid) .map(card => card.cid).join(','); const varRule = { ruleId: rule.id, cardIds: cardIds, totalCardNum: varCards.size }; varRuleMap[vid].push(varRule); } ); } const devRuleData = Object.fromEntries( Object.entries(devRuleMap).map(([did, devRules]) => [ did, { device: { name: devMap[did]?.name ?? `did: ${did}`, roomName: devMap[did]?.roomName ?? `未知` }, rules: devRules.map(dr => { const rule = ruleList.find(r => r.id === dr.ruleId); return { id: dr.ruleId, cardIds: dr.cardIds, totalCardNum: dr.totalCardNum, name: rule ? rule.userData.name : 'Unknown' }; }) } ]) ); const varRuleData = Object.fromEntries( Object.entries(varRuleMap).map(([vid, varRules]) => [ vid, { rules: varRules.map(vr => { const rule = ruleList.find(r => r.id === vr.ruleId); return { id: vr.ruleId, cardIds: vr.cardIds, totalCardNum: vr.totalCardNum, name: rule ? rule.userData.name : 'Unknown' }; }) } ]) ); const container = document.createElement('div'); container.id = 'device-rule-map'; container.style.position = 'fixed'; container.style.top = '10px'; container.style.right = '40px'; container.style.width = windowWidth+'px'; container.style.height = windowHeight+'px'; container.style.overflowY = 'scroll'; container.style.backgroundColor = 'white'; container.style.border = '1px solid #ccc'; container.style.paddingTop = '45px'; container.style.zIndex = 10000; container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; const topBar = document.createElement('div'); topBar.id = 'topBar'; topBar.style.position = 'fixed'; topBar.style.top = '0'; topBar.style.right = '40px'; topBar.style.width = windowWidth+'px'; topBar.style.height = '38px'; topBar.style.backgroundColor = 'white'; topBar.style.zIndex = 10001; topBar.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; topBar.style.display = 'flex'; topBar.style.justifyContent = 'space-between'; topBar.style.alignItems = 'center'; topBar.style.padding = '0 10px'; const titleDiv = document.createElement('div'); titleDiv.style.display = 'flex'; const title = document.createElement('h2'); title.style.margin = '0'; title.textContent = scriptTitle; titleDiv.appendChild(title); const version = document.createElement('span'); version.textContent = scriptVersion; version.style.marginLeft = '2px'; version.style.paddingTop = '16px'; version.style.fontSize = '9px'; titleDiv.appendChild(version); const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; const optionsButton = document.createElement('button'); optionsButton.id="optionsButton"; optionsButton.textContent = '选项'; optionsButton.title = '选项'; optionsButton.onclick = () => { handleOptionsBtnClick(); }; const collapseButton = document.createElement('button'); collapseButton.id="collapseButton"; collapseButton.textContent = '折叠'; collapseButton.title = '快捷键为Ctrl+E'; collapseButton.onclick = () => { handleCollapseBtnClick(); }; const closeButton = document.createElement('button'); closeButton.id="closeButton"; closeButton.title = '快捷键为Ctrl+Q'; closeButton.textContent = '关闭'; closeButton.onclick = () => { document.body.removeChild(container); isInit=false; } const refreshButton = document.createElement('button'); refreshButton.id="refreshButton"; refreshButton.title = '快捷键为Ctrl+R'; refreshButton.textContent = '刷新'; refreshButton.onclick = () => { document.body.removeChild(container); isInit=false; executeScript(); } buttonContainer.appendChild(optionsButton); buttonContainer.appendChild(collapseButton); buttonContainer.appendChild(refreshButton); buttonContainer.appendChild(closeButton); topBar.appendChild(titleDiv); topBar.appendChild(buttonContainer); const optionsContainer = document.createElement('div'); optionsContainer.id = 'optionsContainer'; optionsContainer.style.display = 'flex'; optionsContainer.style.gap = '10px'; optionsContainer.style.position = 'fixed'; optionsContainer.style.top = '40px'; optionsContainer.style.right = '40px'; optionsContainer.style.width = '890px'; optionsContainer.style.height = '50px'; optionsContainer.style.border = '1px solid #ccc'; optionsContainer.style.backgroundColor = '#ddd'; optionsContainer.style.paddingTop = '10px'; optionsContainer.style.zIndex = 10003; optionsContainer.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; optionsContainer.style.display = 'none'; const widthInput = document.createElement('input'); widthInput.type = 'text'; widthInput.placeholder = windowWidth+'px'; widthInput.style.width = '60px'; widthInput.style.marginBottom = '10px'; widthInput.style.marginLeft = '10px'; widthInput.style.height = '28px'; widthInput.style.borderStyle = 'solid'; widthInput.style.borderWidth = '1px'; widthInput.onchange = () => { windowWidth = parseInt(widthInput.value, 10) < minWindowWidth ? minWindowWidth : parseInt(widthInput.value, 10); GM_setValue("windowWidth", windowWidth); container.style.width = windowWidth + 'px'; topBar.style.width = windowWidth + 'px'; }; const spanW = document.createElement('span'); spanW.textContent = '宽度:'; spanW.style.marginLeft = '10px'; optionsContainer.appendChild(spanW); optionsContainer.appendChild(widthInput); const heightInput = document.createElement('input'); heightInput.type = 'text'; heightInput.placeholder = windowHeight+'px'; heightInput.style.width = '60px'; heightInput.style.marginBottom = '10px'; heightInput.style.marginLeft = '10px'; heightInput.style.height = '28px'; heightInput.style.borderStyle = 'solid'; heightInput.style.borderWidth = '1px'; heightInput.onchange = () => { windowHeight = parseInt(heightInput.value, 10) < minWindowHeight ? minWindowHeight : parseInt(heightInput.value, 10); GM_setValue("windowHeight", windowHeight); container.style.height = windowHeight + 'px'; }; const spanH = document.createElement('span'); spanH.textContent = '高度:'; spanH.style.marginLeft = '10px'; optionsContainer.appendChild(spanH); optionsContainer.appendChild(heightInput); const ruleStyleSelect = document.createElement('select'); ruleStyleSelect.style.marginBottom = '10px'; ruleStyleSelect.style.height = '28px'; ruleStyleSelect.style.marginLeft = '10px'; ruleStyleSelect.style.borderStyle = 'solid'; ruleStyleSelect.style.borderWidth = '1px'; ruleStyleSelect.innerHTML = '<option value="1">每行1列</option>' + '<option value="2">每行2列</option>' + '<option value="3">每行3列</option>' + '<option value="4">每行4列</option>' + '<option value="5">每行5列</option>' ; ruleStyleSelect.onchange = () => { GM_setValue("ruleStyle", ruleStyleSelect.value); changeRuleListStyle(ruleStyleSelect.value); }; const spanS = document.createElement('span'); spanS.textContent = '规则列表:'; spanS.style.marginLeft = '10px'; optionsContainer.appendChild(spanS); optionsContainer.appendChild(ruleStyleSelect); ruleStyleSelect.value=ruleStyle; changeRuleListStyle(ruleStyle); const colorInput = document.createElement('input'); colorInput.type = 'text'; colorInput.placeholder=defaultColor; colorInput.style.width = '80px'; colorInput.style.marginBottom = '10px'; colorInput.style.marginLeft = '10px'; colorInput.style.height = '28px'; colorInput.style.borderStyle = 'solid'; colorInput.style.borderWidth = '1px'; colorInput.oninput = () => { backgroundColor = colorInput.value; GM_setValue("backgroundColor", backgroundColor); }; const spanC = document.createElement('span'); spanC.textContent = '卡片颜色:'; spanC.style.marginLeft = '10px'; optionsContainer.appendChild(spanC); optionsContainer.appendChild(colorInput); const logLabel = document.createElement('label'); logLabel.htmlFor = 'highlightLogCheck'; logLabel.appendChild(document.createTextNode('日志高亮')); logLabel.style.marginBottom = '10px'; logLabel.style.marginLeft = '10px'; optionsContainer.appendChild(logLabel); const highlightLogCheck = document.createElement('input'); highlightLogCheck.type = 'checkbox'; highlightLogCheck.id = 'highlightLogCheck'; highlightLogCheck.checked=enableEnhancedDisplayLog; highlightLogCheck.style.marginLeft = '2px'; highlightLogCheck.onchange=function() { enableEnhancedDisplayLog = highlightLogCheck.checked; GM_setValue("enableEnhancedDisplayLog", enableEnhancedDisplayLog); }; optionsContainer.appendChild(highlightLogCheck); const fitLabel = document.createElement('label'); fitLabel.htmlFor = 'autoFitCheck'; fitLabel.appendChild(document.createTextNode('自动画布')); fitLabel.style.marginBottom = '10px'; fitLabel.style.marginLeft = '10px'; optionsContainer.appendChild(fitLabel); const autoFitCheck = document.createElement('input'); autoFitCheck.type = 'checkbox'; autoFitCheck.id = 'autoFitCheck'; autoFitCheck.checked=enableAutoFitContent; autoFitCheck.style.marginLeft = '2px'; autoFitCheck.onchange=function() { enableAutoFitContent = autoFitCheck.checked; GM_setValue("enableAutoFitContent", enableAutoFitContent); }; optionsContainer.appendChild(autoFitCheck); const autoCollapseLabel = document.createElement('label'); autoCollapseLabel.htmlFor = 'autoCollapseCheck'; autoCollapseLabel.appendChild(document.createTextNode('自动折叠')); autoCollapseLabel.style.marginBottom = '10px'; autoCollapseLabel.style.marginLeft = '10px'; optionsContainer.appendChild(autoCollapseLabel); const autoCollapseCheck = document.createElement('input'); autoCollapseCheck.type = 'checkbox'; autoCollapseCheck.id = 'autoCollapseCheck'; autoCollapseCheck.checked=enableAutoCollapseCheck; autoCollapseCheck.style.marginLeft = '2px'; autoCollapseCheck.onchange=function() { enableAutoCollapseCheck = autoCollapseCheck.checked; GM_setValue("enableAutoCollapseCheck", enableAutoCollapseCheck); autoCollapse(); }; optionsContainer.appendChild(autoCollapseCheck); container.appendChild(optionsContainer); //设备表格 const devTable = document.createElement('table'); devTable.id = 'devTable'; devTable.border = '1'; devTable.cellSpacing = '0'; devTable.cellPadding = '5'; devTable.style.width = '100%'; devTable.style.userSelect = 'text'; const devThead = document.createElement('thead'); const devHeaderRow = document.createElement('tr'); const devRoomHeader = document.createElement('th'); const devDeviceHeader = document.createElement('th'); const devRuleHeader = document.createElement('th'); const devTbody = document.createElement('tbody'); let roomSortOrder = 'asc'; let deviceSortOrder = 'asc'; let ruleSortOrder = 'asc'; const devUpdateSortMarkers = () => { devRoomHeader.innerHTML = `房间 ${roomSortOrder === 'asc' ? '⬆️' : '⬇️'}`; devDeviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`; devRuleHeader.innerHTML = `规则 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`; }; devRoomHeader.textContent = '房间'; devRoomHeader.style.textWrap= 'nowrap'; devDeviceHeader.textContent = '设备'; devDeviceHeader.style.textWrap = 'nowrap'; devRuleHeader.textContent = '规则'; devRoomHeader.onclick = () => { roomSortOrder = roomSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(devTbody,0, roomSortOrder); devUpdateSortMarkers(); }; devDeviceHeader.onclick = () => { deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(devTbody,1, deviceSortOrder); devUpdateSortMarkers(); }; devRuleHeader.onclick = () => { ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(devTbody,2, ruleSortOrder); devUpdateSortMarkers(); }; devHeaderRow.appendChild(devRoomHeader); devHeaderRow.appendChild(devDeviceHeader); devHeaderRow.appendChild(devRuleHeader); devThead.appendChild(devHeaderRow); devTable.appendChild(devThead); const devFilterContainer = document.createElement('span'); devFilterContainer.id = 'devFilterContainer'; devFilterContainer.style.display = ''; const roomFilterSelect = document.createElement('select'); roomFilterSelect.style.marginBottom = '10px'; roomFilterSelect.style.height = '28px'; roomFilterSelect.style.borderStyle = 'solid'; roomFilterSelect.style.borderWidth = '1px'; roomFilterSelect.innerHTML = `<option value="">所有房间</option>` + roomNames.map(room => `<option value="${room}">${room}</option>`).join(''); roomFilterSelect.onchange = () => { filterDevTable(roomFilterSelect.value,deviceFilterInput.value, devRuleFilterInput.value); }; const deviceFilterInput = document.createElement('input'); deviceFilterInput.type = 'text'; deviceFilterInput.placeholder = '设备筛选'; deviceFilterInput.style.width = '200px'; deviceFilterInput.style.marginBottom = '10px'; deviceFilterInput.style.marginLeft = '10px'; deviceFilterInput.style.height = '28px'; deviceFilterInput.style.borderStyle = 'solid'; deviceFilterInput.style.borderWidth = '1px'; deviceFilterInput.oninput = () => { filterDevTable(roomFilterSelect.value,deviceFilterInput.value, devRuleFilterInput.value); }; const devRuleFilterInput = document.createElement('input'); devRuleFilterInput.type = 'text'; devRuleFilterInput.placeholder = '规则筛选'; devRuleFilterInput.style.width = '200px'; devRuleFilterInput.style.marginBottom = '10px'; devRuleFilterInput.style.marginLeft = '10px'; devRuleFilterInput.style.height = '28px'; devRuleFilterInput.style.borderStyle = 'solid'; devRuleFilterInput.style.borderWidth = '1px'; devRuleFilterInput.oninput = () => { filterDevTable(roomFilterSelect.value,deviceFilterInput.value, devRuleFilterInput.value); }; devFilterContainer.appendChild(roomFilterSelect); devFilterContainer.appendChild(deviceFilterInput); devFilterContainer.appendChild(devRuleFilterInput); container.appendChild(devFilterContainer); const varFilterContainer = document.createElement('span'); varFilterContainer.id = 'varFilterContainer'; varFilterContainer.style.display = 'none'; const varScopeFilterSelect = document.createElement('select'); varScopeFilterSelect.style.marginBottom = '10px'; varScopeFilterSelect.style.height = '28px'; varScopeFilterSelect.style.marginLeft = '10px'; varScopeFilterSelect.style.borderStyle = 'solid'; varScopeFilterSelect.style.borderWidth = '1px'; varScopeFilterSelect.innerHTML = '<option value="">所有变量范围</option>' + '<option value="全局">全局</option>' + '<option value="规则内">规则内</option>' ; varScopeFilterSelect.onchange = () => { filterVarTable(varScopeFilterSelect.value,varTypeFilterSelect.value,varNameFilterInput.value,varValueFilterInput.value,varRuleFilterInput.value); }; const varTypeFilterSelect = document.createElement('select'); varTypeFilterSelect.style.marginBottom = '10px'; varTypeFilterSelect.style.height = '28px'; varTypeFilterSelect.style.marginLeft = '10px'; varTypeFilterSelect.style.borderStyle = 'solid'; varTypeFilterSelect.style.borderWidth = '1px'; varTypeFilterSelect.innerHTML = '<option value="">所有变量类型</option>' + '<option value="文本">文本</option>' + '<option value="数值">数值</option>' ; varTypeFilterSelect.onchange = () => { filterVarTable(varScopeFilterSelect.value,varTypeFilterSelect.value,varNameFilterInput.value,varValueFilterInput.value,varRuleFilterInput.value); }; const varNameFilterInput = document.createElement('input'); varNameFilterInput.type = 'text'; varNameFilterInput.placeholder = '变量名称筛选'; varNameFilterInput.style.width = '200px'; varNameFilterInput.style.marginBottom = '10px'; varNameFilterInput.style.marginLeft = '10px'; varNameFilterInput.style.height = '28px'; varNameFilterInput.style.borderStyle = 'thin'; varNameFilterInput.style.borderWidth = '1px'; varNameFilterInput.oninput = () => { filterVarTable(varScopeFilterSelect.value,varTypeFilterSelect.value,varNameFilterInput.value,varValueFilterInput.value,varRuleFilterInput.value); }; const varValueFilterInput = document.createElement('input'); varValueFilterInput.type = 'text'; varValueFilterInput.placeholder = '变量值称筛选'; varValueFilterInput.style.width = '200px'; varValueFilterInput.style.marginBottom = '10px'; varValueFilterInput.style.marginLeft = '10px'; varValueFilterInput.style.height = '28px'; varValueFilterInput.style.borderStyle = 'thin'; varValueFilterInput.style.borderWidth = '1px'; varValueFilterInput.oninput = () => { filterVarTable(varScopeFilterSelect.value,varTypeFilterSelect.value,varNameFilterInput.value,varValueFilterInput.value,varRuleFilterInput.value); }; const varRuleFilterInput = document.createElement('input'); varRuleFilterInput.type = 'text'; varRuleFilterInput.placeholder = '规则筛选'; varRuleFilterInput.style.width = '200px'; varRuleFilterInput.style.marginBottom = '10px'; varRuleFilterInput.style.marginLeft = '10px'; varRuleFilterInput.style.height = '28px'; varRuleFilterInput.style.borderStyle = 'solid'; varRuleFilterInput.style.borderWidth = '1px'; varRuleFilterInput.oninput = () => { filterVarTable(varScopeFilterSelect.value,varTypeFilterSelect.value,varNameFilterInput.value,varValueFilterInput.value,varRuleFilterInput.value); }; varFilterContainer.appendChild(varScopeFilterSelect); varFilterContainer.appendChild(varTypeFilterSelect); varFilterContainer.appendChild(varNameFilterInput); varFilterContainer.appendChild(varValueFilterInput); varFilterContainer.appendChild(varRuleFilterInput); container.appendChild(varFilterContainer); const modeButton = document.createElement('button'); modeButton.id="modeButton"; modeButton.textContent = '切换至变量视图'; modeButton.title = '切换模式'; modeButton.style.marginRight = '15px'; modeButton.style.float = 'right'; modeButton.style.padding = '5px'; modeButton.onclick = () => { handleModeBtnClick(); }; container.appendChild(modeButton); Object.entries(devRuleData).forEach(([did, data]) => { const device=data.device; const rules=data.rules; const row = document.createElement('tr'); const roomCell = document.createElement('td'); roomCell.textContent = device.roomName; roomCell.style.textWrap= 'nowrap'; const deviceCell = document.createElement('td'); deviceCell.textContent = device.name; deviceCell.style.textWrap= 'nowrap'; const ruleCell = document.createElement('td'); const host = window.location.host; let sequence = 0; rules.forEach(rule => { const link = document.createElement('a'); link.href = `http://${host}/#/graph/${rule.id}`; link.target = '_self'; link.textContent = ++sequence+"、"+rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]"; link.onclick = () => { window.location.hash = '#/'; selectCardIds = rule.cardIds; }; ruleCell.appendChild(link); ruleCell.appendChild(document.createElement('br')); }); row.appendChild(roomCell); row.appendChild(deviceCell); row.appendChild(ruleCell); devTbody.appendChild(row); }); devTable.appendChild(devTbody); //变量表格 const varTable = document.createElement('table'); varTable.id = 'varTable'; varTable.border = '1'; varTable.cellSpacing = '0'; varTable.cellPadding = '5'; varTable.style.width = '100%'; varTable.style.userSelect = 'text'; const varThead = document.createElement('thead'); const varHeaderRow = document.createElement('tr'); const varScopeHeader = document.createElement('th'); const varTypeHeader = document.createElement('th'); const varNameHeader = document.createElement('th'); const varValueHeader = document.createElement('th'); const varRuleHeader = document.createElement('th'); const varTbody = document.createElement('tbody'); let varScopeSortOrder = 'asc'; let varTypeSortOrder = 'asc'; let varNameSortOrder = 'asc'; let varValueSortOrder = 'asc'; let varRuleSortOrder = 'asc'; const varUpdateSortMarkers = () => { varScopeHeader.innerHTML = `变量范围 ${varScopeSortOrder === 'asc' ? '⬆️' : '⬇️'}`; varTypeHeader.innerHTML = `变量类型 ${varTypeSortOrder === 'asc' ? '⬆️' : '⬇️'}`; varNameHeader.innerHTML = `变量名 ${varNameSortOrder === 'asc' ? '⬆️' : '⬇️'}`; varValueHeader.innerHTML = `变量值 ${varValueSortOrder === 'asc' ? '⬆️' : '⬇️'}`; varRuleHeader.innerHTML = `规则 ${varRuleSortOrder === 'asc' ? '⬆️' : '⬇️'}`; }; varScopeHeader.textContent = '变量范围'; varScopeHeader.style.textWrap= 'nowrap'; varTypeHeader.textContent = '变量类型'; varTypeHeader.style.textWrap= 'nowrap'; varNameHeader.textContent = '变量名'; varNameHeader.style.textWrap = 'nowrap'; varValueHeader.textContent = '变量值'; varValueHeader.style.textWrap = 'nowrap'; varRuleHeader.textContent = '规则'; varRuleHeader.style.textWrap = 'nowrap'; varScopeHeader.onclick = () => { varScopeSortOrder = varScopeSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(varTbody,0, varScopeSortOrder); varUpdateSortMarkers(); }; varTypeHeader.onclick = () => { varTypeSortOrder = varTypeSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(varTbody,1, varTypeSortOrder); varUpdateSortMarkers(); }; varNameHeader.onclick = () => { varNameSortOrder = varNameSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(varTbody,2, varNameSortOrder); varUpdateSortMarkers(); }; varValueHeader.onclick = () => { varValueSortOrder = varValueSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(varTbody,3, varValueSortOrder); varUpdateSortMarkers(); }; varRuleHeader.onclick = () => { varRuleSortOrder = varRuleSortOrder === 'asc' ? 'desc' : 'asc'; sortTable(varTbody,4, varRuleSortOrder); varUpdateSortMarkers(); }; varHeaderRow.appendChild(varScopeHeader); varHeaderRow.appendChild(varTypeHeader); varHeaderRow.appendChild(varNameHeader); varHeaderRow.appendChild(varValueHeader); varHeaderRow.appendChild(varRuleHeader); varThead.appendChild(varHeaderRow); varTable.appendChild(varThead); Object.entries(varRuleData).forEach(([vid, data]) => { const rules=data.rules; const varData=varMap[vid]; const row = document.createElement('tr'); const varScopeCell = document.createElement('td'); varScopeCell.textContent = varData.scope; varScopeCell.style.textWrap= 'nowrap'; const varTypeCell = document.createElement('td'); varTypeCell.textContent = varData.type; varTypeCell.style.textWrap= 'nowrap'; const varNameCell = document.createElement('td'); varNameCell.textContent = varData.name; varNameCell.style.textWrap= 'nowrap'; const varValueCell = document.createElement('td'); varValueCell.textContent = varData.value; varValueCell.style.textWrap= 'nowrap'; const varRuleCell = document.createElement('td'); const host = window.location.host; let sequence = 0; rules.forEach(rule => { const link = document.createElement('a'); link.href = `http://${host}/#/graph/${rule.id}`; link.target = '_self'; link.textContent = ++sequence+"、"+rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]"; link.onclick = () => { window.location.hash = '#/'; selectCardIds = rule.cardIds; }; varRuleCell.appendChild(link); varRuleCell.appendChild(document.createElement('br')); }); row.appendChild(varScopeCell); row.appendChild(varTypeCell); row.appendChild(varNameCell); row.appendChild(varValueCell); row.appendChild(varRuleCell); varTbody.appendChild(row); }); varTable.appendChild(varTbody); container.appendChild(topBar); container.appendChild(devTable); container.appendChild(varTable); document.body.appendChild(container); function sortTable(tbody,columnIndex, sortOrder) { const rows = Array.from(tbody.rows); const sortedRows = rows.sort((a, b) => { const aText = a.cells[columnIndex].textContent; const bText = b.cells[columnIndex].textContent; if (sortOrder === 'asc') { return aText.localeCompare(bText); } else { return bText.localeCompare(aText); } }); tbody.innerHTML = ''; sortedRows.forEach(row => tbody.appendChild(row)); } devUpdateSortMarkers(); varUpdateSortMarkers(); function filterDevTable(roomName,deviceKeyword, ruleKeyword) { const rows = Array.from(devTable.rows); rows.forEach(row => { const roomText = row.cells[0].textContent; const deviceText = row.cells[1].textContent.toLowerCase(); const ruleText = row.cells[2].textContent.toLowerCase(); if ((roomName === '' || roomText === roomName) && deviceText.includes(deviceKeyword.toLowerCase()) && ruleText.includes(ruleKeyword.toLowerCase())) { row.style.display = ''; } else { row.style.display = 'none'; } }); } function filterVarTable(varScope,varType, varName,varValue, ruleName) { const rows = Array.from(varTable.rows); rows.forEach(row => { const varScopeText = row.cells[0].textContent; const varTypeText = row.cells[1].textContent; const varNameText = row.cells[2].textContent.toLowerCase(); const varValueText = row.cells[3].textContent.toLowerCase(); const ruleNameText = row.cells[4].textContent.toLowerCase(); if ((varScope === '' || varScope === varScopeText) && (varType === '' || varType === varTypeText) && varNameText.includes(varName.toLowerCase()) && varValueText.includes(varValue.toLowerCase()) && ruleNameText.includes(ruleName.toLowerCase())) { row.style.display = ''; } else { row.style.display = 'none'; } }); } autoCollapse(); } catch (error) { isInit=false; console.error('调用 API 时出错:', error); } }; const selectDevices = async () => { await sleep(1000); const cardIds = selectCardIds.split(','); for (const cardId of cardIds) { if (cardId.trim() !== '') { let targetElement = document.querySelector( "[id='"+cardId.trim()+"'] > div > div"); if (targetElement) { targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor; } } } selectCardIds = ''; }; function handleModeBtnClick() { const varTable = document.getElementById('varTable'); const devTable = document.getElementById('devTable'); const modeButton = document.getElementById('modeButton'); const devFilterContainer = document.getElementById('devFilterContainer'); const varFilterContainer = document.getElementById('varFilterContainer'); if (varTable && devTable && modeButton) { if (modeButton.textContent === "切换至变量视图") { devTable.style.display = 'none'; varTable.style.display = ''; devFilterContainer.style.display = 'none'; varFilterContainer.style.display = ''; modeButton.textContent = '切换至设备视图'; }else{ devTable.style.display = ''; varTable.style.display = 'none'; devFilterContainer.style.display = ''; varFilterContainer.style.display = 'none'; modeButton.textContent = '切换至变量视图'; } } } function handleOptionsBtnClick() { const optionsContainer = document.getElementById('optionsContainer'); if (optionsContainer) { if(optionsContainer.style.display === 'none'){ optionsContainer.style.display = ''; }else{ optionsContainer.style.display = 'none'; } } } function handleCollapseBtnClick() { const container = document.getElementById('device-rule-map'); if (container) { if (container.style.height === windowHeight + 'px') { collapse(); }else{ expand(); } } } function collapse() { const container = document.getElementById('device-rule-map'); if (container) { const collapseButton = document.getElementById('collapseButton'); const topBar = document.getElementById('topBar'); topBar.style.width = '450px'; container.style.height = '0px'; container.style.width = '0px'; collapseButton.textContent = '展开'; } } function expand() { const container = document.getElementById('device-rule-map'); if (container) { const collapseButton = document.getElementById('collapseButton'); const topBar = document.getElementById('topBar'); topBar.style.width = windowWidth + 'px'; container.style.width = windowWidth + 'px'; container.style.height = windowHeight + 'px'; collapseButton.textContent = '折叠'; } } function autoFitContent() { if(enableAutoFitContent && editor && editor.transformTool){ editor.transformTool.fitToBestPos(); } } function autoCollapse() { if(enableAutoCollapseCheck){ collapse(); } } function enhancedDisplayLog() { //监听画布变化 const canvas = document.getElementById('canvas-root'); if (canvas) { const config = {attributes: false, childList: true, subtree: true}; const callback = function (mutationsList, observer) { if (enableEnhancedDisplayLog) { let element = document.querySelector('.panel-log-card-blink'); if (element && element.style.outline !== "red solid 20px") { element.style.outline = "red solid 10px"; } let animateElement = document.querySelector('animate'); if (animateElement && animateElement.getAttribute('stroke-width') != '10') { let pathElement = animateElement.parentElement; pathElement.setAttribute('stroke-width', '10'); if (pathElement) { let gElement = pathElement.parentElement; gElement.setAttribute('stroke', 'red'); } } } }; const observer = new MutationObserver(callback); observer.observe(canvas, config); } } function addShortcutHelp(){ const addedSchortcut = document.getElementById('addedSchortcut'); if(addedSchortcut){ return; } const helpLeft = document.querySelector('.help-modal-left'); if(helpLeft) { const leftContent = ` <div></div> <div class="help-modal-item-label" id="addedSchortcut">自适应画布</div> <div class="help-modal-item-btn">Ctrl 或 ⌘</div> <div class="help-modal-item-btn" style="width: 41px;">B</div> <div class="help-modal-item-label">关闭画布</div> <div class="help-modal-item-btn">Ctrl</div> <div class="help-modal-item-btn" style="width: 41px;">W</div> <div class="help-modal-item-label">关闭助手</div> <div class="help-modal-item-btn">Ctrl</div> <div class="help-modal-item-btn" style="width: 41px;">Q</div> `; helpLeft.insertAdjacentHTML('beforeend', leftContent); } const helpRight = document.querySelector('.help-modal-right'); if(helpRight) { const rightContent = ` <div></div> <div class="help-modal-item-label">折叠助手</div> <div class="help-modal-item-btn" style="width: auto;">Ctrl 或 ⌘</div> <div class="help-modal-item-btn" style="width: 41px;">E</div> <div class="help-modal-item-label">刷新助手</div> <div class="help-modal-item-btn">Ctrl</div> <div class="help-modal-item-btn" style="width: 41px;">R</div> `; helpRight.insertAdjacentHTML('beforeend', rightContent); } GM_addStyle('.help-modal .help-modal-middle {grid-template-rows: 32px 32px 32px 32px 32px;}'); GM_addStyle('.help-modal .help-modal-right {grid-template-rows: 32px 32px 32px 32px 32px;}'); GM_addStyle('.help-modal {height: 238px;}'); } function changeRuleListStyle(count) { GM_addStyle('.automation-rule-page .ant-spin-container .content-scroll-wrapper .rule-list { grid-template-columns: repeat('+count+', 1fr);}') } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function sortVars() { const varLists = document.querySelectorAll(".var-list"); console.log(varLists); varLists.forEach(varList => { const commonVarCells = Array.from(varList.querySelectorAll(".common-var-cell")); const cellsWithTitle = commonVarCells.map(cell => ({ cell: cell, title: (cell.querySelector(".var-name")?.getAttribute("title") || "").toUpperCase() })); cellsWithTitle.sort((a, b) => a.title.localeCompare(b.title)); const fragment = document.createDocumentFragment(); cellsWithTitle.forEach(item => fragment.appendChild(item.cell)); varList.innerHTML = ''; varList.appendChild(fragment); }); } function isMiJiaJiKePage() { return document.title === "米家自动化极客版" && !document.querySelector('.pin-code-with-keyboard') && editor; } function handleUrlChange() { if (isMiJiaJiKePage()) { executeScript(); if (window.location.hash.match(/^#\/graph\/.*/g)) { selectDevices(); setTimeout(function () { autoFitContent(); enhancedDisplayLog(); autoCollapse(); addShortcutHelp(); }, 500); } if (window.location.hash.match(/^#\/vars/g)) { setTimeout(function () { sortVars(); }, 500); sortVars(); } } } //页面变化 window.addEventListener('popstate', handleUrlChange); window.addEventListener('hashchange', handleUrlChange); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(this, arguments); handleUrlChange(); }; history.replaceState = function() { originalReplaceState.apply(this, arguments); handleUrlChange(); }; //快捷键 document.addEventListener('keydown', function(event) { if ((event.metaKey || event.ctrlKey) && event.key === 'e') { event.preventDefault(); if(document.getElementById('collapseButton')){ document.getElementById('collapseButton').click(); } } if (event.ctrlKey && event.key === 'q') { event.preventDefault(); if(document.getElementById('closeButton')){ document.getElementById('closeButton').click(); } } if (event.ctrlKey && event.key === 'r') { event.preventDefault(); if(document.getElementById('refreshButton')){ document.getElementById('refreshButton').click(); } } if ((event.metaKey || event.ctrlKey) && event.key === 'b') { event.preventDefault(); if(editor && editor.transformTool){ editor.transformTool.fitToBestPos(); } } if (event.ctrlKey && event.key === 'w') { event.preventDefault(); const selectedMenuItem = document.querySelector('.app-header-menu-item-selected'); if(selectedMenuItem){ const actionElement = selectedMenuItem.querySelector('.app-header-menu-item-action'); if(actionElement) { actionElement.click(); } } } }); window.onload = function () { //监控登录页面 const loginForm = document.querySelector('.account-content'); if (loginForm) { const config = {attributes: true, childList: true, subtree: true}; const callback = function (mutationsList, observer) { setTimeout(function () { handleUrlChange(); }, 1500); }; const observer = new MutationObserver(callback); observer.observe(loginForm, config); } }; })();