'
- + '';
});
-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;i
0) 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;
- }
+ + ''
+ + '';
+ 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 ''
+ + ''
+ + '
'
+ + '
'
+ + '
';
}
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)+'';
+ [].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 = ''
- + '
';
- 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;i