diff --git a/config/web/css/style.css b/config/web/css/style.css index 884c662..20556ab 100644 --- a/config/web/css/style.css +++ b/config/web/css/style.css @@ -208,15 +208,15 @@ a { color: var(--accent); text-decoration: none; } Tables ============================================================ */ .table-wrap { overflow-x: auto; border-radius: var(--radius-md); border: 1px solid var(--border); } -.signal-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.signal-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; } .signal-table th { - background: var(--bg-surface); padding: 10px 14px; + background: var(--bg-surface); padding: 10px 8px; text-align: left; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); border-bottom: 1px solid var(--border); - position: sticky; top: 0; z-index: 1; + position: sticky; top: 0; z-index: 1; white-space: nowrap; } -.signal-table td { padding: 9px 14px; border-bottom: 1px solid rgba(30,48,64,0.6); } +.signal-table td { padding: 9px 8px; border-bottom: 1px solid rgba(30,48,64,0.6); overflow: hidden; text-overflow: ellipsis; } .signal-table tbody tr { transition: background var(--transition); } .signal-table tbody tr:hover { background: var(--bg-hover); } .signal-table .empty-row td { text-align: center; color: var(--text-muted); padding: 40px; } @@ -537,14 +537,48 @@ a { color: var(--accent); text-decoration: none; } #plc-zoom-val { font-family: var(--font-mono); font-weight: 600; color: var(--text-primary); } /* ============================================================ - PLC Config (stub - preserved for backward compat) + PLC Config — 信号面板 / 逻辑图查看器 / 编辑器 ============================================================ */ -.plc-canvas-wrap { - background: var(--bg-card); border: 1px solid var(--border); - border-radius: var(--radius-md); height: calc(100vh - 200px); - display: flex; align-items: center; justify-content: center; - color: var(--text-muted); font-size: 14px; +.plc-sig-row { + display: flex; align-items: center; gap: 6px; + padding: 4px 6px; margin: 2px 0; border-radius: 4px; + cursor: pointer; transition: background var(--transition); } +.plc-sig-row:hover { background: var(--bg-hover); } +.plc-modal-row { + padding: 8px 10px; margin: 2px 0; border-radius: 4px; + cursor: pointer; transition: background var(--transition); + border: 1px solid transparent; +} +.plc-modal-row:hover { background: var(--bg-hover); border-color: var(--border); } +.plc-modal-overlay { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); display: flex; + align-items: center; justify-content: center; + z-index: 99990; +} +.plc-modal-box { + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius-lg); padding: 24px; + box-shadow: var(--shadow); color: var(--text-primary); + max-height: 80vh; overflow-y: auto; +} +.plc-modal-box h3 { margin-bottom: 12px; font-size: 16px; font-weight: 600; } +.plc-cfg-tabs { display: flex; gap: 0; margin-bottom: 12px; } +.plc-cfg-tab { + padding: 8px 16px; background: var(--bg-surface); + border: 1px solid var(--border); border-right: none; + cursor: pointer; font-size: 13px; color: var(--text-secondary); + transition: all var(--transition); +} +.plc-cfg-tab:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); } +.plc-cfg-tab:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; border-right: 1px solid var(--border); } +.plc-cfg-tab.active { background: var(--accent); color: var(--bg-deep); border-color: var(--accent); font-weight: 600; } +.plc-sig-search { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; } +.plc-sig-table { width: 100%; border-collapse: collapse; font-size: 12px; } +.plc-sig-table th { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); color: var(--text-secondary); font-weight: 600; } +.plc-sig-table td { padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.04); } +.plc-sig-table tr:hover td { background: var(--bg-hover); } /* ============================================================ Login Page diff --git a/config/web/js/pages/param.js b/config/web/js/pages/param.js index a9baa69..edd32ce 100644 --- a/config/web/js/pages/param.js +++ b/config/web/js/pages/param.js @@ -1,58 +1,83 @@ /** * @file param.js - * @brief 定值管理页面 — 多定值区展开 + * @brief 定值管理页面 — 多定值区展开 + 内联输入框 */ - Router.register('#param', function(c) { var signals = SignalStore.get('param'); var rows = ''; if (signals.length === 0) { - rows = '' + I18n.t('signal.no_data') + ''; + rows = '' + I18n.t('signal.no_data') + ''; } else { - signals.forEach(function(s) { + signals.forEach(function(s, idx) { var zones = s.setting_zone_list || []; var zoneHtml = ''; zones.forEach(function(z) { - zoneHtml += '
' - + '#' + esc(z.id) + '' - + '' + esc(z.val) + '' - + '' + zoneHtml += '
' + + '#' + esc(z.id) + '' + + '' + esc(z.val) + '' + + '' + + '' + '
'; }); - rows += '' + rows += '' + + '' + (idx + 1) + '' + '' + esc(s.saddr) + '' + '' + esc(s.desc || '') + '' + '' + esc(s.type || '?') + '' + '' + (zoneHtml || '\u2014') + '' - + '' + + '' + ''; }); } c.innerHTML = '
' - + '
PARAM \u2014 ' + signals.length + ' signals' + + '
PARAM — ' + signals.length + ' signals' + '' + '
' + '
' - + '' - + '' + rows + '' + + '' + + '' + rows + '' + '
' + I18n.t('signal.saddr') + '' + I18n.t('signal.desc') + '' + I18n.t('signal.type') + '' + I18n.t('signal.zone') + '' + I18n.t('signal.action') + '
#' + I18n.t('signal.saddr') + '' + I18n.t('signal.desc') + '' + I18n.t('signal.type') + '' + I18n.t('signal.zone') + '' + I18n.t('signal.action') + '
'; }); -var ParamActions = { - set: function(saddr, zoneId) { - showModal(I18n.t('signal.set_title') + ' \u2014 ' + esc(saddr) + ' ' + I18n.t('signal.zone') + ' ' + zoneId, - '
', - function(ov) { - var val = getModalVal(ov, '#mf-val'); - WsClient.send({ saddr: saddr, signal_type: 'param', curd: 'set', signal_data: val, setting_zone: zoneId }); - toast(I18n.t('toast.set_sent') + ': ' + saddr + '#' + zoneId + ' = ' + val, 'success'); - ov.remove(); - }, I18n.t('signal.set')); +/* Param event delegation */ +document.addEventListener('click', function(e) { + var btn = e.target.closest('[data-action="param-set-go"]'); + if (!btn) return; + var saddr = btn.getAttribute('data-saddr'); + var zone = btn.getAttribute('data-zone') || '0'; + var row = btn.closest('tr'); + var inp = row ? row.querySelector('[data-action="param-set"][data-zone="' + zone + '"]') : null; + var val = inp ? inp.value.trim() : ''; + if (!val) { toast(I18n.t('toast.val_required'), 'error'); return; } + WsClient.send({ saddr: saddr, signal_type: 'param', curd: 'set', signal_data: val, setting_zone: zone }); + /* 立即更新本地 SignalStore + DOM */ + var arr2 = SignalStore.get('param'); + for (var k2 = 0; k2 < arr2.length; k2++) { + if (arr2[k2].saddr === saddr && arr2[k2].setting_zone_list) { + for (var z = 0; z < arr2[k2].setting_zone_list.length; z++) { + if (arr2[k2].setting_zone_list[z].id === zone) { arr2[k2].setting_zone_list[z].val = val; break; } + } + break; + } } -}; + var zc = row.querySelector('.param-zone-val[data-zone="' + zone + '"]'); + if (zc) { zc.textContent = val; zc.style.transition = 'color 0.3s'; zc.style.color = '#2ed573'; setTimeout(function(){zc.style.color='';}, 800); } + inp.value = ''; +}); +/* Enter key on param inline input */ +document.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && e.target.classList.contains('param-zone-input')) { + e.preventDefault(); + var inp = e.target; + var row = inp.closest('tr'); + var zone = inp.getAttribute('data-zone'); + var btn = row ? row.querySelector('[data-action="param-set-go"][data-zone="' + zone + '"]') : null; + if (btn) btn.click(); + } +}); diff --git a/config/web/js/pages/plc_config.js b/config/web/js/pages/plc_config.js index 5c129e7..ba065fd 100644 --- a/config/web/js/pages/plc_config.js +++ b/config/web/js/pages/plc_config.js @@ -1,15 +1,16 @@ /** * @file plc_config.js - * @brief PLC 逻辑配置页 — 信号列表 + 逻辑图查看器 + 编辑器 - * @details Canvas 可视化 + XML 解析/生成, 11 种门类型 + * @brief PLC 逻辑配置页 — 信号面板 + 逻辑图查看(当前/备份) + 在线编辑 + * @details Canvas 可视化 + XML 解析/生成, 11 种门类型. + * 从原工程迁移, 按现工程 UI 风格重构. + * + * 布局: 左侧信号面板(datacenter_out.xml) | 右侧 Canvas + * 功能: 查看(当前/备份切换) | 编辑(可选基础: 当前/备份/空白) */ -/* ======================================================================== - * PLC 逻辑配置页面 — 信号管理 + 逻辑图查看 + 新建配置编辑器 - * ======================================================================== */ var PlcConfigPage = (function() { "use strict"; - /* ---- Canvas roundRect polyfill (兼容旧浏览器) ---- */ + /* ========== Canvas roundRect polyfill ========== */ if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r }; @@ -28,2502 +29,478 @@ var PlcConfigPage = (function() { }; } - /* ========== 全局状态 ========== */ - var activeTab = 'signals'; // signals | viewer | editor - var dcOutSignals = []; // [{no, saddr, desc}] 从 datacenter_out.xml - var logicGraphs = []; // 已解析的逻辑图数组 - var onlineCfgId = ''; - var loadedFiles = {}; // {path: content} 记录已加载的文件 - - /* ======== 编辑器状态 ======== */ - var editNodes = []; // [{type, id, x, y, saddr, desc, no, inPins, _lx, _ly}] - var editLinks = []; // [{srcIdx, dstIdx, dstPin}] — dstPin 目标节点输入引脚序号 - var editKey = ''; // 逻辑图 key - var editName = ''; // 逻辑图名称 - var editOnlineCfgId = ''; // 当前 online_cfgId - var editSelectedNode = -1; // 当前选中节点索引,-1=无选中 - var editSelectedLink = -1; // 当前选中连线索引,-1=无选中 - var editWiring = null; // {fromNodeIdx, fromSide, fromPin} 当前连线起点 - var editDragNode = null; // 拖拽中的节点索引 - var editDragOffX = 0, editDragOffY = 0; - var editDragCanvas = null; // 画布拖拽状态 - var editPanX = 0, editPanY = 0; // 画布偏移 - var editScale = 1.0; // 缩放 - var editNextGateIdx = {}; // {type: nextIdx} 每种门类型独立序号从0开始 - var editLoadedFromFile = false; // 是否从文件加载 - var editLastClickTime = 0; // 上次 click 时间戳(双击检测) - var editLastClickNode = -1; // 上次 click 的节点索引 - - /* ========== 查看器状态 ========== */ - var viewPanX = 0, viewPanY = 0; - var viewScale = 1.0; - var viewDrag = null; // {startX, startY, panX, panY} - var viewAutoFit = true; // 首次 / resize 后自动适配缩放 - /* ========== 节点类型常量 ========== */ - var NT_OUTPUT = 0, NT_INPUT = 1, NT_OR = 2, NT_AND = 3, NT_NOT = 4; - var NT_62P = 5, NT_62D = 6, NT_RISING = 7, NT_SR = 8, NT_RS = 9, NT_FALLING = 10; + var NT_OUTPUT=0, NT_INPUT=1, NT_OR=2, NT_AND=3, NT_NOT=4; + var NT_62P=5, NT_62D=6, NT_RISING=7, NT_SR=8, NT_RS=9, NT_FALLING=10; var NODE_INFO = { - 0: { name: '输出点', cls: 'node-out', icon: '📤', color: '#4CAF50', inPins: 1, outPins: 0, inMax: 1, outMax: 0 }, - 1: { name: '输入点', cls: 'node-in', icon: '📥', color: '#2196F3', inPins: 0, outPins: 1, inMax: 0, outMax: 99 }, - 2: { name: '或门 OR', cls: 'node-or', icon: '🔶', color: '#FF9800', inPins: 2, outPins: 1, inMax: 10, outMax: 10 }, - 3: { name: '与门 AND', cls: 'node-and', icon: '🔷', color: '#9C27B0', inPins: 2, outPins: 1, inMax: 10, outMax: 10 }, - 4: { name: '非门 NOT', cls: 'node-not', icon: '🔺', color: '#f44336', inPins: 1, outPins: 1, inMax: 1, outMax: 10 }, - 5: { name: '62p延时', cls: 'node-62p', icon: '⏱️', color: '#00BCD4', inPins: 1, outPins: 1, inMax: 1, outMax: 10 }, - 6: { name: '62d延时', cls: 'node-62d', icon: '⏱️', color: '#009688', inPins: 1, outPins: 1, inMax: 1, outMax: 10 }, - 7: { name: '上升沿', cls: 'node-re', icon: '⬆️', color: '#E91E63', inPins: 1, outPins: 1, inMax: 1, outMax: 10 }, - 8: { name: 'SR触发器', cls: 'node-sr', icon: '🔒', color: '#795548', inPins: 2, outPins: 1, inMax: 2, outMax: 10 }, - 9: { name: 'RS触发器', cls: 'node-rs', icon: '🔒', color: '#607D8B', inPins: 2, outPins: 1, inMax: 2, outMax: 10 }, - 10: { name: '下降沿', cls: 'node-fe', icon: '⬇️', color: '#FF5722', inPins: 1, outPins: 1, inMax: 1, outMax: 10 } + 0: { name:'输出点', icon:'📤', color:'#4CAF50', inPins:1, outPins:0, inMax:1, outMax:0 }, + 1: { name:'输入点', icon:'📥', color:'#2196F3', inPins:0, outPins:1, inMax:0, outMax:99 }, + 2: { name:'或门 OR', icon:'🔶', color:'#FF9800', inPins:2, outPins:1, inMax:10, outMax:10 }, + 3: { name:'与门 AND', icon:'🔷', color:'#9C27B0', inPins:2, outPins:1, inMax:10, outMax:10 }, + 4: { name:'非门 NOT', icon:'🔺', color:'#f44336', inPins:1, outPins:1, inMax:1, outMax:10 }, + 5: { name:'62p延时', icon:'⏱️', color:'#00BCD4', inPins:1, outPins:1, inMax:1, outMax:10 }, + 6: { name:'62d延时', icon:'⏱️', color:'#009688', inPins:1, outPins:1, inMax:1, outMax:10 }, + 7: { name:'上升沿', icon:'⬆️', color:'#E91E63', inPins:1, outPins:1, inMax:1, outMax:10 }, + 8: { name:'SR触发器', icon:'🔒', color:'#795548', inPins:2, outPins:1, inMax:2, outMax:10 }, + 9: { name:'RS触发器', icon:'🔒', color:'#607D8B', inPins:2, outPins:1, inMax:2, outMax:10 }, + 10: { name:'下降沿', icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1, outMax:10 } }; + /* ========== 全局状态 ========== */ + var mode = 'view'; // 'view' | 'edit' + var dcOutSignals = []; // [{no, saddr, desc}] + var logicGraphs = []; // parsed graphs + var loadedFiles = {}; // {path: content} + + /* ======== 编辑器状态 ======== */ + var editNodes = []; + var editLinks = []; + var editKey = '', editName = ''; + var editSelectedNode = -1, editSelectedLink = -1; + var editWiring = null; + var editDragNode = null, editDragOffX = 0, editDragOffY = 0; + var editDragCanvas = null; + var editPanX = 0, editPanY = 0, editScale = 1.0; + var editNextGateIdx = {}; + var editLastClickTime = 0, editLastClickNode = -1; + + /* ======== 查看器状态 ======== */ + var viewPanX = 0, viewPanY = 0, viewScale = 1.0; + var viewDrag = null, viewAutoFit = true; + /* ========== 工具函数 ========== */ - function fmtVal(v) { return (v === null || v === undefined) ? '' : String(v); } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - - /* ---- 通过节点 ID 查找信号信息 (datacenter out 中 no 即 ID) ---- */ - function findSignalByNo(signalNo) { - for (var i = 0; i < dcOutSignals.length; i++) { - if (dcOutSignals[i].no === signalNo) return dcOutSignals[i]; - } - return null; - } - - /* ---- 生成节点显示标签 (十进制ID, 含saddr/desc) ---- */ + function escXml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + function findSignalByNo(no) { for (var i=0;i0) return node.inPins; + return (NODE_INFO[node.type]||{}).inPins||0; } - - function showCfgToast(msg, type) { // wraps global toast - type = type || 'info'; - var t = document.getElementById('plc-cfg-toast'); - if (!t) { - t = document.createElement('div'); - t.id = 'plc-cfg-toast'; - t.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:6px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;color:#fff'; - document.body.appendChild(t); - } - t.style.background = (type === 'err') ? '#f44336' : (type === 'ok') ? '#4CAF50' : '#333'; - t.textContent = msg; t.style.opacity = '1'; - clearTimeout(t._timer); - t._timer = setTimeout(function() { t.style.opacity = '0'; }, 2000); + function getNodeSize(node) { + var isIO=(node.type===NT_OUTPUT||node.type===NT_INPUT),isSR=(node.type===NT_SR||node.type===NT_RS); + var inP=getNodeInPins(node); + return {w:isIO?250:170, h:isIO?80:(isSR?86:(66+Math.max(0,inP-2)*14))}; + } + function showToast(msg, type) { + type=type||'info'; + var t=document.getElementById('plc-cfg-toast'); + if(!t){t=document.createElement('div');t.id='plc-cfg-toast';t.style.cssText='position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:6px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;color:#fff';document.body.appendChild(t);} + t.style.background=type==='err'?'#f44336':type==='ok'?'#4CAF50':'#333'; + t.textContent=msg;t.style.opacity='1';clearTimeout(t._timer);t._timer=setTimeout(function(){t.style.opacity='0';},2000); } /* ========== WebSocket 文件请求 ========== */ - function requestFile(path, callback) { - if (loadedFiles[path]) { - callback(loadedFiles[path]); - return; - } - if (!PlcConfigPage._pending) PlcConfigPage._pending = {}; - PlcConfigPage._pending[path] = callback; - var sent = WsClient.send({ type: 'get_file', path: path }); - if (!sent) { - if (!PlcConfigPage._retryList) PlcConfigPage._retryList = []; - PlcConfigPage._retryList.push(path); - } - } + function requestFile(path,cb){if(loadedFiles[path]){cb(loadedFiles[path]);return;}if(!PlcConfigPage._pending)PlcConfigPage._pending={};PlcConfigPage._pending[path]=cb;if(!WsClient.send({type:'get_file',path:path})){if(!PlcConfigPage._retryList)PlcConfigPage._retryList=[];PlcConfigPage._retryList.push(path);}} /* ========== 主渲染 ========== */ - function renderTabs() { - var tabs = [ - { id: 'signals', label: '📋 信号列表', title: 'datacenter_out.xml — 已注册 Out 信号' }, - { id: 'viewer', label: '👁️ 逻辑图查看', title: 'Reclose_logic.xml 可视化' }, - { id: 'editor', label: '✏️ 新建配置', title: '新建/编辑 PLC 逻辑配置' } - ]; - var html = '
'; - for (var i = 0; i < tabs.length; i++) { - var cls = (tabs[i].id === activeTab) ? 'plc-cfg-tab active' : 'plc-cfg-tab'; - html += ''; - } - html += '
'; - return html; - } - function render(container) { container.innerHTML = '' - + '' - + renderTabs() - + '
'; - - // Tab 切换事件 - var tabEls = container.querySelectorAll('.plc-cfg-tab'); - for (var i = 0; i < tabEls.length; i++) { - tabEls[i].onclick = function() { - activeTab = this.getAttribute('data-tab'); - render(container); - }; - } - - // 渲染当前 Tab - var content = document.getElementById('plc-cfg-content'); - if (!content) return; - - switch (activeTab) { - case 'signals': renderSignalsTab(content); break; - case 'viewer': renderViewerTab(content); break; - case 'editor': renderEditorTab(content); break; - } + + '
🔌 PLC 逻辑配置' + + '
' + + '' + + '' + + '
' + + '
'; + document.getElementById('plc-btn-view').onclick = function(){mode='view';editWiring=null;render(container);}; + document.getElementById('plc-btn-edit').onclick = function(){mode='edit';editWiring=null;render(container);}; + var main=document.getElementById('plc-main-area');if(!main)return; + if(mode==='view')renderViewMode(main);else renderEditMode(main); + if(dcOutSignals.length===0)loadDcOutSignals(function(){refreshSigPanel();});else refreshSigPanel(); } /* ================================================================ - * Tab 1: 信号列表 — 解析 datacenter_out.xml + * 信号面板 * ================================================================ */ - function renderSignalsTab(container) { - container.innerHTML = '' - + '
' - + '' - + '' - + '' - + '
' - + '
' - + '' - + '' - + '
序号 (no)短地址 (saddr)描述 (desc)
'; - - document.getElementById('plc-sig-search').oninput = function() { refreshSigTable(); }; - document.getElementById('plc-sig-reload').onclick = function() { - loadedFiles['config/SYSTEM/datacenter_out.xml'] = null; - dcOutSignals = []; - loadDcOutSignals(function() { refreshSigTable(); }); - }; - - if (dcOutSignals.length === 0) { - loadDcOutSignals(function() { refreshSigTable(); }); - } else { - refreshSigTable(); - } + function renderSigPanel() { + return '
' + + '
📋 信号 ('+dcOutSignals.length+')' + + '
' + + '' + + '
' + + '
'; } function loadDcOutSignals(cb) { requestFile('config/SYSTEM/datacenter_out.xml', function(content) { - if (!content) { showToast('加载 datacenter_out.xml 失败,请先在终端执行 datacenter out', 'err'); return; } - parseDcOutXml(content); - if (cb) cb(); + if(!content){showToast('加载 datacenter_out.xml 失败','err');return;} + dcOutSignals=[]; + try{ + var doc=new DOMParser().parseFromString(content,'text/xml'); + var outNodes=doc.documentElement.getElementsByTagName('Out'); + if(outNodes.length>0){var sigs=outNodes[0].getElementsByTagName('Signal');for(var i=0;i 0) { - var signals = outNodes[0].getElementsByTagName('Signal'); - for (var i = 0; i < signals.length; i++) { - dcOutSignals.push({ - no: parseInt(signals[i].getAttribute('no')) || i, - saddr: signals[i].getAttribute('saddr') || '', - desc: signals[i].getAttribute('desc') || '' - }); - } - } - } catch(e) { - showToast('解析 datacenter_out.xml 失败: ' + e.message, 'err'); - } - } - - function refreshSigTable() { - var search = (document.getElementById('plc-sig-search').value || '').toLowerCase(); - var data = dcOutSignals; - - // 过滤 - var filtered = data; - if (search) { - filtered = data.filter(function(s) { - return s.saddr.toLowerCase().indexOf(search) !== -1 - || s.desc.toLowerCase().indexOf(search) !== -1 - || String(s.no).indexOf(search) !== -1; - }); - } - - document.getElementById('plc-sig-count').textContent = '共 ' + filtered.length + ' / ' + data.length + ' 条'; - - var html = ''; - for (var i = 0; i < filtered.length; i++) { - var s = filtered[i]; - html += '' - + '' + s.no + '' - + '' + escHtml(s.saddr) + '' - + '' + escHtml(s.desc) + '' - + ''; - } - document.getElementById('plc-sig-tbody').innerHTML = html; + function refreshSigPanel() { + var list=document.getElementById('plc-sig-list'),search=(document.getElementById('plc-sig-search')||{}).value||''; + if(!list)return; + var data=dcOutSignals; + if(search){var q=search.toLowerCase();data=data.filter(function(s){return s.saddr.toLowerCase().indexOf(q)!==-1||s.desc.toLowerCase().indexOf(q)!==-1||String(s.no).indexOf(q)!==-1;});} + var h=''; + for(var i=0;i#'+s.no+''+escHtml(s.saddr)+''+escHtml(s.desc)+'
';} + list.innerHTML=h||'
无匹配信号
'; + [].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){row.onclick=function(){if(mode!=='edit')return;var no=parseInt(this.dataset.no),saddr=this.dataset.saddr,desc=this.dataset.desc||'';editNodes.push({type:NT_INPUT,id:no,x:0,y:0,_lx:-260,_ly:editNodes.filter(function(n){return n.type===NT_INPUT;}).length*110,saddr:saddr,desc:desc,no:no});drawEditor();showToast('已添加输入: '+saddr,'ok');};}); } /* ================================================================ - * Tab 2: 逻辑图查看器 — Canvas 可视化 Reclose_logic.txt + * 查看模式 * ================================================================ */ - function renderViewerTab(container) { - container.innerHTML = '' - + '
' - + '' + function renderViewMode(main) { + main.innerHTML = renderSigPanel() + + '
' + + '
👁️ 逻辑图查看' + + '' + '' - + '' - + '' - + '' - + '' + + '' + + '' + + '' + + '' + '
' - + '
' - + '' - + '
' - + '
'; - - // 图例 - var legendHtml = '图例: '; - var types = [1,0,3,2,4,5,6,7,8,9,10]; - for (var i = 0; i < types.length; i++) { - var info = NODE_INFO[types[i]]; - legendHtml += '' + info.icon + ' ' + info.name + ''; - } - document.getElementById('plc-view-legend').innerHTML = legendHtml; - - // 绑定事件 - document.getElementById('plc-view-zoomin').onclick = function() { viewScale = Math.min(3, viewScale * 1.2); drawViewer(); }; - document.getElementById('plc-view-zoomout').onclick = function() { viewScale = Math.max(0.2, viewScale / 1.2); drawViewer(); }; - document.getElementById('plc-view-reset').onclick = function() { viewAutoFit = true; drawViewer(); }; - document.getElementById('plc-view-reload').onclick = function() { - loadedFiles['config/PLC/Reclose_logic.xml'] = null; - loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null; - logicGraphs = []; - loadLogicFileAll(function() { refreshGraphSelect(); }); - }; - document.getElementById('plc-view-graph').onchange = function() { viewAutoFit = true; drawViewer(); }; - - // 画布交互 - setupViewerCanvas(); - - if (logicGraphs.length === 0) { - loadLogicFileAll(function() { refreshGraphSelect(); }); - } else { - refreshGraphSelect(); - } - } - - function loadLogicFileAll(cb) { - var loadCount = 0; - var loadedFiles2 = []; // 记录成功加载的文件名 - var failedFiles = []; // 记录失败的文件名 - function tryCb() { - loadCount++; - if (loadCount >= 2 && cb) { - if (failedFiles.length > 0) { - var msg = loadedFiles2.length > 0 - ? '已加载: ' + loadedFiles2.join(', ') + ' | 缺失: ' + failedFiles.join(', ') - : '配置文件均缺失: ' + failedFiles.join(', ') + ',请先在编辑器中生成配置'; - showToast(msg, loadedFiles2.length > 0 ? 'info' : 'err'); - } - cb(); - } - } - requestFile('config/PLC/Reclose_logic.xml', function(content) { - if (!content) { failedFiles.push('Reclose_logic.xml'); tryCb(); return; } - parseLogicFile(content, 'Reclose_logic.xml'); - loadedFiles2.push('Reclose_logic.xml'); - tryCb(); - }); - requestFile('config/PLC/Reclose_logic.xml.bak', function(content) { - if (!content) { tryCb(); return; } - parseLogicFile(content, 'Reclose_logic.xml.bak'); - loadedFiles2.push('Reclose_logic.xml.bak'); - tryCb(); - }); - } - - - function loadLogicFile(cb) { - requestFile('config/PLC/Reclose_logic.xml', function(content) { - if (!content) { showToast('加载 Reclose_logic.xml 失败', 'err'); return; } - parseLogicFile(content, 'Reclose_logic.xml'); - if (cb) cb(); - }); - } - function parseLogicFile(content, sourceName) { - logicGraphs = logicGraphs.filter(function(g) { return g._source !== sourceName; }); - onlineCfgId = onlineCfgId || ''; - parseLogicXml(content, sourceName); - } - - // XML → 扁平 graph,与 C++ parse_xml_comb 完全一致 - function parseLogicXml(xmlStr, sourceName) { - var parser = new DOMParser(); - var doc = parser.parseFromString(xmlStr, 'text/xml'); - var root = doc.documentElement; - if (!root || root.nodeName !== 'PLCConfig') { - showToast('XML 根元素不是 PLCConfig', 'err'); return; - } - if (sourceName.indexOf('.bak') < 0) { - onlineCfgId = root.getAttribute('checksum') || ''; - } - var thisKey = root.getAttribute('key') || sourceName; - var thisName = root.getAttribute('name') || ''; - var g = { key: thisKey, name: thisName, nodes: [], links: [] }; - var GTM = {'INPUT':1,'OUTPUT':0,'OR':2,'AND':3,'NOT':4,'T62P':5,'T62D':6,'RISING':7,'SR':8,'RS':9,'FALLING':10}; - - var sigNodeMap = {}; - function ensureSig(ioType, no, x, y) { - var k = ioType + ':' + no; - if (sigNodeMap[k] !== undefined) { - if (x !== undefined) { g.nodes[sigNodeMap[k]].x = x; g.nodes[sigNodeMap[k]].y = y; } - return sigNodeMap[k]; - } - g.nodes.push({ type:ioType, id:no, x:x||0, y:y||0 }); - sigNodeMap[k] = g.nodes.length - 1; - return g.nodes.length - 1; - } - function ensureGate(type, id, x, y, inPins, delayMs) { - for (var i = 0; i < g.nodes.length; i++) - if (g.nodes[i].type === type && (g.nodes[i].id & 0xFF) === (id & 0xFF)) return i; - var nd = { type:type, id:id, x:x||0, y:y||0 }; - if (inPins) nd.inPins = inPins; - if (delayMs) nd.delayMs = delayMs; - g.nodes.push(nd); - return g.nodes.length - 1; - } - - function parseComb(combEl) { - var ge = combEl.firstElementChild; - while (ge && ge.nodeName !== 'Gate') ge = ge.nextElementSibling; - if (!ge) return -1; - var t = GTM[ge.getAttribute('type')] || 0; - var gid = parseInt(ge.getAttribute('id')); - var gi = ensureGate(t, gid, - parseInt(ge.getAttribute('x')), parseInt(ge.getAttribute('y')), - ge.getAttribute('inPins') ? parseInt(ge.getAttribute('inPins')) : null, - ge.getAttribute('delayMs') ? parseInt(ge.getAttribute('delayMs')) : null); - - // 读取 Comb id - var combId = combEl.getAttribute('id'); - if (combId) g.nodes[gi].combId = parseInt(combId); - - // 读取 SR/RS 的 R/S 属性 - var rAttr = ge.getAttribute('R'); - var sAttr = ge.getAttribute('S'); - if (rAttr) g.nodes[gi].rsR = rAttr; - if (sAttr) g.nodes[gi].rsS = sAttr; - - var c = combEl.firstElementChild; - while (c) { - if (c.nodeName === 'Comb') { - var upi = parseComb(c); - if (upi >= 0) g.links.push({ src_type:g.nodes[upi].type, src_id:g.nodes[upi].id, dst_type:t, dst_id:gid }); - } - c = c.nextElementSibling; - } - - var ie = combEl.firstElementChild; - while (ie && ie.nodeName !== 'Inputs') ie = ie.nextElementSibling; - if (ie) { - var s = ie.firstElementChild; - while (s) { - if (s.nodeName === 'Signal') { - var no = parseInt(s.getAttribute('no')); - ensureSig(NT_INPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y'))); - g.links.push({ src_type:NT_INPUT, src_id:no, dst_type:t, dst_id:gid }); - } - s = s.nextElementSibling; - } - } - - var oe = combEl.firstElementChild; - while (oe && oe.nodeName !== 'Outputs') oe = oe.nextElementSibling; - if (oe) { - var s = oe.firstElementChild; - while (s) { - if (s.nodeName === 'Signal') { - var no = parseInt(s.getAttribute('no')); - ensureSig(NT_OUTPUT, no, parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y'))); - g.links.push({ src_type:t, src_id:gid, dst_type:NT_OUTPUT, dst_id:no }); - } - s = s.nextElementSibling; - } - } - return gi; - } - - var chains = root.getElementsByTagName('Chain'); - for (var ci = 0; ci < chains.length; ci++) { - var chain = chains[ci]; - var c = chain.firstElementChild; - while (c) { - if (c.nodeName === 'Inputs') { - var s = c.firstElementChild; - while (s) { - if (s.nodeName === 'Signal') - ensureSig(NT_INPUT, parseInt(s.getAttribute('no')), - parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y'))); - s = s.nextElementSibling; - } - } - if (c.nodeName === 'Outputs') { - var s = c.firstElementChild; - while (s) { - if (s.nodeName === 'Signal') - ensureSig(NT_OUTPUT, parseInt(s.getAttribute('no')), - parseInt(s.getAttribute('x')), parseInt(s.getAttribute('y'))); - s = s.nextElementSibling; - } - } - if (c.nodeName === 'Comb') parseComb(c); - if (c.nodeName === 'Gate') { - var gt = GTM[c.getAttribute('type')] || 0; - var giChain = ensureGate(gt, parseInt(c.getAttribute('id')), - parseInt(c.getAttribute('x')), parseInt(c.getAttribute('y'))); - var rAttrC = c.getAttribute('R'); - var sAttrC = c.getAttribute('S'); - var dmsC = c.getAttribute('delayMs'); - if (rAttrC) g.nodes[giChain].rsR = rAttrC; - if (sAttrC) g.nodes[giChain].rsS = sAttrC; - if (dmsC) g.nodes[giChain].delayMs = parseInt(dmsC); - } - c = c.nextElementSibling; - } - } - - // Comb链: Comb Gate → 链级 Gate (一个Comb一根线) + 链级 Gate → Outputs - // 简单链: Inputs → Gate → Outputs - for (var ci2 = 0; ci2 < chains.length; ci2++) { - var chain2 = chains[ci2]; - var hasComb = false; - var c3 = chain2.firstElementChild; - while (c3) { if (c3.nodeName === 'Comb') { hasComb = true; break; } c3 = c3.nextElementSibling; } - var ge2 = null, ie2 = null, oe2 = null; - c3 = chain2.firstElementChild; - while (c3) { - if (c3.nodeName === 'Gate') ge2 = c3; - if (c3.nodeName === 'Inputs') ie2 = c3; - if (c3.nodeName === 'Outputs') oe2 = c3; - c3 = c3.nextElementSibling; - } - if (hasComb && ge2) { - // 链级 Comb → 取其 Gate → 链级 Gate - c3 = chain2.firstElementChild; - while (c3) { - if (c3.nodeName === 'Comb') { - var cg = c3.firstElementChild; - while (cg && cg.nodeName !== 'Gate') cg = cg.nextElementSibling; - if (cg) { - var ct = GTM[cg.getAttribute('type')] || 0; - var ci = parseInt(cg.getAttribute('id')); - g.links.push({ src_type:ct, src_id:ci, dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) }); - } - } - c3 = c3.nextElementSibling; - } - // 链级 Inputs(如 RS/SR 直接连 Input 信号)→ 链级 Gate - if (ie2) { - var s = ie2.firstElementChild; - while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:GTM[ge2.getAttribute('type')] || 0, dst_id:parseInt(ge2.getAttribute('id')) }); s = s.nextElementSibling; } - } - // RS/SR 的 R/S 属性引用的 Input 信号(无 块时也需创建链接) - if (ge2) { - var gtCS = GTM[ge2.getAttribute('type')] || 0; - if (gtCS === NT_SR || gtCS === NT_RS) { - var sAttrCS = ge2.getAttribute('S'); - var rAttrCS = ge2.getAttribute('R'); - if (sAttrCS && sAttrCS.indexOf('Input#') === 0) { - var sNo = parseInt(sAttrCS.substring(6)); - ensureSig(NT_INPUT, sNo, 0, 0); - g.links.push({ src_type:NT_INPUT, src_id:sNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) }); - } - if (rAttrCS && rAttrCS.indexOf('Input#') === 0) { - var rNo = parseInt(rAttrCS.substring(6)); - ensureSig(NT_INPUT, rNo, 0, 0); - g.links.push({ src_type:NT_INPUT, src_id:rNo, dst_type:gtCS, dst_id:parseInt(ge2.getAttribute('id')) }); - } - } - } - if (oe2) { - var s = oe2.firstElementChild; - while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:GTM[ge2.getAttribute('type')] || 0, src_id:parseInt(ge2.getAttribute('id')), dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; } - } - } else if (!hasComb && ge2) { - var gt = GTM[ge2.getAttribute('type')] || 0; - var gid = parseInt(ge2.getAttribute('id')); - if (ie2) { - var s = ie2.firstElementChild; - while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:NT_INPUT, src_id:parseInt(s.getAttribute('no')), dst_type:gt, dst_id:gid }); s = s.nextElementSibling; } - } - if (oe2) { - var s = oe2.firstElementChild; - while (s) { if (s.nodeName === 'Signal') g.links.push({ src_type:gt, src_id:gid, dst_type:NT_OUTPUT, dst_id:parseInt(s.getAttribute('no')) }); s = s.nextElementSibling; } - } - } - } - - // 最终遍:为 SR/RS 节点修复 dstPin - for (var ni = 0; ni < g.nodes.length; ni++) { - var nd = g.nodes[ni]; - if (nd.type !== NT_SR && nd.type !== NT_RS) continue; - var sSrc = nd.rsS || ''; - var rSrc = nd.rsR || ''; - if (!sSrc && !rSrc) continue; - // 遍历所有到该节点的链路 - for (var lj = 0; lj < g.links.length; lj++) { - var lk = g.links[lj]; - if (lk.dst_type !== nd.type || (lk.dst_id & 0xFF) !== (nd.id & 0xFF)) continue; - var srcDesc = ''; - var sN = null; - for (var sn = 0; sn < g.nodes.length; sn++) { - if (g.nodes[sn].type === lk.src_type && - (g.nodes[sn].type <= 1 ? g.nodes[sn].id === lk.src_id : (g.nodes[sn].id & 0xFF) === (lk.src_id & 0xFF))) { - sN = g.nodes[sn]; break; - } - } - if (!sN) continue; - if (sN.type === NT_INPUT) srcDesc = 'Input#' + sN.id; - else srcDesc = 'Comb#' + (sN.combId !== undefined ? sN.combId : (sN.id & 0xFF)); - if (nd.type === NT_SR) { - if (sSrc && srcDesc === sSrc) lk.dstPin = 0; - else if (rSrc && srcDesc === rSrc) lk.dstPin = 1; - } else { - if (sSrc && srcDesc === sSrc) lk.dstPin = 1; - else if (rSrc && srcDesc === rSrc) lk.dstPin = 0; - } - } - } - - g._source = sourceName; - logicGraphs.push(g); - } - - function refreshGraphSelect() { - var sel = document.getElementById('plc-view-graph'); - if (!sel) return; - sel.innerHTML = ''; - for (var i = 0; i < logicGraphs.length; i++) { - var g = logicGraphs[i]; - var bakTag = (g._source && g._source.indexOf(".bak") >= 0) ? " [备份]" : ""; - var label = g.key + " — " + (g.name || "(未命名)") + bakTag + " (" + g.nodes.length + "节点 " + g.links.length + "连线)"; - sel.innerHTML += ''; - } - if (logicGraphs.length > 0) { - sel.value = '0'; - setTimeout(function() { drawViewer(); }, 100); - } - } - - /* ---- 查看器 Canvas ---- */ - function setupViewerCanvas() { - var wrap = document.getElementById('plc-view-canvas-wrap'); - var canvas = document.getElementById('plc-view-canvas'); - if (!wrap || !canvas) return; - - function resize() { - canvas.width = wrap.clientWidth; - canvas.height = wrap.clientHeight - 4; - viewAutoFit = true; - drawViewer(); - } - resize(); - window.addEventListener('resize', function() { resize(); }); - - // 鼠标事件 - canvas.onmousedown = function(e) { - viewDrag = { startX: e.clientX, startY: e.clientY, panX: viewPanX, panY: viewPanY }; - }; - canvas.onmousemove = function(e) { - if (!viewDrag) return; - viewPanX = viewDrag.panX + (e.clientX - viewDrag.startX); - viewPanY = viewDrag.panY + (e.clientY - viewDrag.startY); - drawViewer(); - }; - canvas.onmouseup = function() { viewDrag = null; }; - canvas.onmouseleave = function() { viewDrag = null; }; - canvas.onwheel = function(e) { - e.preventDefault(); - var delta = e.deltaY > 0 ? 0.9 : 1.1; - var newScale = Math.max(0.2, Math.min(3, viewScale * delta)); - // 以鼠标位置为中心缩放 - var rect = canvas.getBoundingClientRect(); - var mx = e.clientX - rect.left; - var my = e.clientY - rect.top; - viewPanX = mx - (mx - viewPanX) * (newScale / viewScale); - viewPanY = my - (my - viewPanY) * (newScale / viewScale); - viewScale = newScale; - drawViewer(); - }; - } - - function drawViewer() { - var canvas = document.getElementById('plc-view-canvas'); - if (!canvas) return; - var ctx = canvas.getContext('2d'); - var W = canvas.width, H = canvas.height; - - ctx.clearRect(0, 0, W, H); - ctx.fillStyle = '#1a1a2e'; - ctx.fillRect(0, 0, W, H); - - var sel = document.getElementById('plc-view-graph'); - if (!sel || sel.value === '') { - ctx.fillStyle = '#888'; - ctx.font = '16px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('请选择一个逻辑图', W/2, H/2); - return; - } - - var g = logicGraphs[parseInt(sel.value)]; - if (!g) return; - - // 使用文件中保存的 x,y 坐标(与编辑器布局一致) - var viewNodes = []; - var bMinX = Infinity, bMaxX = -Infinity, bMinY = Infinity, bMaxY = -Infinity; - for (var vi = 0; vi < g.nodes.length; vi++) { - var vn = g.nodes[vi]; - var vx = vn.x, vy = vn.y; - if (vx < bMinX) bMinX = vx; if (vx > bMaxX) bMaxX = vx; - if (vy < bMinY) bMinY = vy; if (vy > bMaxY) bMaxY = vy; - var vsig = findSignalByNo(vn.id); - viewNodes.push({ - type: vn.type, id: vn.id, _lx: vx, _ly: vy, - saddr: (vsig && vsig.saddr) || '', desc: (vsig && vsig.desc) || '', no: vn.id - }); - } - - // 首次绘制 / resize 后自动适配缩放 - if (viewAutoFit) { - viewAutoFit = false; - var cw2 = bMaxX - bMinX, ch2 = bMaxY - bMinY; - if (cw2 <= 0) cw2 = 800; if (ch2 <= 0) ch2 = 600; - var pad = 80; - viewScale = Math.min((W - pad * 2) / cw2, (H - pad * 2) / ch2, 1.2); - viewPanX = W / 2 - (bMinX + cw2 / 2) * viewScale; - viewPanY = H / 2 - (bMinY + ch2 / 2) * viewScale; - } - - // 链路 —— 修复:首个命中而非最后命中(避免同名门 ID 歧义) - var viewLinks = []; - for (var vlj = 0; vlj < g.links.length; vlj++) { - var vl = g.links[vlj]; - var vls = -1, vld = -1; - for (var vlk = 0; vlk < g.nodes.length; vlk++) { - var vln = g.nodes[vlk]; - if (vls < 0 && vln.type === vl.src_type && (vln.type <= 1 ? vln.id === vl.src_id : (vln.id & 0xFF) === (vl.src_id & 0xFF))) vls = vlk; - if (vld < 0 && vln.type === vl.dst_type && (vln.type <= 1 ? vln.id === vl.dst_id : (vln.id & 0xFF) === (vl.dst_id & 0xFF))) vld = vlk; - } - if (vls >= 0 && vld >= 0) viewLinks.push({ srcIdx: vls, dstIdx: vld, dstPin: vl.dstPin || 0 }); - } - - // 网格 - ctx.strokeStyle = 'rgba(255,255,255,0.04)'; - ctx.lineWidth = 1; - var gridSize = 50; - var startGx = Math.floor(-viewPanX / viewScale / gridSize) * gridSize; - var startGy = Math.floor(-viewPanY / viewScale / gridSize) * gridSize; - - ctx.save(); - ctx.translate(viewPanX, viewPanY); - ctx.scale(viewScale, viewScale); - - for (var gx = startGx - gridSize; gx < startGx + W/viewScale + gridSize; gx += gridSize) { - ctx.beginPath(); ctx.moveTo(gx, startGy - gridSize); ctx.lineTo(gx, startGy + H/viewScale + gridSize); ctx.stroke(); - } - for (var gy = startGy - gridSize; gy < startGy + H/viewScale + gridSize; gy += gridSize) { - ctx.beginPath(); ctx.moveTo(startGx - gridSize, gy); ctx.lineTo(startGx + W/viewScale + gridSize, gy); ctx.stroke(); - } - - drawGraphContent(ctx, viewNodes, viewLinks, -1, -1, null); - - ctx.restore(); - } - - function calcLayout(g) { - // 自动布局:每条链路 (输入→运算→输出) 构成一条横向数据流链 - // 节点类型常量复用的全局 NODE_INFO - var layout = {}; - - // 1. 构建邻接关系,按连线确定连通分量(每条链) - var adjOut = {}; for (var i = 0; i < g.nodes.length; i++) adjOut[i] = []; - var adjIn = {}; for (var i = 0; i < g.nodes.length; i++) adjIn[i] = []; - for (var j = 0; j < g.links.length; j++) { - var l = g.links[j]; - var si = -1, di = -1; - for (var k = 0; k < g.nodes.length; k++) { - var n = g.nodes[k]; - if (si < 0 && n.type === l.src_type && (n.type <= 1 ? n.id === l.src_id : (n.id & 0xFF) === (l.src_id & 0xFF))) si = k; - if (di < 0 && n.type === l.dst_type && (n.type <= 1 ? n.id === l.dst_id : (n.id & 0xFF) === (l.dst_id & 0xFF))) di = k; - } - if (si >= 0 && di >= 0) { adjOut[si].push(di); adjIn[di].push(si); } - } - - // BFS 找连通分量 - var visited = {}; - var chains = []; - for (var idx = 0; idx < g.nodes.length; idx++) { - if (visited[idx]) continue; - var chain = []; - var queue = [idx]; - visited[idx] = true; - while (queue.length > 0) { - var cur = queue.shift(); - chain.push(cur); - var nbrs = adjOut[cur].concat(adjIn[cur]); - for (var nb = 0; nb < nbrs.length; nb++) { - if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; queue.push(nbrs[nb]); } - } - } - chains.push(chain); - } - - // 2. 计算每条链的实际竖向高度(含节点+间距) - var NODE_IO_H = 80; // 输入/输出节点高度 - var NODE_GATE_H = 66; // 逻辑门节点高度 - var ITEM_GAP = 28; // 同列节点纵向间距 - - function calcChainPixels(chain) { - var cin = 0, cg = 0, cout = 0; - for (var i = 0; i < chain.length; i++) { - var nd = g.nodes[chain[i]]; - if (nd.type === NT_INPUT) cin++; - else if (nd.type === NT_OUTPUT) cout++; - else cg++; - } - // 每列独立高度 - var hIn = cin > 0 ? cin * NODE_IO_H + (cin - 1) * ITEM_GAP : 0; - var hGate = cg > 0 ? cg * NODE_GATE_H + (cg - 1) * ITEM_GAP : 0; - var hOut = cout > 0 ? cout * NODE_IO_H + (cout - 1) * ITEM_GAP : 0; - return Math.max(hIn, hGate, hOut, 60); // 至少 60px 避免空链过扁 - } - - var CHAIN_GAP = 80; // 链间纵向间距 - - // 累计 Y 偏移 - var chainHeights = []; - var totalH = 0; - for (var ci = 0; ci < chains.length; ci++) { - var h = calcChainPixels(chains[ci]); - chainHeights.push(h); - totalH += h; - } - totalH += (chains.length - 1) * CHAIN_GAP; - - // 使链条在 Y 轴居中:startY = -(totalH / 2) + (firstHalfHeight/2) - var startY = -totalH / 2; - var curY = startY; - - // 3. 列布局 (输入 | 运算 | 输出) - var COL_INPUT = -420; - var COL_GATE = 0; - var COL_OUTPUT = 420; - - for (var ci2 = 0; ci2 < chains.length; ci2++) { - var chain = chains[ci2]; - var ch = chainHeights[ci2]; - var cy = curY + ch / 2; // 链中心 Y - - // 分类节点 - var cInputs = [], cGates = [], cOutputs = []; - for (var n = 0; n < chain.length; n++) { - var nd = g.nodes[chain[n]]; - if (nd.type === NT_INPUT) cInputs.push(chain[n]); - else if (nd.type === NT_OUTPUT) cOutputs.push(chain[n]); - else cGates.push(chain[n]); - } - - // 计算每列起始 Y(使该列节点在链高度内居中) - function colTop(count, itemH) { - if (count === 0) return cy; - var colH = count * itemH + (count - 1) * ITEM_GAP; - return cy - colH / 2 + itemH / 2; - } - - var topIn = colTop(cInputs.length, NODE_IO_H); - var topGate = colTop(cGates.length, NODE_GATE_H); - var topOut = colTop(cOutputs.length, NODE_IO_H); - - for (var a = 0; a < cInputs.length; a++) - layout[cInputs[a]] = { x: COL_INPUT, y: topIn + a * (NODE_IO_H + ITEM_GAP) }; - for (var b = 0; b < cGates.length; b++) - layout[cGates[b]] = { x: COL_GATE, y: topGate + b * (NODE_GATE_H + ITEM_GAP) }; - for (var c = 0; c < cOutputs.length; c++) - layout[cOutputs[c]] = { x: COL_OUTPUT, y: topOut + c * (NODE_IO_H + ITEM_GAP) }; - - curY += ch + CHAIN_GAP; - } - - // 暴露边界信息供 canvas 自适应 - layout._bounds = { - minX: COL_INPUT - 130, maxX: COL_OUTPUT + 130, - minY: startY - 40, maxY: startY + totalH + 40 - }; - return layout; - } - - /* ---- 通用绘制函数(编辑器 + 查看器共用) ---- */ - function drawGraphContent(ctx, nodes, links, selectedIdx, selectedLinkIdx, wiring) { - // 节点渲染尺寸常量 - var IO_W = 250, IO_H = 80; // 输入/输出 - var GATE_W = 170, GATE_H = 66; // 逻辑门 - var CORNER_R = 12; - var SHADOW_OFF = 3; - var LEFT_BAR_W = 8; - - // ---- 连线层 ---- - for (var i = 0; i < links.length; i++) { - var l = links[i]; - if (l.srcIdx >= nodes.length || l.dstIdx >= nodes.length) continue; - var sn = nodes[l.srcIdx], dn = nodes[l.dstIdx]; - var srcIsIO = (sn.type === NT_OUTPUT || sn.type === NT_INPUT); - var dstIsIO = (dn.type === NT_OUTPUT || dn.type === NT_INPUT); - var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS); - var sW = srcIsIO ? IO_W : GATE_W, dW = dstIsIO ? IO_W : GATE_W; - var dH = dstIsIO ? IO_H : (dstIsSRRS ? 86 : GATE_H); - var x1 = sn._lx + sW/2; - var y1 = sn._ly; - var dstInP = getNodeInPins(dn); - var effPinIdx = (l.dstPin !== undefined) ? l.dstPin : 0; - var y2 = dn._ly - dH/2 + dH * (effPinIdx + 1) / (dstIsSRRS ? 3 : (dstInP + 1)); - var x2 = dn._lx - dW/2; - var isActive = wiring && wiring.fromIdx === l.srcIdx; - var isLinkSelected = (selectedLinkIdx === i); - - var midX = (x1 + x2) / 2; - var lineAlpha = (isActive || isLinkSelected) ? 1 : 0.6; - var lineW = (isActive || isLinkSelected) ? 3 : 2; - var lineColor = isLinkSelected ? '#FF5722' : (isActive ? '#FFC107' : 'rgba(180,200,220,' + lineAlpha + ')'); - - // 发光效果 (阴影线) - ctx.beginPath(); - ctx.strokeStyle = isLinkSelected ? 'rgba(255,87,34,0.3)' : 'rgba(255,200,0,' + (lineAlpha * 0.2) + ')'; - ctx.lineWidth = lineW + 4; - ctx.moveTo(x1, y1); - ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2); - ctx.stroke(); - - // 主线 - ctx.beginPath(); - ctx.strokeStyle = lineColor; - ctx.lineWidth = lineW; - ctx.moveTo(x1, y1); - ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2); - ctx.stroke(); - - // 箭头 (指向目标) - ctx.fillStyle = lineColor; - ctx.beginPath(); - ctx.moveTo(x2, y2); - ctx.lineTo(x2 - 7, y2 - 3.5); - ctx.lineTo(x2 - 7, y2 + 3.5); - ctx.closePath(); - ctx.fill(); - - // 选中连线删除按钮标记(×) - if (isLinkSelected) { - var labelX = (x1 + x2) / 2, labelY = (y1 + y2) / 2 - 18; - ctx.fillStyle = '#FF5722'; - ctx.font = 'bold 14px sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText('✕ 右键删除', labelX, labelY); - } - } - - // 临时连线(仅编辑器) - if (wiring && wiring.fromIdx < nodes.length) { - var fn = nodes[wiring.fromIdx]; - var fnIsIO = (fn.type === NT_OUTPUT || fn.type === NT_INPUT); - var fW = fnIsIO ? IO_W : GATE_W; - var fx = (wiring.fromSide === 'out') ? fn._lx + fW/2 : fn._lx - fW/2; - var fy = fn._ly; - ctx.beginPath(); - ctx.strokeStyle = '#FFD700'; - ctx.lineWidth = 2; - ctx.setLineDash([6, 4]); - ctx.moveTo(fx, fy); - ctx.lineTo(fx + (wiring.fromSide === 'out' ? 100 : -100), fy); - ctx.stroke(); - ctx.setLineDash([]); - } - - // ---- 节点层 ---- - for (var j = 0; j < nodes.length; j++) { - var n = nodes[j]; - var info = NODE_INFO[n.type] || { name: '?', color: '#555', icon: '?' }; - var isIO = (n.type === NT_OUTPUT || n.type === NT_INPUT); - var inP = getNodeInPins(n); - var nw = isIO ? IO_W : GATE_W; - var nh = isIO ? IO_H : (GATE_H + Math.max(0, inP - 2) * 14); - var x = n._lx, y = n._ly; - var halfW = nw/2, halfH = nh/2; - var isWiringSrc = wiring && wiring.fromIdx === j; - var isSelected = (selectedIdx === j); - - // 发光描边宽度 - var glowW = (isSelected || isWiringSrc) ? 3 : 1; - - // --- 阴影 --- - ctx.fillStyle = 'rgba(0,0,0,0.5)'; - ctx.beginPath(); - ctx.roundRect(x - halfW + SHADOW_OFF, y - halfH + SHADOW_OFF, nw, nh, CORNER_R); - ctx.fill(); - - // --- 主体渐变 --- - var grad = ctx.createLinearGradient(x - halfW, y - halfH, x - halfW, y + halfH); - if (isSelected) { - grad.addColorStop(0, '#3b3b6b'); - grad.addColorStop(1, '#282848'); - } else if (isWiringSrc) { - grad.addColorStop(0, '#3c3c50'); - grad.addColorStop(1, '#252538'); - } else { - grad.addColorStop(0, '#2e2e4a'); - grad.addColorStop(1, '#1e1e36'); - } - ctx.fillStyle = grad; - ctx.beginPath(); - ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R); - ctx.fill(); - - // --- 描边 --- - ctx.strokeStyle = isSelected ? '#FFD700' : (isWiringSrc ? '#FFC107' : info.color); - ctx.lineWidth = glowW; - ctx.beginPath(); - ctx.roundRect(x - halfW, y - halfH, nw, nh, CORNER_R); - ctx.stroke(); - - // --- 左侧色条 --- - ctx.fillStyle = info.color; - ctx.beginPath(); - ctx.roundRect(x - halfW, y - halfH, LEFT_BAR_W, nh, [CORNER_R, 0, 0, CORNER_R]); - ctx.fill(); - - // --- 顶部类型标签 --- - if (info.name) { - var labelBgX = x - halfW + LEFT_BAR_W; - var labelBgW = info.name.length * 7.5 + 18; - var labelBgY = y - halfH + 2; - var labelBgH = 16; - ctx.fillStyle = info.color; - ctx.globalAlpha = 0.25; - ctx.fillRect(labelBgX, labelBgY, Math.min(labelBgW, nw - LEFT_BAR_W - 4), labelBgH); - ctx.globalAlpha = 1; - - ctx.fillStyle = isIO ? '#fff' : info.color; - ctx.font = '9px -apple-system, sans-serif'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillText(info.name, labelBgX + 4, labelBgY + 2.5); - } - - // --- 文本内容 --- - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - if (isIO) { - var decId = n.id; - var sig = n.saddr ? n : findSignalByNo(decId); - var prefix = (n.type === NT_INPUT) ? '📥' : '📤'; - var roleLabel = (n.type === NT_INPUT) ? '输入点' : '输出点'; - - // ID 行 - ctx.fillStyle = '#e0e0e0'; - ctx.font = 'bold 12px -apple-system, sans-serif'; - ctx.fillText(prefix + ' ' + roleLabel + ' #' + decId, x, y - 18); - - // saddr 行 - ctx.fillStyle = '#aaa'; - ctx.font = '10px monospace'; - var saddrText = (sig && sig.saddr) ? sig.saddr : '—'; - ctx.fillText(saddrText, x, y); - - // desc 行 - var descText = (sig && sig.desc) ? sig.desc : ''; - if (descText) { - ctx.fillStyle = '#888'; - ctx.font = '9px -apple-system, sans-serif'; - var truncated = descText.length > 18 ? descText.substring(0, 16) + '…' : descText; - ctx.fillText(truncated, x, y + 14); - } - } else { - // 逻辑门 - var gateIdx = n.id & 0xFF; - var realIn = 0, realOut = 0; - for (var li = 0; li < links.length; li++) { - if (links[li].dstIdx === j) realIn++; - if (links[li].srcIdx === j) realOut++; - } - - // 中部:名称 + 序号 - ctx.fillStyle = '#e8e8e8'; - ctx.font = 'bold 11px -apple-system, sans-serif'; - var titleText = info.icon + ' #' + gateIdx; - if (n.type === NT_62P || n.type === NT_62D) { - titleText += ' (' + (n.delayMs || 1000) + 'ms)'; - } - ctx.fillText(titleText, x, y - 6); - - // 连接数 / 延时信息 - ctx.fillStyle = '#999'; - ctx.font = '9px monospace'; - if (n.type === NT_SR || n.type === NT_RS) { - ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14); - } else { - ctx.fillText(realIn + '入 ' + realOut + '出', x, y + 14); - } - } - - // --- 输入引脚 --- - if (inP > 0) { - var isSRRS = (n.type === NT_SR || n.type === NT_RS); - var renderPins = isSRRS ? 2 : (isIO ? inP : 1); - for (var p = 0; p < renderPins; p++) { - var py = y - nh/2 + nh * (p + 1) / (isSRRS ? 3 : (inP + 1)); - // 外圈 - ctx.fillStyle = '#2e2e4a'; - ctx.beginPath(); - ctx.arc(x - halfW, py, 7, 0, Math.PI * 2); - ctx.fill(); - ctx.strokeStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50'; - ctx.lineWidth = 2; - ctx.stroke(); - // 内芯 - ctx.fillStyle = isSRRS ? (p === 0 ? '#FFC107' : '#FF5722') : '#4CAF50'; - ctx.beginPath(); - ctx.arc(x - halfW, py, 3.5, 0, Math.PI * 2); - ctx.fill(); - // SR/RS 引脚标签 - if (isSRRS) { - ctx.fillStyle = '#fff'; - ctx.font = 'bold 10px monospace'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - var srl = (n.type === NT_SR) ? ['S', 'R'] : ['R', 'S']; - ctx.fillText(srl[p], x - halfW - 12, py); - } - } - } - - // --- 输出引脚 --- - if (info.outPins > 0) { - // 外圈 - ctx.fillStyle = '#2e2e4a'; - ctx.beginPath(); - ctx.arc(x + halfW, y, 7, 0, Math.PI * 2); - ctx.fill(); - ctx.strokeStyle = '#FF9800'; - ctx.lineWidth = 2; - ctx.stroke(); - // 内芯 - ctx.fillStyle = '#FF9800'; - ctx.beginPath(); - ctx.arc(x + halfW, y, 3.5, 0, Math.PI * 2); - ctx.fill(); - } - } - } - - function renderEditorTab(container) { - editNodes = []; - editLinks = []; - editKey = ''; - editName = ''; - editOnlineCfgId = ''; - editSelectedNode = -1; - editSelectedLink = -1; - editWiring = null; - editDragNode = null; - editPanX = 0; editPanY = 0; - editScale = 1.0; - editLoadedFromFile = false; - editNextGateIdx = {}; - - container.innerHTML = '' - + '
' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '
' - + '
' - + '
' - + '' - + '
' - + '
' - + '
' - + '💡 双击模块可修改配置 | 单击选中/引脚连线 | 右键删除连线 | 滚轮缩放 | 按住空白拖动画布' + + '
' + + '
' + '
'; - - // 工具栏事件 - document.getElementById('plc-edit-key').oninput = function() { editKey = this.value; }; - document.getElementById('plc-edit-name').oninput = function() { editName = this.value; }; - document.getElementById('plc-edit-add-in').onclick = function() { showAddInputModal(); }; - document.getElementById('plc-edit-add-out').onclick = function() { showAddOutputModal(); }; - document.getElementById('plc-edit-add-gate').onclick = function() { showAddGateModal(); }; - document.getElementById('plc-edit-zoomin').onclick = function() { editScale = Math.min(3, editScale * 1.2); drawEditor(); }; - document.getElementById('plc-edit-zoomout').onclick = function() { editScale = Math.max(0.2, editScale / 1.2); drawEditor(); }; - document.getElementById('plc-edit-del').onclick = function() { deleteSelectedNode(); }; - document.getElementById('plc-edit-modify').onclick = function() { modifySelectedNode(); }; - document.getElementById('plc-edit-generate').onclick = function() { generateAndSaveConfig(); }; - - setupEditorCanvas(); - drawEditor(); - - // 加载当前 Reclose_logic.xml 为编辑起点 - function initEditor() { - loadLogicFile(function() { - if (logicGraphs.length > 0) { - // 优先使用非 bak 文件的最新配置 - var g = null; - for (var gi = 0; gi < logicGraphs.length; gi++) { - if (!logicGraphs[gi]._source || logicGraphs[gi]._source.indexOf('.bak') < 0) { - g = logicGraphs[gi]; break; - } - } - if (!g && logicGraphs.length > 0) g = logicGraphs[0]; - if (g) { - editKey = g.key; - editName = g.name || ''; - editOnlineCfgId = onlineCfgId; - // 复制节点到编辑器 - for (var i = 0; i < g.nodes.length; i++) { - var n = g.nodes[i]; - var newNode = { - type: n.type, id: n.id, x: n.x, y: n.y, - _lx: n.x, _ly: n.y, - saddr: '', desc: '', no: n.id - }; - // 查找信号信息 - var sig = findSignalByNo(n.id); - if (sig) { newNode.saddr = sig.saddr; newNode.desc = sig.desc; } - // 逻辑门 inPins — 优先使用文件中保存的配置 - if (n.type !== NT_INPUT && n.type !== NT_OUTPUT) { - newNode.inPins = (n.inPins !== undefined) ? n.inPins : ((NODE_INFO[n.type] || {}).inPins || 2); - } - // 延时字段 - if (n.delayMs !== undefined) newNode.delayMs = n.delayMs; - // Comb id 和 RS 信息 - if (n.combId !== undefined) newNode.combId = n.combId; - if (n.rsR) newNode.rsR = n.rsR; - if (n.rsS) newNode.rsS = n.rsS; - editNodes.push(newNode); - } - // 复制链路(按节点在新数组中的索引) - for (var j = 0; j < g.links.length; j++) { - var l = g.links[j]; - var si = findEditNodeIndex(l.src_type, l.src_id); - var di = findEditNodeIndex(l.dst_type, l.dst_id); - if (si >= 0 && di >= 0) { - var el = { srcIdx: si, dstIdx: di }; - if (l.dstPin !== undefined) el.dstPin = l.dstPin; - editLinks.push(el); - } - } - editLoadedFromFile = true; - - // 按类型统计最大序号,后续新增时每种类型独立递增 - var maxByType = {}; - for (var di = 0; di < editNodes.length; di++) { - var dn = editNodes[di]; - if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue; - var idx = dn.id & 0xFF; - if (idx >= (maxByType[dn.type] || -1)) maxByType[dn.type] = idx; - } - for (var t in maxByType) { - editNextGateIdx[t] = maxByType[t] + 1; - } - - // 消除已有的重复 type+id 并基于每类型独立序号分配新 ID - var seenIds = {}; - for (var di = 0; di < editNodes.length; di++) { - var dn = editNodes[di]; - if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue; - var key2 = dn.type + ':' + (dn.id & 0xFF); - if (seenIds[key2]) { - var next = editNextGateIdx[dn.type] || 0; - editNodes[di].id = (dn.id & ~0xFF) | next; - editNextGateIdx[dn.type] = next + 1; - } else { - seenIds[key2] = true; - } - } - - // 更新输入框 - var keyEl = document.getElementById('plc-edit-key'); - var nameEl = document.getElementById('plc-edit-name'); - if (keyEl) keyEl.value = editKey; - if (nameEl) nameEl.value = editName; - } else { - editKey = String(Date.now()); - editName = ''; - var keyEl = document.getElementById('plc-edit-key'); - if (keyEl) keyEl.value = editKey; - } - drawEditor(); - } - }); - } - - if (dcOutSignals.length === 0) { - loadDcOutSignals(function() { initEditor(); }); - } else { - initEditor(); - } + var lh='图例: '; + for(var t in NODE_INFO){var inf=NODE_INFO[t];lh+=''+inf.icon+' '+inf.name+'';} + document.getElementById('plc-view-legend').innerHTML=lh; + document.getElementById('plc-sig-search').oninput=refreshSigPanel; + document.getElementById('plc-sig-reload').onclick=function(){loadedFiles['config/SYSTEM/datacenter_out.xml']=null;dcOutSignals=[];loadDcOutSignals(function(){refreshSigPanel();});}; + document.getElementById('plc-view-zoomin').onclick=function(){viewScale=Math.min(3,viewScale*1.2);drawViewer();}; + document.getElementById('plc-view-zoomout').onclick=function(){viewScale=Math.max(0.2,viewScale/1.2);drawViewer();}; + document.getElementById('plc-view-reset').onclick=function(){viewAutoFit=true;drawViewer();}; + document.getElementById('plc-view-reload').onclick=function(){loadedFiles['config/PLC/Reclose_logic.xml']=null;loadedFiles['config/PLC/Reclose_logic.xml.bak']=null;logicGraphs=[];loadAllConfigs(function(){refreshViewSelect();});}; + document.getElementById('plc-view-graph').onchange=function(){viewAutoFit=true;drawViewer();}; + setupViewerCanvas(); + if(logicGraphs.length===0)loadAllConfigs(function(){refreshViewSelect();});else refreshViewSelect(); } - function findEditNodeIndex(type, id) { - for (var i = 0; i < editNodes.length; i++) { - var n = editNodes[i]; - if (n.type === type) { - if (type <= 1 && n.id === id) return i; - if (type > 1 && (n.id & 0xFF) === (id & 0xFF)) return i; - } - } - return -1; - } + function loadAllConfigs(cb){var pending=2;function done(){pending--;if(pending<=0&&cb)cb();}requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml');done();});requestFile('config/PLC/Reclose_logic.xml.bak',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml.bak');done();});} - function deleteSelectedNode() { - if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) { - showToast('请先在画布上双击或单击选中一个模块', 'err'); - return; - } - var node = editNodes[editSelectedNode]; - var idx = editSelectedNode; - var label = getNodeDisplayLabel(node); - var html = ''; - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - document.getElementById('plc-modal-cancel').onclick = closeModal; - document.getElementById('plc-modal-ok').onclick = function() { - closeModal(); - var delType = node.type; - editLinks = editLinks.filter(function(l) { return l.srcIdx !== idx && l.dstIdx !== idx; }); - for (var i = 0; i < editLinks.length; i++) { - if (editLinks[i].srcIdx > idx) editLinks[i].srcIdx--; - if (editLinks[i].dstIdx > idx) editLinks[i].dstIdx--; - } - editNodes.splice(idx, 1); - editSelectedNode = -1; + function loadCurrentLogic(cb){requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){showToast('加载失败','err');return;}parseLogicXml(c,'Reclose_logic.xml');if(cb)cb();});} - // 压缩同类型门序号:删除 #1 后 #2→#1, #3→#2 … - if (delType !== NT_INPUT && delType !== NT_OUTPUT) { - var sameType = []; - for (var s = 0; s < editNodes.length; s++) { - if (editNodes[s].type === delType) sameType.push(s); - } - sameType.sort(function(a, b) { return (editNodes[a].id & 0xFF) - (editNodes[b].id & 0xFF); }); - for (var a = 0; a < sameType.length; a++) { - editNodes[sameType[a]].id = (editNodes[sameType[a]].id & ~0xFF) | a; - } - editNextGateIdx[delType] = sameType.length; - } + function refreshViewSelect(){var sel=document.getElementById('plc-view-graph');if(!sel)return;sel.innerHTML='';for(var i=0;i=0?' [备份]':'';sel.innerHTML+='';}if(logicGraphs.length>0){sel.value='0';setTimeout(function(){drawViewer();},100);}} - drawEditor(); - showToast('模块已删除', 'ok'); - }; - } + function setupViewerCanvas(){var wrap=document.getElementById('plc-view-canvas-wrap'),cnv=document.getElementById('plc-view-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=wrap.clientHeight-4;viewAutoFit=true;drawViewer();}resize();window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){viewDrag={sx:e.clientX,sy:e.clientY,px:viewPanX,py:viewPanY};};cnv.onmousemove=function(e){if(!viewDrag)return;viewPanX=viewDrag.px+(e.clientX-viewDrag.sx);viewPanY=viewDrag.py+(e.clientY-viewDrag.sy);drawViewer();};cnv.onmouseup=function(){viewDrag=null;};cnv.onmouseleave=function(){viewDrag=null;};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,ns=Math.max(0.2,Math.min(3,viewScale*d)),r=cnv.getBoundingClientRect();viewPanX=(e.clientX-r.left)-((e.clientX-r.left)-viewPanX)*(ns/viewScale);viewPanY=(e.clientY-r.top)-((e.clientY-r.top)-viewPanY)*(ns/viewScale);viewScale=ns;drawViewer();};} - function modifySelectedNode() { - if (editSelectedNode < 0 || editSelectedNode >= editNodes.length) { - showToast('请先在画布上双击或单击选中一个模块', 'err'); - return; - } - var idx = editSelectedNode; - var node = editNodes[idx]; - if (node.type === NT_INPUT || node.type === NT_OUTPUT) { - showRebindSignalModal(idx); - } else if (node.type === NT_AND || node.type === NT_OR || node.type === NT_NOT) { - showGateConfigModal(idx); - } else if (node.type === NT_62P || node.type === NT_62D) { - showDelayEditModal(idx); - } else if (node.type === NT_SR || node.type === NT_RS) { - showRSInfoModal(idx); - } else { - showGateConfigModal(idx); - } - } + function drawViewer(){var cnv=document.getElementById('plc-view-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);var sel=document.getElementById('plc-view-graph');if(!sel||sel.value===''){ctx.fillStyle='#556677';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText('请选择一个逻辑图',W/2,H/2);return;}var g=logicGraphs[parseInt(sel.value)];if(!g)return;var vn=[],bMinX=Infinity,bMaxX=-Infinity,bMinY=Infinity,bMaxY=-Infinity;for(var vi=0;vibMaxX)bMaxX=vv.x;if(vv.ybMaxY)bMaxY=vv.y;var vsig=findSignalByNo(vv.id);vn.push({type:vv.type,id:vv.id,_lx:vv.x,_ly:vv.y,saddr:(vsig&&vsig.saddr)||'',desc:(vsig&&vsig.desc)||'',no:vv.id});}if(viewAutoFit){viewAutoFit=false;var cw=bMaxX-bMinX,ch=bMaxY-bMinY;if(cw<=0)cw=800;if(ch<=0)ch=600;viewScale=Math.min((W-160)/cw,(H-160)/ch,1.2);viewPanX=W/2-(bMinX+cw/2)*viewScale;viewPanY=H/2-(bMinY+ch/2)*viewScale;}var vl=[];for(var vj=0;vj=0&&vld>=0)vl.push({srcIdx:vls,dstIdx:vld,dstPin:vlk.dstPin||0});}ctx.strokeStyle='rgba(255,255,255,0.04)';ctx.lineWidth=1;var gs=50,sx=Math.floor(-viewPanX/viewScale/gs)*gs,sy=Math.floor(-viewPanY/viewScale/gs)*gs;ctx.save();ctx.translate(viewPanX,viewPanY);ctx.scale(viewScale,viewScale);for(var gx=sx-gs;gx⏱️ ' + info.name + ' — 修改延时' - + '

当前延时: ' + curMs + ' ms

' - + '' - + '
' - + '' - + '' - + '
'; - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - document.getElementById('plc-modal-ok').onclick = function() { - var val = parseInt(document.getElementById('plc-delay-input').value); - if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; } - editNodes[nodeIdx].delayMs = val; - closeModal(); - drawEditor(); - showToast('延时已更新: ' + val + ' ms', 'ok'); - }; - document.getElementById('plc-delay-input').onkeydown = function(e) { - if (e.key === 'Enter') document.getElementById('plc-modal-ok').click(); - }; - setTimeout(function() { - var inp = document.getElementById('plc-delay-input'); - if (inp) { inp.focus(); inp.select(); } - }, 100); - } - - /** SR/RS 双击:显示 R/S 连接信息 */ - function showRSInfoModal(nodeIdx) { - var node = editNodes[nodeIdx]; - var info = NODE_INFO[node.type] || {}; - var sSrc = '', rSrc = ''; - for (var li = 0; li < editLinks.length; li++) { - var l = editLinks[li]; - if (l.dstIdx === nodeIdx) { - var srcNode = editNodes[l.srcIdx]; - var desc = getEditorNodeLabel(srcNode).replace(/\n/g, ' '); - if (node.type === NT_SR) { - if (l.dstPin === 0) sSrc = desc; else rSrc = desc; - } else { - if (l.dstPin === 1) sSrc = desc; else rSrc = desc; - } - } - } - var html = ''; - - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - document.getElementById('plc-modal-save').onclick = function() { - var v = parseInt(document.getElementById('plc-gate-pins').value); - if (isNaN(v) || v < minPins) { showToast('最少 ' + minPins + ' 个输入引脚', 'err'); return; } - if (v > maxPins) { showToast('最多 ' + maxPins + ' 个输入引脚', 'err'); return; } - editNodes[nodeIdx].inPins = v; - closeModal(); - drawEditor(); - showToast(info.name + ' 输入引脚已设为 ' + v, 'ok'); - }; - } + function findEditIdx(type,id){for(var i=0;i1&&(n.id&0xFF)===(id&0xFF))return i;}}return -1;} /* ---- 编辑器 Canvas ---- */ - function setupEditorCanvas() { - var wrap = document.getElementById('plc-edit-canvas-wrap'); - var canvas = document.getElementById('plc-edit-canvas'); - if (!wrap || !canvas) return; + function setupEditorCanvas(){var wrap=document.getElementById('plc-edit-canvas-wrap'),cnv=document.getElementById('plc-edit-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=Math.max(500,wrap.clientHeight-4);editScale=0.7;editPanX=cnv.width/2;editPanY=cnv.height/2;drawEditor();}setTimeout(resize,50);window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;var ph=hitTestPin(mx,my);if(ph){if(editWiring){if(editWiring.fromIdx!==ph.nodeIdx){if(editWiring.fromSide==='out'&&ph.side==='in'){var si=NODE_INFO[editNodes[editWiring.fromIdx].type]||{},om=si.outMax||99,oc=0;for(var lc=0;lc=om){showToast('输出上限('+om+')','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}var lk={srcIdx:editWiring.fromIdx,dstIdx:ph.nodeIdx,dstPin:ph.pinIdx};var dn=editNodes[ph.nodeIdx];if(dn.type===NT_SR||dn.type===NT_RS){for(var lc2=0;lc2=0){editSelectedLink=lh;editSelectedNode=-1;editDragNode=null;if(editWiring)editWiring=null;drawEditor();return;}var nh=hitTestNode(mx,my);if(nh>=0){editSelectedNode=nh;editSelectedLink=-1;editDragNode=nh;editDragOffX=mx-editNodes[nh]._lx;editDragOffY=my-editNodes[nh]._ly;drawEditor();return;}editSelectedNode=-1;editSelectedLink=-1;if(editWiring)editWiring=null;editDragCanvas={sx:e.clientX,sy:e.clientY,px:editPanX,py:editPanY};cnv.style.cursor='grabbing';drawEditor();};cnv.onmousemove=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;if(editDragCanvas){editPanX=editDragCanvas.px+(e.clientX-editDragCanvas.sx);editPanY=editDragCanvas.py+(e.clientY-editDragCanvas.sy);drawEditor();return;}if(editDragNode!==null){editNodes[editDragNode]._lx=mx-editDragOffX;editNodes[editDragNode]._ly=my-editDragOffY;drawEditor();return;}if(editWiring&&editWiring.fromIdx=0?'grab':'default');};cnv.onmouseup=function(e){if(editDragCanvas){editDragCanvas=null;cnv.style.cursor='default';return;}if(editDragNode!==null){var nd=editNodes[editDragNode],ni=editDragNode;var dx=Math.abs((e.clientX-cnv.getBoundingClientRect().left-editPanX)/editScale-nd._lx-editDragOffX);var dy=Math.abs((e.clientY-cnv.getBoundingClientRect().top-editPanY)/editScale-nd._ly-editDragOffY);if(dx<3&&dy<3){var n=Date.now();if(ni===editLastClickNode&&(n-editLastClickTime)<400){onNodeDblClick(ni);editLastClickTime=0;editLastClickNode=-1;}else{editLastClickTime=n;editLastClickNode=ni;}}}editDragNode=null;};cnv.onmouseleave=function(){editDragNode=null;editDragCanvas=null;cnv.style.cursor='default';};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,r=cnv.getBoundingClientRect();var ns=Math.max(0.2,Math.min(3,editScale*d));editPanX=(e.clientX-r.left)-((e.clientX-r.left)-editPanX)*(ns/editScale);editPanY=(e.clientY-r.top)-((e.clientY-r.top)-editPanY)*(ns/editScale);editScale=ns;drawEditor();};cnv.oncontextmenu=function(e){e.preventDefault();if(editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;editSelectedNode=-1;showToast('连线已删除','ok');drawEditor();return;}if(editSelectedNode>=0){deleteSelectedNode();return;}};cnv.tabIndex=0;cnv.onkeydown=function(e){if(e.key==='Escape'){editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();}if(e.key==='Delete'&&editSelectedNode>=0){deleteSelectedNode();}if(e.key==='Delete'&&editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;showToast('连线已删除','ok');drawEditor();}};} - function resize() { - canvas.width = wrap.clientWidth; - canvas.height = Math.max(500, wrap.clientHeight - 4); - // 居中画布 - editScale = 0.7; - editPanX = canvas.width / 2; - editPanY = canvas.height / 2; - drawEditor(); - } - setTimeout(resize, 50); - window.addEventListener('resize', function() { resize(); }); + function hitTestNode(mx,my){for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n);if(mx>n._lx-sz.w/2&&mxn._ly-sz.h/2&&my=0;i--){var n=editNodes[i],sz=getNodeSize(n),info=NODE_INFO[n.type]||NODE_INFO[0];var isIO=(n.type===NT_INPUT||n.type===NT_OUTPUT),isSR=(n.type===NT_SR||n.type===NT_RS);var inP=getNodeInPins(n);if(info.inPins>0){var tp=isSR?2:(isIO?inP:1);for(var p=0;p0){if(Math.sqrt((mx-(n._lx+sz.w/2))*(mx-(n._lx+sz.w/2))+(my-n._ly)*(my-n._ly))<=pinR)return{nodeIdx:i,side:'out',pinIdx:0};}}return null;} + function hitTestLink(mx,my){for(var i=0;i=editNodes.length||l.dstIdx>=editNodes.length)continue;var sn=editNodes[l.srcIdx],dn=editNodes[l.dstIdx],szS=getNodeSize(sn),szD=getNodeSize(dn);var x1=sn._lx+szS.w/2,y1=sn._ly,isSR=(dn.type===NT_SR||dn.type===NT_RS);var inP=getNodeInPins(dn),dpi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-szD.h/2+szD.h*(dpi+1)/(isSR?3:(inP+1)),x2=dn._lx-szD.w/2;var t=Math.max(0,Math.min(1,((mx-x1)*(x2-x1)+(my-y1)*(y2-y1))/((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))));var px=x1+t*(x2-x1),py=y1+t*(y2-y1);if(Math.sqrt((mx-px)*(mx-px)+(my-py)*(my-py))<10)return i;}return -1;} - canvas.onmousedown = function(e) { - var rect = canvas.getBoundingClientRect(); - var mx = (e.clientX - rect.left - editPanX) / editScale; - var my = (e.clientY - rect.top - editPanY) / editScale; + function drawEditor(){var cnv=document.getElementById('plc-edit-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);ctx.save();ctx.translate(editPanX,editPanY);ctx.scale(editScale,editScale);ctx.strokeStyle='rgba(255,255,255,0.05)';ctx.lineWidth=1;var gs=40,sx=Math.floor(-editPanX/editScale/gs)*gs,sy=Math.floor(-editPanY/editScale/gs)*gs;for(var gx=sx-gs;gx=0&&editSelectedNode= outMax) { - showToast('该模块输出已达上限 (' + outMax + ')', 'err'); - editWiring = null; - editSelectedNode = -1; - drawEditor(); - return; - } - var link = { srcIdx: editWiring.fromIdx, dstIdx: pinHit.nodeIdx, dstPin: pinHit.pinIdx }; - // SR/RS 每个引脚只能接一根线 - var dstNode = editNodes[pinHit.nodeIdx]; - if (dstNode.type === NT_SR || dstNode.type === NT_RS) { - for (var lc2 = 0; lc2 < editLinks.length; lc2++) { - if (editLinks[lc2].dstIdx === pinHit.nodeIdx && editLinks[lc2].dstPin === pinHit.pinIdx) { - var pinLabel = (dstNode.type === NT_SR) ? ['S','R'] : ['R','S']; - showToast('引脚 ' + pinLabel[pinHit.pinIdx] + ' 已有连线,每个引脚只能接一根线', 'err'); - editWiring = null; - editSelectedNode = -1; - drawEditor(); - return; - } - } - } - editLinks.push(link); - showToast('连线已添加', 'ok'); - } else if (editWiring.fromSide === 'in' && pinHit.side === 'out') { - editLinks.push({ srcIdx: pinHit.nodeIdx, dstIdx: editWiring.fromIdx, dstPin: 0 }); - showToast('连线已添加', 'ok'); - } else { - showToast('不能这样连线', 'err'); - } - } - editWiring = null; - editSelectedNode = -1; - editSelectedLink = -1; - drawEditor(); - return; - } else { - editWiring = { fromIdx: pinHit.nodeIdx, fromSide: pinHit.side, fromPin: pinHit.pinIdx }; - editSelectedNode = -1; - editSelectedLink = -1; - drawEditor(); - return; - } - } + /* ---- 节点操作 ---- */ + function onNodeDblClick(idx){var nd=editNodes[idx];editSelectedNode=idx;drawEditor();if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(idx);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(idx);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(idx);else showGateCfgModal(idx);} + function modifySelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode];if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(editSelectedNode);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(editSelectedNode);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(editSelectedNode);else showGateCfgModal(editSelectedNode);} + function deleteSelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode],idx=editSelectedNode,lb=getNodeDisplayLabel(nd);showConfirm('🗑 确认删除','确定删除 '+escHtml(lb)+' 及其连线?',function(){var dt=nd.type;editLinks=editLinks.filter(function(l){return l.srcIdx!==idx&&l.dstIdx!==idx;});for(var i=0;iidx)editLinks[i].srcIdx--;if(editLinks[i].dstIdx>idx)editLinks[i].dstIdx--;}editNodes.splice(idx,1);editSelectedNode=-1;if(dt!==NT_INPUT&&dt!==NT_OUTPUT){var st=[];for(var s=0;s= 0) { - editSelectedLink = linkHit; - editSelectedNode = -1; - editDragNode = null; - if (editWiring) { editWiring = null; } - drawEditor(); - return; - } + /* ---- Modals ---- */ + function showConfirm(title,msg,cb){var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){closeModal();cb();};} + function closeModal(){var m=document.getElementById('plc-modal');if(m)m.remove();} - // 检查是否点击了节点 → 拖拽或选中 - var nodeHit = hitTestNode(mx, my); - if (nodeHit >= 0) { - editSelectedNode = nodeHit; - editSelectedLink = -1; - editDragNode = nodeHit; - editDragOffX = mx - editNodes[nodeHit]._lx; - editDragOffY = my - editNodes[nodeHit]._ly; - drawEditor(); - return; - } + function showIOModal(title,cb){if(dcOutSignals.length===0){showToast('信号列表为空','err');return;}var h='
';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;var si2=document.getElementById('plc-modal-search');if(si2)si2.oninput=function(){var q=this.value.toLowerCase();document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.style.display=(r.textContent||'').toLowerCase().indexOf(q)!==-1?'':'none';});};document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(){var no=parseInt(this.dataset.no)||0,sa=this.dataset.saddr,de=this.dataset.desc||'';var rs=cb(no,sa,de);if(rs!==false)closeModal();};});} - // 空白处取消选中和连线,并开始画布拖拽 - editSelectedNode = -1; - editSelectedLink = -1; - if (editWiring) { editWiring = null; } - editDragCanvas = { startX: e.clientX, startY: e.clientY, panX: editPanX, panY: editPanY }; - canvas.style.cursor = 'grabbing'; - drawEditor(); - }; + function showAddIOModal(ioType){var title=ioType===NT_INPUT?'📥 绑定输入信号':'📤 绑定输出信号';showIOModal(title,function(no,saddr,desc){editNodes.push({type:ioType,id:no,x:0,y:0,_lx:ioType===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===ioType;}).length*110,saddr:saddr,desc:desc,no:no});drawEditor();showToast('已添加: '+saddr,'ok');return true;});} + function showRebindModal(idx){var nd=editNodes[idx],title=nd.type===NT_INPUT?'📥 重新绑定输入':'📤 重新绑定输出';showIOModal(title,function(no,saddr,desc){editNodes[idx].no=no;editNodes[idx].id=no;editNodes[idx].saddr=saddr;editNodes[idx].desc=desc;drawEditor();showToast('已绑定: '+saddr,'ok');return true;});} - canvas.onmousemove = function(e) { - var rect = canvas.getBoundingClientRect(); - var mx = (e.clientX - rect.left - editPanX) / editScale; - var my = (e.clientY - rect.top - editPanY) / editScale; + function showAddGateModal(){var gates=[{t:NT_AND,n:'与门AND',d:'全1出1',i:'🔷'},{t:NT_OR,n:'或门OR',d:'有1出1',i:'🔶'},{t:NT_NOT,n:'非门NOT',d:'取反',i:'🔺'},{t:NT_62P,n:'62p延时',d:'开延时',i:'⏱️'},{t:NT_62D,n:'62d延时',d:'关延时',i:'⏱️'},{t:NT_RISING,n:'上升沿',d:'0→1脉冲',i:'⬆️'},{t:NT_FALLING,n:'下降沿',d:'1→0脉冲',i:'⬇️'},{t:NT_SR,n:'SR触发器',d:'S优先',i:'🔒'},{t:NT_RS,n:'RS触发器',d:'R优先',i:'🔒'}];var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt),info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;if(gt===NT_62P||gt===NT_62D){closeModal();showDelayInputModal(gt,ni,info);return;}addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;closeModal();drawEditor();};});} - if (editDragCanvas) { - editPanX = editDragCanvas.panX + (e.clientX - editDragCanvas.startX); - editPanY = editDragCanvas.panY + (e.clientY - editDragCanvas.startY); - drawEditor(); - return; - } + function showDelayInputModal(gateType,nextIdx,info){var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}closeModal();addGateNode(gateType,nextIdx,v,info);editNextGateIdx[gateType]=nextIdx+1;drawEditor();};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp=document.getElementById('plc-delay-input');if(inp){inp.focus();inp.select();}},100);} + function showDelayEditModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},cur=nd.delayMs||1000;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}editNodes[idx].delayMs=v;closeModal();drawEditor();showToast('延时更新: '+v+' ms','ok');};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp2=document.getElementById('plc-delay-input');if(inp2){inp2.focus();inp2.select();}},100);} + function showRSInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},sSrc='',rSrc='';for(var li=0;li

S(置位): '+(sSrc||'未连接')+'

R(复位): '+(rSrc||'未连接')+'

💡 拖输出到S/R引脚连线

';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;} + function showGateCfgModal(idx){var nd=editNodes[idx],cur=getNodeInPins(nd),info=NODE_INFO[nd.type]||{},mx=info.inMax||99;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){var v=parseInt(document.getElementById('plc-gate-pins').value);if(isNaN(v)||v<1){showToast('最少1个','err');return;}if(v>mx){showToast('最多'+mx+'个','err');return;}editNodes[idx].inPins=v;closeModal();drawEditor();showToast(info.name+' 输入='+v,'ok');};} + function addGateNode(gateType,nextIdx,delayMs,info){var pos=findEmptyPos();var nd={type:gateType,id:nextIdx,x:0,y:0,_lx:pos.x,_ly:pos.y,saddr:'',desc:'',no:0};if(delayMs>0)nd.delayMs=delayMs;editNodes.push(nd);showToast('已添加: '+(info?info.name:NODE_INFO[gateType].name)+' #'+nextIdx,'ok');} + function findEmptyPos(){if(editNodes.length===0)return{x:0,y:0};var mxX=-Infinity,mnX=Infinity,mxY=-Infinity,mnY=Infinity;for(var i=0;imxX)mxX=n._lx;if(n._lxmxY)mxY=n._ly;if(n._ly=nodes.length||l.dstIdx>=nodes.length)continue;var sn=nodes[l.srcIdx],dn=nodes[l.dstIdx];var sIO=(sn.type===NT_OUTPUT||sn.type===NT_INPUT),dIO=(dn.type===NT_OUTPUT||dn.type===NT_INPUT);var dSR=(dn.type===NT_SR||dn.type===NT_RS);var sW=sIO?IW:GW,dW=dIO?IW:GW,dH=dIO?IH:(dSR?86:GH);var x1=sn._lx+sW/2,y1=sn._ly;var dIP=getNodeInPins(dn),epi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-dH/2+dH*(epi+1)/(dSR?3:(dIP+1)),x2=dn._lx-dW/2;var isAct=wiring&&wiring.fromIdx===l.srcIdx,isSel=(selectedLinkIdx===i);var mid=(x1+x2)/2,la=isAct||isSel?1:0.6,lw=isAct||isSel?3:2;var lc=isSel?'#FF5722':(isAct?'#FFC107':'rgba(180,200,220,'+la+')');ctx.beginPath();ctx.strokeStyle=isSel?'rgba(255,87,34,0.3)':'rgba(255,200,0,'+(la*0.2)+')';ctx.lineWidth=lw+4;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.beginPath();ctx.strokeStyle=lc;ctx.lineWidth=lw;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.fillStyle=lc;ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-7,y2-3.5);ctx.lineTo(x2-7,y2+3.5);ctx.closePath();ctx.fill();if(isSel){var lx2=(x1+x2)/2,ly2=(y1+y2)/2-18;ctx.fillStyle='#FF5722';ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('✕ 右键删除',lx2,ly2);}}if(wiring&&wiring.fromIdx18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:1);for(var p=0;p0){ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x+hw,y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#FF9800';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#FF9800';ctx.beginPath();ctx.arc(x+hw,y,3.5,0,Math.PI*2);ctx.fill();}}} - if (editWiring && editWiring.fromIdx < editNodes.length) { - drawEditor(); - } + /* ================================================================ + * XML 解析 (与原工程完全一致) + * ================================================================ */ + function parseLogicXml(xmlStr,sourceName){var parser=new DOMParser(),doc=parser.parseFromString(xmlStr,'text/xml'),root=doc.documentElement;if(!root||root.nodeName!=='PLCConfig'){showToast('根元素不是 PLCConfig','err');return;}var thisKey=root.getAttribute('key')||sourceName,thisName=root.getAttribute('name')||'';var g={key:thisKey,name:thisName,nodes:[],links:[]};var GTM={INPUT:1,OUTPUT:0,OR:2,AND:3,NOT:4,T62P:5,T62D:6,RISING:7,SR:8,RS:9,FALLING:10};var sigMap={};function ens(ioType,no,x,y){var k=ioType+':'+no;if(sigMap[k]!==undefined){if(x!==undefined){g.nodes[sigMap[k]].x=x;g.nodes[sigMap[k]].y=y;}return sigMap[k];}g.nodes.push({type:ioType,id:no,x:x||0,y:y||0});sigMap[k]=g.nodes.length-1;return g.nodes.length-1;}function eng(type,id,x,y,inPins,delayMs){for(var i=0;i=0)g.links.push({src_type:g.nodes[upi].type,src_id:g.nodes[upi].id,dst_type:t,dst_id:gid});}c=c.nextElementSibling;}var ie=ce.firstElementChild;while(ie&&ie.nodeName!=='Inputs')ie=ie.nextElementSibling;if(ie){var s=ie.firstElementChild;while(s){if(s.nodeName==='Signal'){var no=parseInt(s.getAttribute('no'));ens(NT_INPUT,no,parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));g.links.push({src_type:NT_INPUT,src_id:no,dst_type:t,dst_id:gid});}s=s.nextElementSibling;}}var oe=ce.firstElementChild;while(oe&&oe.nodeName!=='Outputs')oe=oe.nextElementSibling;if(oe){var s2=oe.firstElementChild;while(s2){if(s2.nodeName==='Signal'){var no2=parseInt(s2.getAttribute('no'));ens(NT_OUTPUT,no2,parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));g.links.push({src_type:t,src_id:gid,dst_type:NT_OUTPUT,dst_id:no2});}s2=s2.nextElementSibling;}}return gi;}var chains=root.getElementsByTagName('Chain');for(var ci=0;ci= 0 ? 'grab' : 'default'); - }; - - canvas.onmouseup = function(e) { - if (editDragCanvas) { - editDragCanvas = null; - canvas.style.cursor = 'default'; - return; - } - if (editDragNode !== null) { - var node = editNodes[editDragNode]; - var nodeIdx = editDragNode; - var dx = Math.abs((e.clientX - canvas.getBoundingClientRect().left - editPanX) / editScale - node._lx - editDragOffX); - var dy = Math.abs((e.clientY - canvas.getBoundingClientRect().top - editPanY) / editScale - node._ly - editDragOffY); - // 小于 3px 视为点击(非拖拽) - if (dx < 3 && dy < 3) { - // 双击检测:同节点 400ms 内连续点击 - var now = Date.now(); - if (nodeIdx === editLastClickNode && (now - editLastClickTime) < 400) { - onNodeDoubleClicked(nodeIdx); - editLastClickTime = 0; - editLastClickNode = -1; - } else { - editLastClickTime = now; - editLastClickNode = nodeIdx; - } - } - } - editDragNode = null; - }; - canvas.onmouseleave = function() { editDragNode = null; editDragCanvas = null; canvas.style.cursor = 'default'; }; - - canvas.onwheel = function(e) { - e.preventDefault(); - var delta = e.deltaY > 0 ? 0.9 : 1.1; - var rect = canvas.getBoundingClientRect(); - var mx = e.clientX - rect.left; - var my = e.clientY - rect.top; - var ns = Math.max(0.2, Math.min(3, editScale * delta)); - editPanX = mx - (mx - editPanX) * (ns / editScale); - editPanY = my - (my - editPanY) * (ns / editScale); - editScale = ns; - drawEditor(); - }; - - // 右键 —— 删除选中的连线 / 节点 - canvas.oncontextmenu = function(e) { - e.preventDefault(); - if (editSelectedLink >= 0) { - editLinks.splice(editSelectedLink, 1); - editSelectedLink = -1; - editSelectedNode = -1; - showToast('连线已删除', 'ok'); - drawEditor(); - return; - } - // 如果选中了节点也可以用右键删除 - if (editSelectedNode >= 0) { - deleteSelectedNode(); - return; - } - }; - - canvas.tabIndex = 0; - canvas.onkeydown = function(e) { - if (e.key === 'Escape') { editWiring = null; editSelectedNode = -1; editSelectedLink = -1; drawEditor(); } - if (e.key === 'Delete' && editSelectedNode >= 0) { deleteSelectedNode(); } - if (e.key === 'Delete' && editSelectedLink >= 0) { editLinks.splice(editSelectedLink, 1); editSelectedLink = -1; showToast('连线已删除', 'ok'); drawEditor(); } - }; - } - - /* ---- 获取节点的实际 inPins(优先使用节点自身的配置) ---- */ - function getNodeInPins(node) { - if (node.inPins !== undefined && node.inPins > 0) return node.inPins; - var info = NODE_INFO[node.type] || {}; - return info.inPins || 0; - } - - function getNodeSize(node) { - var isIO = (node.type === NT_OUTPUT || node.type === NT_INPUT); - var isSRRS = (node.type === NT_SR || node.type === NT_RS); - var inP = getNodeInPins(node); - var h = isIO ? 80 : (isSRRS ? 86 : (66 + Math.max(0, inP - 2) * 14)); - var w = isIO ? 250 : 170; - return { w: w, h: h }; - } - - function hitTestNode(mx, my) { - for (var i = editNodes.length - 1; i >= 0; i--) { - var n = editNodes[i]; - var sz = getNodeSize(n); - var x = n._lx, y = n._ly; - if (mx > x - sz.w/2 && mx < x + sz.w/2 && my > y - sz.h/2 && my < y + sz.h/2) return i; - } - return -1; - } - - function hitTestPin(mx, my) { - var pinR = 8; - for (var i = editNodes.length - 1; i >= 0; i--) { - var n = editNodes[i]; - var sz = getNodeSize(n); - var info = NODE_INFO[n.type] || NODE_INFO[0]; - var x = n._lx, y = n._ly; - var isIO = (n.type === NT_INPUT || n.type === NT_OUTPUT); - var isSRRS = (n.type === NT_SR || n.type === NT_RS); - var inP = getNodeInPins(n); - // 输入引脚(左侧) - if (info.inPins > 0) { - var testPins = isSRRS ? 2 : (isIO ? inP : 1); - for (var p = 0; p < testPins; p++) { - var py = y - sz.h/2 + sz.h * (p + 1) / (isSRRS ? 3 : (inP + 1)); - var dx = mx - (x - sz.w/2), dy = my - py; - if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'in', pinIdx: p }; - } - } - // 输出引脚(右侧) - if (info.outPins > 0) { - var dx = mx - (x + sz.w/2), dy = my - y; - if (Math.sqrt(dx*dx + dy*dy) <= pinR) return { nodeIdx: i, side: 'out', pinIdx: 0 }; - } - } - return null; - } - - function hitTestLink(mx, my) { - for (var i = 0; i < editLinks.length; i++) { - var l = editLinks[i]; - if (l.srcIdx >= editNodes.length || l.dstIdx >= editNodes.length) continue; - var sn = editNodes[l.srcIdx], dn = editNodes[l.dstIdx]; - var szSrc = getNodeSize(sn), szDst = getNodeSize(dn); - var x1 = sn._lx + szSrc.w/2, y1 = sn._ly; - var dstIsSRRS = (dn.type === NT_SR || dn.type === NT_RS); - var gateInPins = getNodeInPins(dn); - var dstPinIdx2 = (l.dstPin !== undefined) ? l.dstPin : 0; - var y2 = dn._ly - szDst.h/2 + szDst.h * (dstPinIdx2 + 1) / (dstIsSRRS ? 3 : (gateInPins + 1)); - var x2 = dn._lx - szDst.w/2; - var t = ((mx - x1)*(x2 - x1) + (my - y1)*(y2 - y1)) / ((x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1)); - t = Math.max(0, Math.min(1, t)); - var px = x1 + t * (x2 - x1), py = y1 + t * (y2 - y1); - if (Math.sqrt((mx - px)*(mx - px) + (my - py)*(my - py)) < 10) return i; - } - return -1; - } - - function getNodeLabel(n) { - var info = NODE_INFO[n.type] || {}; - var label = (info.name || '?'); - if (n.saddr) label += ' ' + n.saddr; - else if (n.type <= 1) label += ' #' + n.id; - else label += ' #' + (n.id & 0xFF); - return label; - } - - /* ---- 编辑器绘制 ---- */ - function drawEditor() { - var canvas = document.getElementById('plc-edit-canvas'); - if (!canvas) return; - var ctx = canvas.getContext('2d'); - var W = canvas.width, H = canvas.height; - - ctx.clearRect(0, 0, W, H); - ctx.fillStyle = '#1a1a2e'; - ctx.fillRect(0, 0, W, H); - - ctx.save(); - ctx.translate(editPanX, editPanY); - ctx.scale(editScale, editScale); - - // 网格 - ctx.strokeStyle = 'rgba(255,255,255,0.05)'; - ctx.lineWidth = 1; - var gridSize = 40; - var startX = Math.floor(-editPanX / editScale / gridSize) * gridSize; - var startY = Math.floor(-editPanY / editScale / gridSize) * gridSize; - for (var gx = startX - gridSize; gx < startX + W/editScale + gridSize; gx += gridSize) { - ctx.beginPath(); ctx.moveTo(gx, startY - gridSize); ctx.lineTo(gx, startY + H/editScale + gridSize); ctx.stroke(); - } - for (var gy = startY - gridSize; gy < startY + H/editScale + gridSize; gy += gridSize) { - ctx.beginPath(); ctx.moveTo(startX - gridSize, gy); ctx.lineTo(startX + W/editScale + gridSize, gy); ctx.stroke(); - } - - // 区域标签 - ctx.fillStyle = 'rgba(255,255,255,0.08)'; - ctx.font = '28px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('📥 输入信号区', -250, -200); - ctx.fillText('🧮 逻辑运算区', 0, -200); - ctx.fillText('📤 输出信号区', 250, -200); - - // 共用绘制 - drawGraphContent(ctx, editNodes, editLinks, editSelectedNode, editSelectedLink, editWiring); - - ctx.restore(); - - // 更新信息栏 - var infoEl = document.getElementById('plc-edit-info'); - if (infoEl) { - var selInfo = ''; - if (editSelectedNode >= 0 && editSelectedNode < editNodes.length) { - selInfo = ' | 🔍 选中: ' + getNodeDisplayLabel(editNodes[editSelectedNode]); - } - infoEl.innerHTML = '节点: ' + editNodes.length + ' | 连线: ' + editLinks.length - + ' | Key: ' + escHtml(editKey) + ' | 名称: ' + escHtml(editName || '(未设置)') - + (editWiring ? ' | 🔗 连线中... (点击目标引脚完成,ESC取消)' : '') - + selInfo - + ' | 右键删除连线 | 滚轮缩放 | Delete键删除选中'; - } - } - - /* ---- 添加输入/输出/运算弹窗 ---- */ - function showAddInputModal() { - if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; } - - var signals = dcOutSignals; - var html = ''; - - showModalCustom(html, function(no, saddr, desc) { - editNodes.push({ - type: NT_INPUT, id: no, x: 0, y: 0, - _lx: -260, _ly: editNodes.filter(function(n){return n.type===NT_INPUT;}).length * 110, - saddr: saddr, desc: desc, no: no - }); - drawEditor(); - showToast('已添加输入: ' + saddr, 'ok'); - return true; - }); - } - - function showAddOutputModal() { - if (dcOutSignals.length === 0) { showToast('请先加载信号', 'err'); return; } - - var signals = dcOutSignals; - var html = ''; - - showModalCustom(html, function(no, saddr, desc) { - editNodes.push({ - type: NT_OUTPUT, id: no, x: 0, y: 0, - _lx: 260, _ly: editNodes.filter(function(n){return n.type===NT_OUTPUT;}).length * 110, - saddr: saddr, desc: desc, no: no - }); - drawEditor(); - showToast('已添加输出: ' + saddr, 'ok'); - return true; - }); - } - - function showAddGateModal() { - var gates = [ - { type: NT_AND, name: '与门 (AND)', desc: '2输入1输出,所有输入为1时输出1', icon: '🔷' }, - { type: NT_OR, name: '或门 (OR)', desc: '2输入1输出,任一输入为1时输出1', icon: '🔶' }, - { type: NT_NOT, name: '非门 (NOT)', desc: '1输入1输出,输入取反', icon: '🔺' }, - { type: NT_62P, name: '62p 延时', desc: '输入1延时输出1,输入0立即输出0', icon: '⏱️' }, - { type: NT_62D, name: '62d 延时', desc: '输入1立即输出1,输入0延时输出0', icon: '⏱️' }, - { type: NT_RISING, name: '上升沿', desc: '0→1跳变时输出脉冲', icon: '⬆️' }, - { type: NT_FALLING, name: '下降沿', desc: '1→0跳变时输出脉冲', icon: '⬇️' }, - { type: NT_SR, name: 'SR触发器', desc: 'S置位优先,S=1置位 R=1复位', icon: '🔒' }, - { type: NT_RS, name: 'RS触发器', desc: 'R复位优先,R=1复位 S=1置位', icon: '🔒' } - ]; - - var html = ''; - - // 自定义 modal 处理 - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - - var rows = document.querySelectorAll('#plc-modal .plc-modal-row'); - for (var j = 0; j < rows.length; j++) { - rows[j].onclick = function(e) { - e.stopPropagation(); - var gateType = parseInt(this.getAttribute('data-gate-type')); - var info = NODE_INFO[gateType]; - var nextIdx = editNextGateIdx[gateType] || 0; - - // 延时模块:弹出自定义延时输入框 - if (gateType === NT_62P || gateType === NT_62D) { - closeModal(); - showDelayInputModal(gateType, nextIdx, info); - return; - } - - // 非延时模块:直接添加 - addGateNode(gateType, nextIdx, 0, info); - editNextGateIdx[gateType] = nextIdx + 1; - closeModal(); - drawEditor(); - }; - } - } - - /** 弹出延时毫秒输入框 */ - function showDelayInputModal(gateType, nextIdx, info) { - var html = ''; - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - document.getElementById('plc-modal-ok').onclick = function() { - var val = parseInt(document.getElementById('plc-delay-input').value); - if (isNaN(val) || val < 0) { showToast('请输入有效的延时毫秒数', 'err'); return; } - closeModal(); - addGateNode(gateType, nextIdx, val, info); - editNextGateIdx[gateType] = nextIdx + 1; - drawEditor(); - }; - // 回车确认 - document.getElementById('plc-delay-input').onkeydown = function(e) { - if (e.key === 'Enter') document.getElementById('plc-modal-ok').click(); - }; - setTimeout(function() { - var inp = document.getElementById('plc-delay-input'); - if (inp) { inp.focus(); inp.select(); } - }, 100); - } - - /** 添加门节点到编辑区,自动找空白位置 */ - function addGateNode(gateType, nextIdx, delayMs, info) { - var pos = findEmptyPosition(); - var node = { - type: gateType, id: nextIdx, - x: 0, y: 0, - _lx: pos.x, _ly: pos.y, - saddr: '', desc: '', no: 0 - }; - if (delayMs > 0) node.delayMs = delayMs; - editNodes.push(node); - showToast('已添加: ' + (info ? info.name : NODE_INFO[gateType].name) + ' #' + nextIdx, 'ok'); - } - - /** 寻找画布上最空白的区域 */ - function findEmptyPosition() { - if (editNodes.length === 0) return { x: 0, y: 0 }; - // 先计算已有节点的包围盒,在右侧偏下寻找空白 - var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - for (var i = 0; i < editNodes.length; i++) { - var n = editNodes[i]; - if (n._lx < minX) minX = n._lx; - if (n._lx > maxX) maxX = n._lx; - if (n._ly < minY) minY = n._ly; - if (n._ly > maxY) maxY = n._ly; - } - // 在所有节点下方居中放置 - var candidates = [ - { x: 0, y: maxY + 140 }, - { x: 0, y: minY - 140 }, - { x: maxX + 300, y: (minY + maxY) / 2 }, - { x: minX - 300, y: (minY + maxY) / 2 } - ]; - for (var c = 0; c < candidates.length; c++) { - if (!isPositionOccupied(candidates[c].x, candidates[c].y)) { - return candidates[c]; - } - } - return { x: 0, y: maxY + 140 }; - } - - function isPositionOccupied(x, y) { - for (var i = 0; i < editNodes.length; i++) { - var n = editNodes[i]; - if (Math.abs(n._lx - x) < 120 && Math.abs(n._ly - y) < 80) return true; - } - return false; - } - - function showModal(html, onSelect) { - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - - // 搜索 - var searchInput = document.getElementById('plc-modal-search'); - if (searchInput) { - searchInput.oninput = function() { - var q = this.value.toLowerCase(); - var rows = document.querySelectorAll('#plc-modal .plc-modal-row'); - for (var r = 0; r < rows.length; r++) { - var text = (rows[r].textContent || '').toLowerCase(); - rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none'; - } - }; - } - - var rows = document.querySelectorAll('#plc-modal .plc-modal-row'); - for (var j = 0; j < rows.length; j++) { - rows[j].onclick = function() { - if (this.classList.contains('plc-modal-used')) { - showToast('该信号已被使用', 'err'); - return; - } - var noAttr = this.getAttribute('data-no'); - var saddr = this.getAttribute('data-saddr'); - var desc = this.getAttribute('data-desc') || ''; - var no = parseInt(noAttr) || 0; - var result = onSelect(no, saddr, desc); - if (result !== false) closeModal(); - }; - } - } - - function showModalCustom(html, onSelect) { - var div = document.createElement('div'); - div.innerHTML = html; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - - var searchInput = document.getElementById('plc-modal-search'); - if (searchInput) { - searchInput.oninput = function() { - var q = this.value.toLowerCase(); - var rows = document.querySelectorAll('#plc-modal .plc-modal-row'); - for (var r = 0; r < rows.length; r++) { - var text = (rows[r].textContent || '').toLowerCase(); - rows[r].style.display = (text.indexOf(q) !== -1) ? '' : 'none'; - } - }; - } - - var rows = document.querySelectorAll('#plc-modal .plc-modal-row'); - for (var j = 0; j < rows.length; j++) { - rows[j].onclick = function() { - if (this.classList.contains('plc-modal-used')) { - showToast('该信号已被使用', 'err'); - return; - } - var noAttr = this.getAttribute('data-no'); - var saddr = this.getAttribute('data-saddr'); - var desc = this.getAttribute('data-desc') || ''; - var no = parseInt(noAttr) || 0; - var result = onSelect(no, saddr, desc); - if (result !== false) closeModal(); - }; - } - } - - function closeModal() { - var m = document.getElementById('plc-modal'); - if (m) m.remove(); - } - - /* ---- 生成并保存配置 ---- */ - // 生成 XML 配置文件 + /* ================================================================ + * XML 生成 + 保存 + * ================================================================ */ function generateXmlConfig() { - var typeNames = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING'}; - var adjIn = {}, adjOut = {}; - for (var i = 0; i < editNodes.length; i++) { adjIn[i] = []; adjOut[i] = []; } + var tN = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING'}; + var aI = {}, aO = {}; + for (var i = 0; i < editNodes.length; i++) { aI[i] = []; aO[i] = []; } for (var j = 0; j < editLinks.length; j++) { var l = editLinks[j]; - adjOut[l.srcIdx].push(l.dstIdx); - adjIn[l.dstIdx].push(l.srcIdx); + aO[l.srcIdx].push(l.dstIdx); + aI[l.dstIdx].push(l.srcIdx); } - - var visited = {}; - var chains = []; + var vis = {}, chs = []; for (var s = 0; s < editNodes.length; s++) { - if (visited[s]) continue; - var c = [], q = [s]; visited[s] = true; + if (vis[s]) continue; + var c = [], q = [s]; vis[s] = true; while (q.length > 0) { var cur = q.shift(); c.push(cur); - var nbrs = adjOut[cur].concat(adjIn[cur]); - for (var nb = 0; nb < nbrs.length; nb++) - if (!visited[nbrs[nb]]) { visited[nbrs[nb]] = true; q.push(nbrs[nb]); } + var nb = aO[cur].concat(aI[cur]); + for (var nb2 = 0; nb2 < nb.length; nb2++) + if (!vis[nb[nb2]]) { vis[nb[nb2]] = true; q.push(nb[nb2]); } } - chains.push(c); + chs.push(c); } - - function sigXml(n) { return ''; } - function gateTag(n, nodeIdx) { - var tn = typeNames[n.type] || '?'; + function sx(n) { return ''; } + function gt(n, ni) { + var tn = tN[n.type] || '?'; var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"'; if (n.inPins && n.inPins > 2) a += ' inPins="' + n.inPins + '"'; if (n.type === NT_62P || n.type === NT_62D) a += ' delayMs="' + (n.delayMs || 1000) + '"'; - // SR/RS: 输出 R/S 属性 - if ((n.type === NT_SR || n.type === NT_RS) && nodeIdx !== undefined) { - var srLinks = []; - for (var li = 0; li < editLinks.length; li++) { - if (editLinks[li].dstIdx === nodeIdx) srLinks.push(editLinks[li]); + if ((n.type === NT_SR || n.type === NT_RS) && ni !== undefined) { + var sl = []; + for (var li = 0; li < editLinks.length; li++) + if (editLinks[li].dstIdx === ni) sl.push(editLinks[li]); + var hp = false; + for (var si2 = 0; si2 < sl.length; si2++) + if (sl[si2].dstPin !== undefined) { hp = true; break; } + if (!hp) for (var si3 = 0; si3 < sl.length; si3++) { + if (n.type === NT_SR) sl[si3].dstPin = si3; + else sl[si3].dstPin = 1 - si3; } - // 按 dstPin 排序,未设置的用出现顺序兜底 - var hasPin = false; - for (var sl = 0; sl < srLinks.length; sl++) { - if (srLinks[sl].dstPin !== undefined) { hasPin = true; break; } - } - if (!hasPin) { - // 兜底:SR 按出现顺序 S→R,RS 按出现顺序 R→S - for (var sl2 = 0; sl2 < srLinks.length; sl2++) { - if (n.type === NT_SR) srLinks[sl2].dstPin = sl2; // 0=S, 1=R - else srLinks[sl2].dstPin = 1 - sl2; // 0=R, 1=S - } - } - for (var sl3 = 0; sl3 < srLinks.length; sl3++) { - var l3 = srLinks[sl3]; - var srcNode = editNodes[l3.srcIdx]; - var srcDesc = ''; - if (srcNode.type === NT_INPUT) { - srcDesc = 'Input#' + srcNode.id; - } else { - srcDesc = 'Comb#' + (srcNode.combId !== undefined ? srcNode.combId : (srcNode.id & 0xFF)); - } - if (n.type === NT_SR) { - if (l3.dstPin === 0) a += ' S="' + srcDesc + '"'; - else a += ' R="' + srcDesc + '"'; - } else { - if (l3.dstPin === 1) a += ' S="' + srcDesc + '"'; - else a += ' R="' + srcDesc + '"'; + var dedupSR = {}; + for (var si4 = 0; si4 < sl.length; si4++) { + var l3 = sl[si4], snl = editNodes[l3.srcIdx]; + var sd2 = ''; + if (snl.type === NT_INPUT) sd2 = 'Input#' + snl.id; + else sd2 = 'Comb#' + (snl.combId !== undefined ? snl.combId : (snl.id & 0xFF)); + var dk = snl.type + '_' + snl.id + '_' + (l3.dstPin || 0); + if (!dedupSR[dk]) { + dedupSR[dk] = true; + if (n.type === NT_SR) { + if (l3.dstPin === 0) a += ' S="' + sd2 + '"'; + else a += ' R="' + sd2 + '"'; + } else { + if (l3.dstPin === 1) a += ' S="' + sd2 + '"'; + else a += ' R="' + sd2 + '"'; + } } } } return ''; } - - /** 后序遍历分配 combId(自底向上,从 0 起) */ - function assignCombIds(idx, ctrObj, cGatesArr) { + function ac(idx, ctr, cga) { editNodes[idx].combId = undefined; - for (var ui = 0; ui < adjIn[idx].length; ui++) { - var up = adjIn[idx][ui]; - if (editNodes[up].type > 1 && cGatesArr.indexOf(up) >= 0) { - assignCombIds(up, ctrObj, cGatesArr); - } + for (var ui = 0; ui < aI[idx].length; ui++) { + var up = aI[idx][ui]; + if (editNodes[up].type > 1 && cga.indexOf(up) >= 0) ac(up, ctr, cga); } - editNodes[idx].combId = ctrObj.count++; + editNodes[idx].combId = ctr.count++; } - - function combXml(idx, indent) { - var s = ''; + function cx(idx, ind) { + var s2 = ''; var cid = editNodes[idx].combId; - s += indent + '\n'; - var innerIndent = indent + ' '; - var di = []; - for (var fi = 0; fi < adjIn[idx].length; fi++) - if (editNodes[adjIn[idx][fi]].type === NT_INPUT) di.push(adjIn[idx][fi]); - if (di.length > 0) { - s += innerIndent + '\n'; - for (var d = 0; d < di.length; d++) s += innerIndent + ' ' + sigXml(editNodes[di[d]]) + '\n'; - s += innerIndent + '\n'; + s2 += ind + '\n'; + var i2 = ind + ' '; + var di2 = []; + for (var fi = 0; fi < aI[idx].length; fi++) + if (editNodes[aI[idx][fi]].type === NT_INPUT) di2.push(aI[idx][fi]); + if (di2.length > 0) { + s2 += i2 + '\n'; + for (var d = 0; d < di2.length; d++) s2 += i2 + ' ' + sx(editNodes[di2[d]]) + '\n'; + s2 += i2 + '\n'; } - for (var ui = 0; ui < adjIn[idx].length; ui++) - if (editNodes[adjIn[idx][ui]].type > 1 && cGates.indexOf(adjIn[idx][ui]) >= 0) { - s += combXml(adjIn[idx][ui], innerIndent); - } - s += innerIndent + gateTag(editNodes[idx], idx) + '\n'; + for (var ui2 = 0; ui2 < aI[idx].length; ui2++) + if (editNodes[aI[idx][ui2]].type > 1 && cGs.indexOf(aI[idx][ui2]) >= 0) + s2 += cx(aI[idx][ui2], i2); + s2 += i2 + gt(editNodes[idx], idx) + '\n'; var dos = []; - for (var oi = 0; oi < adjOut[idx].length; oi++) - if (editNodes[adjOut[idx][oi]].type === NT_OUTPUT) dos.push(adjOut[idx][oi]); + for (var oi = 0; oi < aO[idx].length; oi++) + if (editNodes[aO[idx][oi]].type === NT_OUTPUT) dos.push(aO[idx][oi]); if (dos.length > 0) { - s += innerIndent + '\n'; - for (var o = 0; o < dos.length; o++) s += innerIndent + ' ' + sigXml(editNodes[dos[o]]) + '\n'; - s += innerIndent + '\n'; + s2 += i2 + '\n'; + for (var o = 0; o < dos.length; o++) s2 += i2 + ' ' + sx(editNodes[dos[o]]) + '\n'; + s2 += i2 + '\n'; } - s += indent + '\n'; - return s; + s2 += ind + '\n'; + return s2; } - + var cGs = []; var xml = '\n'; - xml += '\n'; - - for (var ci = 0; ci < chains.length; ci++) { - var chain = chains[ci]; - var cIns = [], cGates = [], cOuts = []; - for (var ni = 0; ni < chain.length; ni++) { - var nd = editNodes[chain[ni]]; - if (nd.type === NT_INPUT) cIns.push(chain[ni]); - else if (nd.type === NT_OUTPUT) cOuts.push(chain[ni]); - else cGates.push(chain[ni]); + xml += '\n'; + for (var ci = 0; ci < chs.length; ci++) { + var ch = chs[ci], cis = [], cos = []; + cGs = []; + for (var ni = 0; ni < ch.length; ni++) { + var nd = editNodes[ch[ni]]; + if (nd.type === NT_INPUT) cis.push(ch[ni]); + else if (nd.type === NT_OUTPUT) cos.push(ch[ni]); + else cGs.push(ch[ni]); } - if (cGates.length === 0) continue; - - xml += ' \n'; - - if (cGates.length === 1) { - // 简单链: Inputs → Gate → Outputs + if (cGs.length === 0) continue; + xml += ' \n'; + if (cGs.length === 1) { xml += ' \n'; - for (var ii = 0; ii < cIns.length; ii++) xml += ' ' + sigXml(editNodes[cIns[ii]]) + '\n'; + for (var ii = 0; ii < cis.length; ii++) xml += ' ' + sx(editNodes[cis[ii]]) + '\n'; xml += ' \n'; - xml += ' ' + gateTag(editNodes[cGates[0]], cGates[0]) + '\n'; + xml += ' ' + gt(editNodes[cGs[0]], cGs[0]) + '\n'; xml += ' \n'; - for (var oo = 0; oo < cOuts.length; oo++) xml += ' ' + sigXml(editNodes[cOuts[oo]]) + '\n'; + for (var oo = 0; oo < cos.length; oo++) xml += ' ' + sx(editNodes[cos[oo]]) + '\n'; xml += ' \n'; } else { - // 组合链: 链级门 + Comb + Outputs - // 链级门 = 下游没有 cGates 内其他门的门(只连输出或链外) - var chainGates = []; - for (var gg = 0; gg < cGates.length; gg++) { - var allToOut = true; - for (var so = 0; so < adjOut[cGates[gg]].length; so++) - if (cGates.indexOf(adjOut[cGates[gg]][so]) >= 0) { allToOut = false; break; } - if (allToOut) chainGates.push(cGates[gg]); + var chG = []; + for (var gg = 0; gg < cGs.length; gg++) { + var ato = true; + for (var so = 0; so < aO[cGs[gg]].length; so++) + if (cGs.indexOf(aO[cGs[gg]][so]) >= 0) { ato = false; break; } + if (ato) chG.push(cGs[gg]); } - // combRoots: 直接馈入 chainGate 的门(combXml 会递归处理上游嵌套) - var combRootSet = {}; - for (var cg = 0; cg < chainGates.length; cg++) { - for (var ui = 0; ui < adjIn[chainGates[cg]].length; ui++) { - var up = adjIn[chainGates[cg]][ui]; - if (cGates.indexOf(up) >= 0) combRootSet[up] = true; + var crS = {}; + for (var cg = 0; cg < chG.length; cg++) + for (var ui3 = 0; ui3 < aI[chG[cg]].length; ui3++) { + var up = aI[chG[cg]][ui3]; + if (cGs.indexOf(up) >= 0) crS[up] = true; } - } - // 去重:若 A 是 B 的下游,只保留 B(combXml 从 B 开始会递归包含 A) - var combRootArr = Object.keys(combRootSet).map(Number); - var combRoots = []; - for (var rr = 0; rr < combRootArr.length; rr++) { - var isDownstream = false; - for (var rr2 = 0; rr2 < combRootArr.length; rr2++) { + var crA = Object.keys(crS).map(Number), crs = []; + for (var rr = 0; rr < crA.length; rr++) { + var isD = false; + for (var rr2 = 0; rr2 < crA.length; rr2++) { if (rr === rr2) continue; - var visited2 = {}; - var queue2 = [combRootArr[rr2]]; - visited2[combRootArr[rr2]] = true; - while (queue2.length > 0) { - var cur2 = queue2.shift(); - if (cur2 === combRootArr[rr]) { isDownstream = true; break; } - for (var si = 0; si < adjOut[cur2].length; si++) { - var nx = adjOut[cur2][si]; - if (!visited2[nx] && cGates.indexOf(nx) >= 0) { visited2[nx] = true; queue2.push(nx); } + var v2 = {}, q2 = [crA[rr2]]; v2[crA[rr2]] = true; + while (q2.length > 0) { + var c2 = q2.shift(); + if (c2 === crA[rr]) { isD = true; break; } + for (var si5 = 0; si5 < aO[c2].length; si5++) { + var nx = aO[c2][si5]; + if (!v2[nx] && cGs.indexOf(nx) >= 0) { v2[nx] = true; q2.push(nx); } } } - if (isDownstream) break; + if (isD) break; } - if (!isDownstream) combRoots.push(combRootArr[rr]); + if (!isD) crs.push(crA[rr]); } - // 输出 Comb(每个 combRoot 对应一个顶层 Comb,内部递归嵌套) - // 先分配 combId - var combIdCtr = { count: 0 }; - for (var tt = 0; tt < combRoots.length; tt++) { - assignCombIds(combRoots[tt], combIdCtr, cGates); - } - for (var tt = 0; tt < combRoots.length; tt++) { - xml += combXml(combRoots[tt], ' '); - } - // 输出链级门 — 若存在直接 Input 连接(未进入 Comb 的),先行输出 - for (var cg = 0; cg < chainGates.length; cg++) { - var cgNode = editNodes[chainGates[cg]]; - var directInputs = []; - for (var cui = 0; cui < adjIn[chainGates[cg]].length; cui++) { - var upIdx = adjIn[chainGates[cg]][cui]; - if (editNodes[upIdx].type === NT_INPUT) { - directInputs.push(upIdx); - } + var cidC = {count: 0}; + for (var tt = 0; tt < crs.length; tt++) ac(crs[tt], cidC, cGs); + for (var tt = 0; tt < crs.length; tt++) xml += cx(crs[tt], ' '); + for (var cg2 = 0; cg2 < chG.length; cg2++) { + var cgN = editNodes[chG[cg2]], dis = [], seenDis = {}; + for (var cui = 0; cui < aI[chG[cg2]].length; cui++) { + var ui4 = aI[chG[cg2]][cui]; + if (editNodes[ui4].type === NT_INPUT && !seenDis[ui4]) { seenDis[ui4] = true; dis.push(ui4); } } - if (directInputs.length > 0) { + if (dis.length > 0) { xml += ' \n'; - for (var di2 = 0; di2 < directInputs.length; di2++) { - xml += ' ' + sigXml(editNodes[directInputs[di2]]) + '\n'; - } + for (var di3 = 0; di3 < dis.length; di3++) xml += ' ' + sx(editNodes[dis[di3]]) + '\n'; xml += ' \n'; } - xml += ' ' + gateTag(cgNode, chainGates[cg]) + '\n'; + xml += ' ' + gt(cgN, chG[cg2]) + '\n'; } - // 链级 Outputs — 仅包含直接连到 chainGate 的输出信号 - // (Comb 内部的输出信号已在 combXml 中处理) - var chainOutSet = {}; - for (var cg2 = 0; cg2 < chainGates.length; cg2++) { - for (var oi = 0; oi < adjOut[chainGates[cg2]].length; oi++) { - var outIdx = adjOut[chainGates[cg2]][oi]; - if (editNodes[outIdx].type === NT_OUTPUT) chainOutSet[outIdx] = true; + var coS = {}; + for (var cg3 = 0; cg3 < chG.length; cg3++) + for (var oi2 = 0; oi2 < aO[chG[cg3]].length; oi2++) { + var oi3 = aO[chG[cg3]][oi2]; + if (editNodes[oi3].type === NT_OUTPUT) coS[oi3] = true; } + for (var oo2 = 0; oo2 < cos.length; oo2++) { + var hgs = false; + for (var ai = 0; ai < aI[cos[oo2]].length; ai++) + if (cGs.indexOf(aI[cos[oo2]][ai]) >= 0) hgs = true; + if (!hgs) coS[cos[oo2]] = true; } - var chainOuts = Object.keys(chainOutSet).map(Number); - // 同时保留直接连到输入信号(无门)的输出 - for (var oo2 = 0; oo2 < cOuts.length; oo2++) { - var hasGateSrc = false; - for (var ai = 0; ai < adjIn[cOuts[oo2]].length; ai++) { - if (cGates.indexOf(adjIn[cOuts[oo2]][ai]) >= 0) hasGateSrc = true; - } - if (!hasGateSrc) chainOutSet[cOuts[oo2]] = true; - } - chainOuts = Object.keys(chainOutSet).map(Number); - if (chainOuts.length > 0) { + var coL = Object.keys(coS).map(Number); + if (coL.length > 0) { xml += ' \n'; - for (var oo = 0; oo < chainOuts.length; oo++) xml += ' ' + sigXml(editNodes[chainOuts[oo]]) + '\n'; + for (var oo3 = 0; oo3 < coL.length; oo3++) xml += ' ' + sx(editNodes[coL[oo3]]) + '\n'; xml += ' \n'; } } - xml += ' \n'; } - xml += '\n'; return xml; } - function escXml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - function generateAndSaveConfig() { - if (editNodes.length === 0) { showToast('请先添加节点', 'err'); return; } - var hasInput = editNodes.some(function(n) { return n.type === NT_INPUT; }); - var hasOutput = editNodes.some(function(n) { return n.type === NT_OUTPUT; }); - if (!hasInput || !hasOutput) { showToast('至少需要一个输入和一个输出', 'err'); return; } - if (editLinks.length === 0) { showToast('请先连线', 'err'); return; } - var seenIds = {}; - for (var di = 0; di < editNodes.length; di++) { - var dn = editNodes[di]; - if (dn.type === NT_INPUT || dn.type === NT_OUTPUT) continue; - var key2 = dn.type + ':' + (dn.id & 0xFF); - if (seenIds[key2]) { - var next = editNextGateIdx[dn.type] || 0; - editNodes[di].id = (dn.id & ~0xFF) | next; - editNextGateIdx[dn.type] = next + 1; - } else { seenIds[key2] = true; } - } + function generateAndSaveConfig(){if(editNodes.length===0){showToast('请先添加节点','err');return;}var hi=editNodes.some(function(n){return n.type===NT_INPUT;}),ho=editNodes.some(function(n){return n.type===NT_OUTPUT;});if(!hi||!ho){showToast('至少需要一个输入和输出','err');return;}if(editLinks.length===0){showToast('请先连线','err');return;}var seen={};for(var di=0;di2000?fc.substring(0,2000)+'…':fc;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){closeModal();var old=loadedFiles['config/PLC/Reclose_logic.xml']||'';if(old&&old.length>10){WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml.bak',content:old});}var snt=WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml',content:fc});if(snt){showToast('已保存!重启PLC生效','ok');loadedFiles['config/PLC/Reclose_logic.xml']=null;loadedFiles['config/PLC/Reclose_logic.xml.bak']=null;logicGraphs=[];}else{showToast('保存失败','err');}};} - var fileContent = generateXmlConfig(); - var preview = fileContent.length > 2000 ? fileContent.substring(0,2000)+'…' : fileContent; + /* ================================================================ + * 页面生命周期 + * ================================================================ */ + function onEnter(){if(PlcConfigPage._retryList&&PlcConfigPage._retryList.length>0){var rty=PlcConfigPage._retryList.slice();PlcConfigPage._retryList=[];for(var ri=0;ri=0)PlcConfigPage._retryList.splice(rIdx,1);}return;}if(data.type==='file_saved'){if(data.success)showToast('已保存: '+data.path,'ok');else showToast('保存失败: '+(data.error||''),'err');}} - var previewHtml = ''; - - var div = document.createElement('div'); - div.innerHTML = previewHtml; - document.body.appendChild(div.firstElementChild); - - document.getElementById('plc-modal-cancel').onclick = closeModal; - document.getElementById('plc-modal-save').onclick = function() { - closeModal(); - var oldContent = loadedFiles['config/PLC/Reclose_logic.xml'] || ''; - if (oldContent && oldContent.length > 10) { - WsClient.send({ type: 'set_file', path: 'config/PLC/Reclose_logic.xml.bak', content: oldContent }); - } - var sent = WsClient.send({ - type: 'set_file', - path: 'config/PLC/Reclose_logic.xml', - content: fileContent - }); - if (sent) { - showToast('XML 配置已保存!请重启 PLC 模块生效', 'ok'); - loadedFiles['config/PLC/Reclose_logic.xml'] = null; - loadedFiles['config/PLC/Reclose_logic.xml.bak'] = null; - logicGraphs = []; - } else { - showToast('保存失败:WebSocket 未连接', 'err'); - } - }; - } - - /* ========== 页面生命周期 ========== */ - function onEnter() { - // 重试之前因WS未连接而失败的文件请求 - if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) { - var retry = PlcConfigPage._retryList.slice(); - PlcConfigPage._retryList = []; - for (var ri = 0; ri < retry.length; ri++) { - requestFile(retry[ri], PlcConfigPage._pending[retry[ri]] || function(){}); - } - } - // 自动加载 datacenter out 信号 - if (dcOutSignals.length === 0) { - loadDcOutSignals(); - } - } - - function onLeave() { - editWiring = null; - editDragNode = null; - editSelectedNode = -1; - editSelectedLink = -1; - } - - function onData(data) { - if (!data || typeof data !== 'object') return; - - // 处理 file_content 响应 - if (data.type === 'file_content') { - var path = data.path; - var content = data.content || ''; - var error = data.error || ''; - if (error) { - // 文件不存在等情况:仍然触发回调(传null),确保 loadLogicFileAll 的 tryCb 能正常计数 - loadedFiles[path] = null; - if (PlcConfigPage._pending && PlcConfigPage._pending[path]) { - PlcConfigPage._pending[path](null); - delete PlcConfigPage._pending[path]; - } - } else { - loadedFiles[path] = content; - if (PlcConfigPage._pending && PlcConfigPage._pending[path]) { - PlcConfigPage._pending[path](content); - delete PlcConfigPage._pending[path]; - } - } - if (PlcConfigPage._retryList) { - var rIdx = PlcConfigPage._retryList.indexOf(path); - if (rIdx >= 0) PlcConfigPage._retryList.splice(rIdx, 1); - } - return; - } - - // 处理 file_saved 响应 - if (data.type === 'file_saved') { - if (data.success) { - showToast('文件已保存: ' + data.path, 'ok'); - } else { - showToast('保存失败: ' + (data.error || '未知错误'), 'err'); - } - return; - } - } - - return { - render: render, - onEnter: onEnter, - onLeave: onLeave, - onData: onData, - _pending: {} - }; + return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{}}; })(); -Router.register('#plc_config', function(c) { PlcConfigPage.render(c); }); \ No newline at end of file +Router.register('#plc_config', function(c) { PlcConfigPage.render(c); }); diff --git a/config/web/js/pages/plc_debug.js b/config/web/js/pages/plc_debug.js index 3bb9fdb..4ec6731 100644 --- a/config/web/js/pages/plc_debug.js +++ b/config/web/js/pages/plc_debug.js @@ -192,7 +192,13 @@ var PlcDebugPage = (function() { if (!b.saddr) { showPicker(function(sa, de) { b.saddr = sa; b.desc = de; var f = findSig(sa); if (f) b.val = f.val; renderAll(); }); return; } var nv = parseFloat(b.val) > 0 ? 0 : 1; WsClient.send({ curd: 'set', signal_type: 'out', saddr: b.saddr, signal_data: String(nv), setting_zone: '0' }); - b.val = nv; renderAll(); + b.val = nv; + /* 同步写入 SignalStore,确保 refreshData() 不覆盖新值 */ + var outs = SignalStore.get('out'); + for (var i = 0; i < outs.length; i++) { + if (outs[i].saddr === b.saddr) { outs[i].val = String(nv); break; } + } + renderAll(); }, _toggleLed: function(i, v) { ledChecked[i] = v; updateDelBtn(); }, _toggleBtnChk: function(j, v) { btnChecked[j] = v; updateDelBtn(); }, @@ -210,6 +216,7 @@ var PlcDebugPage = (function() { sigChecked = {}; renderAll(); }, _refresh: function() { refreshData(); incrRefresh(); }, + incrRefresh: function() { refreshData(); incrRefresh(); }, _delChecked: delChecked, }; })(); diff --git a/config/web/js/ui.js b/config/web/js/ui.js index 6d21187..1c58a5f 100644 --- a/config/web/js/ui.js +++ b/config/web/js/ui.js @@ -104,41 +104,60 @@ function th(s) { /** * @brief 渲染信号管理页面通用模板 */ -function renderSignalPage(type, headers, extraCols, actionsForRow) { +function renderSignalPage(type, showCheckbox, extraHeader, extraCell) { var signals = SignalStore.get(type); var rows = ''; + var colCount = (showCheckbox ? 1 : 0) + 5 + (extraHeader ? 1 : 0) + 1; if (signals.length === 0) { - rows = '' + I18n.t('signal.no_data') + ''; + rows = '' + I18n.t('signal.no_data') + ''; } else { - signals.forEach(function(s) { - var xtra = extraCols ? extraCols(s) : ''; - var acts = actionsForRow ? actionsForRow(s) : ''; - rows += '' + signals.forEach(function(s, idx) { + var xtra = extraCell ? extraCell(s) : ''; + var acts = (type === 'out' || type === 'yk' || type === 'ao') + ? '' + + '' + + '' + : ''; + rows += '' + + (showCheckbox ? '' : '') + + '' + (idx + 1) + '' + '' + esc(s.saddr) + '' + '' + esc(s.desc || '') + '' - + xtra + + '' + esc(s.type || '?') + '' + '' + fmtVal(s.val) + '' - + '' + acts + '' + + xtra + + '' + acts + '' + ''; }); } + var headerHtml = ''; + if (showCheckbox) headerHtml += ''; + headerHtml += '#'; + headerHtml += '' + I18n.t('signal.saddr') + ''; + headerHtml += '' + I18n.t('signal.desc') + ''; + headerHtml += '' + I18n.t('signal.type') + ''; + headerHtml += '' + I18n.t('signal.val') + ''; + if (extraHeader) headerHtml += '' + extraHeader + ''; + headerHtml += '' + I18n.t('signal.action') + ''; + return '
' - + '
' + type.toUpperCase() + ' \u2014 ' + signals.length + ' signals' + + '
' + type.toUpperCase() + ' — ' + signals.length + ' signals' + '
' + '' + '' + '
' + '
' - + '' + headers.map(function(h) { return th(h); }).join('') + '' - + '' + rows + '' + + '' + headerHtml + '' + + '' + rows + '' + '
' + I18n.t('signal.val') + '' + I18n.t('signal.action') + '
'; } + /* ================================================================== * 信号 CRUD 操作 * ================================================================== */ @@ -147,33 +166,21 @@ var SignalActions = { var body = ''; body += '
'; body += '
'; - - if (type === 'in') - { - body += '
'; - } - else - { - body += '
' - + '
'; - } - - if (type === 'yk' || type === 'ao' || type === 'param') - { + body += '
' + + '
'; + if (type === 'yk' || type === 'ao' || type === 'param') { body += '
' + '
'; } - showModal(I18n.t('signal.add_title') + ' (' + type.toUpperCase() + ')', body, function(ov) { var saddr = getModalVal(ov, '#mf-saddr'); if (!saddr) { toast(I18n.t('toast.saddr_required'), 'error'); return; } var msg = { saddr: saddr, signal_type: type, curd: 'add', signal_data: '', setting_zone: '0' }; - if (type === 'in') { msg.link_saddr = getModalVal(ov, '#mf-link'); } WsClient.send(msg); toast(I18n.t('toast.add_sent') + ': ' + saddr, 'success'); ov.remove(); @@ -186,7 +193,6 @@ var SignalActions = { function(ov) { WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); SignalStore.remove(type, saddr); - Router.dispatch(Router.currentHash); toast(I18n.t('toast.del_sent') + ': ' + saddr, 'info'); ov.remove(); }, Icon.get('trash') + ' ' + I18n.t('signal.delete')); @@ -199,8 +205,12 @@ var SignalActions = { '

' + I18n.t('signal.confirm_batch_msg') + ' ' + checked.length + ' ' + I18n.t('signal.confirm_batch_del') + '?

', function(ov) { checked.forEach(function(cb) { - var saddr = cb.closest('tr').dataset.saddr; - WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); + var tr = cb.closest('tr'); + var saddr = tr ? tr.getAttribute('data-saddr') : ''; + if (saddr) { + WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); + SignalStore.remove(type, saddr); + } }); toast(checked.length + ' ' + I18n.t('toast.batch_del_sent'), 'info'); ov.remove(); @@ -218,3 +228,45 @@ var SignalActions = { }, I18n.t('signal.set')); } }; + +/* ---- Event delegation: data-action buttons ---- */ +document.addEventListener('click', function(e) { + var btn = e.target.closest('[data-action]'); + if (!btn) return; + var action = btn.getAttribute('data-action'); + var saddr = btn.getAttribute('data-saddr'); + var type = btn.getAttribute('data-page-type'); + if (!type) { + var tbody = e.target.closest('tbody[data-type]'); + if (tbody) type = tbody.getAttribute('data-type'); + } + if (!type) return; + if (action === 'set-go') { + e.preventDefault(); + var row = btn.closest('tr'); + var inp = row ? row.querySelector('.sig-set-input') : null; + var val = inp ? inp.value.trim() : ''; + if (!val) { toast(I18n.t('toast.val_required'), 'error'); return; } + WsClient.send({ saddr: saddr, signal_type: type, curd: 'set', signal_data: val, setting_zone: '0' }); + /* 立即更新本地 SignalStore 和 DOM,推送到达前先显示 */ + var arr = SignalStore.get(type); + for (var k = 0; k < arr.length; k++) { if (arr[k].saddr === saddr) { arr[k].val = val; break; } } + /* 直接更新当前行的值单元格 */ + var vc = row.querySelector('.cell-val'); + if (vc) { vc.textContent = val; vc.classList.add('changed'); setTimeout(function(){vc.classList.remove('changed');},600); } + inp.value = ''; + } else if (action === 'del') { + e.preventDefault(); + SignalActions.del(type, saddr); + } +}); +/* Enter key on inline input triggers set */ +document.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && e.target.classList.contains('sig-set-input')) { + e.preventDefault(); + var inp = e.target; + var row = inp.closest('tr'); + var btn = row ? row.querySelector('[data-action="set-go"]') : null; + if (btn) btn.click(); + } +}); diff --git a/config/web/js/ws-client.js b/config/web/js/ws-client.js index f03def8..71ea694 100644 --- a/config/web/js/ws-client.js +++ b/config/web/js/ws-client.js @@ -43,6 +43,10 @@ var WsClient = { var self = WsClient; self._setStatus('disconnected'); self.ws = null; + /* 清空纯内存信号缓存 (后端会话已销毁, 前端不应保留旧数据) */ + if (typeof SignalStore !== 'undefined' && SignalStore.clear) { + SignalStore.clear(); + } self._schedule(); };