diff --git a/config/web/css/style.css b/config/web/css/style.css
index 20556ab..00c71b8 100644
--- a/config/web/css/style.css
+++ b/config/web/css/style.css
@@ -358,7 +358,7 @@ a { color: var(--accent); text-decoration: none; }
/* ============================================================
PLC Debug
============================================================ */
-.plc-debug-grid { display: grid; grid-template-columns: 1fr 1.2fr 1fr; gap: 16px; }
+.plc-debug-grid { display: grid; grid-template-columns: 1fr 1.2fr 1fr; gap: 8px; }
.plc-col { min-width: 0; }
.plc-sig-col { border-left: 1px solid var(--border); border-right: 1px solid var(--border); padding: 0 8px; }
.plc-sig-list { max-height: 65vh; overflow-y: auto; }
@@ -419,7 +419,7 @@ a { color: var(--accent); text-decoration: none; }
.plc-debug-grid {
display: grid;
grid-template-columns: 1fr 1.2fr 1fr;
- gap: 12px;
+ gap: 8px;
height: calc(100vh - 200px);
min-height: 500px;
}
@@ -446,6 +446,14 @@ a { color: var(--accent); text-decoration: none; }
background: var(--bg-surface);
position: sticky; top: 0; z-index: 1;
}
+.plc-block { display: flex; flex-direction: column; flex: 1; min-height: 0; }
+.plc-block-header {
+ padding: 8px 12px; font-size: 12px; font-weight: 700;
+ color: var(--text-primary); border-bottom: 1px solid var(--border);
+ background: var(--bg-surface); display: flex; align-items: center;
+ position: sticky; top: 0; z-index: 1;
+}
+
.plc-empty { text-align: center; color: var(--text-muted); padding: 30px 10px; font-size: 12px; }
.plc-item {
@@ -475,7 +483,7 @@ a { color: var(--accent); text-decoration: none; }
.plc-item-val { margin-left: 4px; font-weight: 600; font-size: 10px; }
/* ---- PLC Signal List ---- */
-.plc-arrow-row { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
+.plc-arrow-row { display: flex; gap: 4px; padding: 4px 8px; }
.plc-sig-list { flex: 1; overflow-y: auto; }
.plc-sig-check-item {
display: flex; align-items: center; gap: 6px;
diff --git a/config/web/js/pages/plc_config.js b/config/web/js/pages/plc_config.js
index ba065fd..da61f04 100644
--- a/config/web/js/pages/plc_config.js
+++ b/config/web/js/pages/plc_config.js
@@ -31,7 +31,7 @@ var PlcConfigPage = (function() {
/* ========== 节点类型常量 ========== */
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_62P=5, NT_62D=6, NT_RISING=7, NT_SR=8, NT_RS=9, NT_FALLING=10, NT_TOGGLE=11, NT_MX_ADD=50, NT_MX_SUB=51, NT_MX_MAX=52, NT_MX_MIN=53, NT_MX_MUL=54, NT_MX_DIV=55, NT_MX_ABS=56, NT_MX_CMP=57;
var NODE_INFO = {
0: { name:'输出点', icon:'📤', color:'#4CAF50', inPins:1, outPins:0, inMax:1, outMax:0 },
@@ -44,7 +44,16 @@ var PlcConfigPage = (function() {
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 }
+ 10: { name:'下降沿', icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1, outMax:10 },
+ 11: { name:'翻转元件', icon:'🔄', color:'#CDDC39', inPins:1, outPins:1, inMax:1, outMax:10 },
+ 50: { name:'加法元件', icon:'➕', color:'#E040FB', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 51: { name:'减法元件', icon:'➖', color:'#00E5FF', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 52: { name:'最大值元件', icon:'⬆️', color:'#FFD600', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 53: { name:'最小值元件', icon:'⬇️', color:'#76FF03', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 54: { name:'乘法元件', icon:'✖️', color:'#FF6D00', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 55: { name:'除法元件', icon:'➗', color:'#D50000', inPins:2, outPins:1, inMax:2, outMax:10 },
+ 56: { name:'绝对值元件', icon:'📏', color:'#00C853', inPins:1, outPins:1, inMax:1, outMax:10 },
+ 57: { name:'比较元件', icon:'⚖️', color:'#FF1744', inPins:2, outPins:1, inMax:2, outMax:10 }
};
/* ========== 全局状态 ========== */
@@ -121,7 +130,7 @@ var PlcConfigPage = (function() {
* 信号面板
* ================================================================ */
function renderSigPanel() {
- return '
'
+ return '
'
+ ''
+ ' '
@@ -129,6 +138,13 @@ var PlcConfigPage = (function() {
+ '
';
}
+ function renderOpPanel() {
+ var ops=[{t:NT_AND,n:'与门',i:'🔷',c:'#9C27B0'},{t:NT_OR,n:'或门',i:'🔶',c:'#FF9800'},{t:NT_NOT,n:'非门',i:'🔺',c:'#f44336'},{t:NT_62P,n:'62P',i:'⏱️',c:'#00BCD4'},{t:NT_62D,n:'62D',i:'⏱️',c:'#009688'},{t:NT_RISING,n:'上升沿',i:'⬆️',c:'#E91E63'},{t:NT_FALLING,n:'下降沿',i:'⬇️',c:'#FF5722'},{t:NT_TOGGLE,n:'翻转',i:'🔄',c:'#CDDC39'},{t:NT_SR,n:'SR',i:'🔒',c:'#795548'},{t:NT_RS,n:'RS',i:'🔒',c:'#607D8B'},{t:NT_MX_ADD,n:'加法',i:'➕',c:'#E040FB'},{t:NT_MX_SUB,n:'减法',i:'➖',c:'#00E5FF'},{t:NT_MX_MAX,n:'最大值',i:'⬆️',c:'#FFD600'},{t:NT_MX_MIN,n:'最小值',i:'⬇️',c:'#76FF03'},{t:NT_MX_MUL,n:'乘法',i:'✖️',c:'#FF6D00'},{t:NT_MX_DIV,n:'除法',i:'➗',c:'#D50000'},{t:NT_MX_ABS,n:'绝对值',i:'📏',c:'#00C853'},{t:NT_MX_CMP,n:'比较',i:'⚖️',c:'#FF1744'}];
+ var h='
';
+ for(var oi=0;oi'+op.i+' '+op.n+'
';}
+ h+='
';
+ return h;
+ }
function loadDcOutSignals(cb) {
requestFile('config/SYSTEM/datacenter_out.xml', function(content) {
if(!content){showToast('加载 datacenter_out.xml 失败','err');return;}
@@ -148,9 +164,9 @@ var PlcConfigPage = (function() {
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)+' ';}
+ for(var i=0;i#'+s.no+' '+escHtml(s.saddr)+''+escHtml(s.desc)+' ';if(mode==='edit'){h+='入 出 ';}h+='';}
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');};});
+ [].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){});
}
/* ================================================================
@@ -202,6 +218,7 @@ var PlcConfigPage = (function() {
editWiring=null;editDragNode=null;editPanX=0;editPanY=0;editScale=1.0;editNextGateIdx={};
main.innerHTML = renderSigPanel()
+ + renderOpPanel()
+ '';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();};});}
+ 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_TOGGLE,n:'翻转元件',d:'输入变化翻转',i:'🔄'},{t:NT_SR,n:'SR触发器',d:'S优先',i:'🔒'},{t:NT_RS,n:'RS触发器',d:'R优先',i:'🔒'},{t:NT_MX_ADD,n:'加法元件',d:'两输入求和',i:'➕'},{t:NT_MX_SUB,n:'减法元件',d:'两输入差绝对值',i:'➖'},{t:NT_MX_MAX,n:'最大值元件',d:'两输入取大',i:'⬆️'},{t:NT_MX_MIN,n:'最小值元件',d:'两输入取小',i:'⬇️'},{t:NT_MX_MUL,n:'乘法元件',d:'两输入乘积',i:'✖️'},{t:NT_MX_DIV,n:'除法元件',d:'两输入之商(除0出0)',i:'➗'},{t:NT_MX_ABS,n:'绝对值元件',d:'单输入取绝对值',i:'📏'},{t:NT_MX_CMP,n:'比较元件',d:'A>B出1,否则出0',i:'⚖️'}];var h='🧮 选择运算模块 ';for(var i=0;i'+g.i+' '+g.n+' '+g.d+'
';}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();};});}
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;liS(置位) : '+(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 showMXInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},inPins=getNodeInPins(nd);var aSrc='',bSrc='';for(var li=0;li⚠️ 顺序敏感: A - B';else if(nd.type===NT_MX_DIV)orderNote='⚠️ 顺序敏感: A ÷ B (B=0→结果为0)
';else if(nd.type===NT_MX_CMP)orderNote='⚠️ 顺序敏感: A > B → 1,否则 → 0
';else orderNote='💡 输入可交换,顺序不影响结果
';var h=''+info.icon+' '+info.name+' — 连接 A : '+(aSrc||'未连接 ')+'
B : '+(bSrc||'未连接 ')+'
'+orderNote+'
关闭 ';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();}}}
+ function drawGraphContent(ctx,nodes,links,selectedIdx,selectedLinkIdx,wiring){var IW=250,IH=80,GW=170,GH=66,CR=12,SO=3,LBW=8;for(var i=0;i=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:inP);for(var p=0;p=50&&n.type<=57);if(isMX){ctx.fillStyle='#fff';ctx.font='bold 10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';ctx.fillText(p===0?'A':'B',x-hw-12,py);}}}if(info.outPins>0){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();}}}
/* ================================================================
* 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)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 '; }
+ function sx(n, pin) { var s = '= 0) s += ' pin="' + pin + '"'; s += ' />'; return s; }
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) + '"';
@@ -500,7 +533,20 @@ var PlcConfigPage = (function() {
function onLeave(){editWiring=null;editDragNode=null;editSelectedNode=-1;editSelectedLink=-1;}
function onData(data){if(!data||typeof data!=='object')return;if(data.type==='file_content'){var p=data.path,c=data.content||'',e=data.error||'';loadedFiles[p]=e?null:c;if(PlcConfigPage._pending&&PlcConfigPage._pending[p]){PlcConfigPage._pending[p](e?null:c);delete PlcConfigPage._pending[p];}if(PlcConfigPage._retryList){var rIdx=PlcConfigPage._retryList.indexOf(p);if(rIdx>=0)PlcConfigPage._retryList.splice(rIdx,1);}return;}if(data.type==='file_saved'){if(data.success)showToast('已保存: '+data.path,'ok');else showToast('保存失败: '+(data.error||''),'err');}}
- return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{}};
+ return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{},
+ _addSigIO:function(no,saddr,desc,ioType){
+ if(mode!=='edit')return;
+ var type=ioType===1?NT_INPUT:NT_OUTPUT;
+ editNodes.push({type:type,id:no,x:0,y:0,_lx:type===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===type;}).length*110,saddr:saddr,desc:desc||'',no:no});
+ drawEditor();showToast('已添加'+(type===NT_INPUT?'输入':'输出')+': '+saddr,'ok');
+ },
+ _addOp:function(gt){
+ if(mode!=='edit')return;
+ var info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;
+ if(gt===NT_62P||gt===NT_62D){addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();return;}
+ addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();
+ }
+ };
})();
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 4ec6731..f79ea7c 100644
--- a/config/web/js/pages/plc_debug.js
+++ b/config/web/js/pages/plc_debug.js
@@ -1,70 +1,58 @@
/**
* @file plc_debug.js
- * @brief PLC 调试页 — 指示灯/按键绑定 out 信号 + 增量刷新
+ * @brief PLC 调试页 — 三列布局:左(遥信输入·按钮/遥测输入·设值) | 中(信号表) | 右(遥信输出·LED/遥测输出·展示)
+ * @details 所有条目从中间信号表选择添加,增量刷新
*/
-var PlcDebugLeds = [];
-var PlcDebugButtons = [];
+var PlcDebugStIn = []; // 遥信输入(按钮)
+var PlcDebugMxIn = []; // 遥测输入(设值)
+var PlcDebugStOut = []; // 遥信输出(LED)
+var PlcDebugMxOut = []; // 遥测输出(展示)
Router.register('#plc_debug', function(c) { PlcDebugPage.render(c); });
var PlcDebugPage = (function() {
var outSignals = [];
- var ledChecked = {}, btnChecked = {}, sigChecked = {};
+ var stInChk = {}, mxInChk = {}, stOutChk = {}, mxOutChk = {}, sigChk = {};
function getOut() {
var out = SignalStore.get('out');
return (Array.isArray(out) && out.length) ? out : outSignals;
}
- function showPicker(cb) {
- var sigs = getOut();
- if (!sigs.length) { toast(I18n.t('plc_debug.no_sig'), 'error'); return; }
- var rows = '';
- sigs.forEach(function(s) {
- rows += ''
- + '' + esc(s.saddr) + ' '
- + (s.desc ? ' ' + esc(s.desc) + ' ' : '')
- + '' + fmtVal(s.val) + '
';
- });
- var ov = document.createElement('div');
- ov.className = 'modal-overlay';
- ov.innerHTML = '' + I18n.t('plc_debug.bind_title') + ' '
- + '
' + rows + '
'
- + '
' + I18n.t('toast.cancel') + ' ';
- document.body.appendChild(ov);
- document.getElementById('plc-pick-cancel').onclick = function() { ov.remove(); };
- ov.addEventListener('click', function(e) { if (e.target === ov) ov.remove(); });
- ov.querySelectorAll('.plc-pick-row').forEach(function(r) {
- r.onclick = function() {
- var sa = this.getAttribute('data-saddr'), de = this.getAttribute('data-desc');
- ov.remove();
- if (confirm(I18n.t('plc_debug.confirm_bind') + ' ' + sa + ' ?')) cb(sa, de);
- };
- });
- }
-
function findSig(sa) {
- for (var i = 0; i < outSignals.length; i++) if (outSignals[i].saddr === sa) return outSignals[i];
+ for (var i = 0; i < outSignals.length; i++)
+ if (outSignals[i].saddr === sa) return outSignals[i];
return null;
}
function refreshData() {
var o = getOut(); if (Array.isArray(o)) outSignals = o;
- PlcDebugLeds.forEach(function(l) { var f = findSig(l.saddr); l.val = f ? f.val : 0; });
- PlcDebugButtons.forEach(function(b) { var f = findSig(b.saddr); b.val = f ? f.val : 0; });
+ PlcDebugStIn.forEach(function(l) { var f = findSig(l.saddr); l.val = f ? f.val : 0; });
+ PlcDebugMxIn.forEach(function(m) { var f = findSig(m.saddr); m.val = f ? f.val : 0; });
+ PlcDebugStOut.forEach(function(b) { var f = findSig(b.saddr); b.val = f ? f.val : 0; });
+ PlcDebugMxOut.forEach(function(m) { var f = findSig(m.saddr); m.val = f ? f.val : 0; });
}
function incrRefresh() {
refreshData();
- PlcDebugLeds.forEach(function(l, i) {
+ PlcDebugStIn.forEach(function(b, i) {
+ var on = parseFloat(b.val) > 0;
+ var ve = document.getElementById('sti-val-' + i);
+ if (ve) { ve.textContent = '(' + fmtVal(b.val) + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; }
+ });
+ PlcDebugMxIn.forEach(function(m, i) {
+ var ve = document.getElementById('mxi-cur-' + i);
+ if (ve) ve.textContent = '(' + fmtVal(m.val) + ')';
+ });
+ PlcDebugStOut.forEach(function(l, i) {
var on = parseFloat(l.val) > 0;
- var ve = document.getElementById('led-val-' + i), ie = document.getElementById('led-icon-' + i);
- if (ve) { ve.textContent = '(' + l.val + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; }
+ var ve = document.getElementById('sto-val-' + i), ie = document.getElementById('sto-icon-' + i);
+ if (ve) { ve.textContent = '(' + fmtVal(l.val) + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; }
if (ie) ie.className = 'plc-led' + (on ? ' plc-led-on' : '');
});
- PlcDebugButtons.forEach(function(b, j) {
- var ve = document.getElementById('btn-val-' + j);
- if (ve) { ve.textContent = '(' + b.val + ')'; ve.style.color = parseFloat(b.val) > 0 ? 'var(--accent)' : 'var(--text-muted)'; }
+ PlcDebugMxOut.forEach(function(m, i) {
+ var ve = document.getElementById('mxo-val-' + i);
+ if (ve) ve.textContent = fmtVal(m.val);
});
}
@@ -72,111 +60,168 @@ var PlcDebugPage = (function() {
function delChecked() {
var todel = [];
- for (var k in ledChecked) if (ledChecked.hasOwnProperty(k)) todel.push({ type: 'led', idx: parseInt(k) });
- for (var k in btnChecked) if (btnChecked.hasOwnProperty(k)) todel.push({ type: 'btn', idx: parseInt(k) });
+ for (var k in stInChk) if (stInChk.hasOwnProperty(k)) todel.push({ t: 'sti', idx: parseInt(k) });
+ for (var k in mxInChk) if (mxInChk.hasOwnProperty(k)) todel.push({ t: 'mxi', idx: parseInt(k) });
+ for (var k in stOutChk) if (stOutChk.hasOwnProperty(k)) todel.push({ t: 'sto', idx: parseInt(k) });
+ for (var k in mxOutChk) if (mxOutChk.hasOwnProperty(k)) todel.push({ t: 'mxo', idx: parseInt(k) });
if (!todel.length) { toast(I18n.t('toast.no_selected'), 'error'); return; }
if (!confirm(I18n.t('plc_debug.confirm_del') + ' ' + todel.length + ' ?')) return;
todel.sort(function(a, b) { return b.idx - a.idx; }).forEach(function(t) {
- if (t.type === 'led') PlcDebugLeds.splice(t.idx, 1);
- else PlcDebugButtons.splice(t.idx, 1);
+ if (t.t === 'sti') PlcDebugStIn.splice(t.idx, 1);
+ else if (t.t === 'mxi') PlcDebugMxIn.splice(t.idx, 1);
+ else if (t.t === 'sto') PlcDebugStOut.splice(t.idx, 1);
+ else PlcDebugMxOut.splice(t.idx, 1);
});
- ledChecked = {}; btnChecked = {};
+ stInChk = {}; mxInChk = {}; stOutChk = {}; mxOutChk = {};
renderAll();
}
+ function makeBlock(title, icon, arr, chkObj, idPrefix, blockType) {
+ var html = '';
+ if (!arr.length) {
+ html = '暂无
';
+ } else {
+ arr.forEach(function(item, i) {
+ var on = parseFloat(item.val) > 0, chk = !!chkObj[i];
+ html += '';
+ html += '
';
+
+ if (blockType === 'btn') {
+ html += '
🔘 ';
+ } else if (blockType === 'led') {
+ html += '
';
+ }
+
+ html += '
';
+ if (item.saddr) {
+ html += '' + esc(item.saddr) + ' ';
+ if (item.desc) html += '' + esc(item.desc) + ' ';
+ } else {
+ html += '未绑定 ';
+ }
+ html += '
';
+
+ if (blockType === 'btn' || blockType === 'led') {
+ html += '
(' + fmtVal(item.val) + ') ';
+ } else if (blockType === 'mxi') {
+ html += '
(' + fmtVal(item.val) + ') ';
+ html += '
';
+ html += '
设置 ';
+ } else if (blockType === 'mxo') {
+ html += '
' + fmtVal(item.val) + ' ';
+ }
+ html += '
';
+ });
+ }
+ return '';
+ }
+
function renderAll() {
var c = document.getElementById('plc-content');
if (!c) return;
refreshData();
- /* 指示灯列 */
- var ledHtml = '';
- if (!PlcDebugLeds.length) {
- ledHtml = '' + I18n.t('plc_debug.no_led') + '
';
- } else {
- PlcDebugLeds.forEach(function(l, i) {
- var on = parseFloat(l.val) > 0, chk = !!ledChecked[i];
- ledHtml += ''
- + '
'
- + '
'
- + (l.saddr ? '' + esc(l.saddr) + ' (' + fmtVal(l.val) + ') '
- + (l.desc ? '' + esc(l.desc) + ' ' : '')
- : '' + I18n.t('plc_debug.unbound') + ' ')
- + '
'
- + '
'
- + '
';
- });
- }
-
- /* 按键列 */
- var btnHtml = '';
- if (!PlcDebugButtons.length) {
- btnHtml = '' + I18n.t('plc_debug.no_btn') + '
';
- } else {
- PlcDebugButtons.forEach(function(b, j) {
- var chk = !!btnChecked[j];
- btnHtml += ''
- + '
🔘 '
- + '
'
- + (b.saddr ? '' + esc(b.saddr) + ' (' + fmtVal(b.val) + ') '
- + (b.desc ? '' + esc(b.desc) + ' ' : '')
- : '' + I18n.t('plc_debug.unbound') + ' ')
- + '
'
- + '
'
- + '
';
- });
- }
-
- /* 信号列 */
+ /* 信号表 (中间列) */
var o = getOut();
var sigHtml = '';
if (!o.length) {
- sigHtml = '' + I18n.t('plc_debug.no_sig') + '
';
+ sigHtml = '' + I18n.t('plc_debug.no_sig') + '
';
} else {
o.forEach(function(s, i) {
- var chk = !!sigChecked[i];
- sigHtml += ''
- + '
'
- + '
' + esc(s.saddr) + ' '
- + '
' + esc(s.desc || '') + ' '
- + '
' + fmtVal(s.val) + ' '
+ var chk = !!sigChk[i];
+ sigHtml += '
'
+ + ' '
+ + '#' + i + ' '
+ + '' + esc(s.saddr) + ' '
+ + (s.desc ? '' + esc(s.desc) + ' ' : '')
+ + '' + fmtVal(s.val) + ' '
+ '
';
});
}
c.innerHTML = '
'
- + '
'
- + '
'
- + '
'
+ /* 左列: 遥信输入(上) + 遥测输入(下) */
+ + '
'
+ + makeBlock('遥信输入', '📤', PlcDebugStIn, stInChk, 'sti', 'btn')
+ + makeBlock('遥测输入', '📊', PlcDebugMxIn, mxInChk, 'mxi', 'mxi')
+ + '
'
+ /* 中列: 信号表 */
+ + '
'
+ + '
'
+ + '← 遥信输入 '
+ + '遥信输出 → '
+ + '
'
+ + '
'
+ + '← 遥测输入 '
+ + '遥测输出 → '
+ + '
'
+ + '
' + sigHtml + '
'
+ /* 右列: 遥信输出(上) + 遥测输出(下) */
+ + '
'
+ + makeBlock('遥信输出', '📥', PlcDebugStOut, stOutChk, 'sto', 'led')
+ + makeBlock('遥测输出', '📊', PlcDebugMxOut, mxOutChk, 'mxo', 'mxo')
+ + '
'
+ '
';
+ /* 绑定事件 */
+ [].forEach.call(c.querySelectorAll('.plc-btn-icon'), function(el) {
+ el.onclick = function() {
+ var m = this.id.match(/^sti-icon-(\d+)$/);
+ if (m) PlcDebugPage._toggleStIn(parseInt(m[1]));
+ };
+ });
+ [].forEach.call(c.querySelectorAll('.plc-led'), function(el) {
+ el.onclick = function() {
+ var m = this.id.match(/^sto-icon-(\d+)$/);
+ if (m) PlcDebugPage._bindStOut(parseInt(m[1]));
+ };
+ });
+ [].forEach.call(c.querySelectorAll('[id$="-setbtn-"]'), function(el) {
+ el.onclick = function() {
+ var m = this.id.match(/^mxi-setbtn-(\d+)$/);
+ if (m) PlcDebugPage._setMxIn(parseInt(m[1]));
+ };
+ });
+ [].forEach.call(c.querySelectorAll('.plc-sig-check-item'), function(row) {
+ row.onclick = function(e) {
+ if (e.target.tagName === 'INPUT') return;
+ var cb = this.querySelector('input[type="checkbox"]');
+ if (cb) {
+ cb.checked = !cb.checked;
+ var si = parseInt(cb.getAttribute('data-sig-idx'));
+ if (!isNaN(si)) { if (cb.checked) sigChk[si] = true; else delete sigChk[si]; }
+ }
+ };
+ });
+ [].forEach.call(c.querySelectorAll('.plc-sig-check-item input[type="checkbox"]'), function(cb) {
+ cb.onclick = function(e) {
+ e.stopPropagation();
+ var si = parseInt(this.getAttribute('data-sig-idx'));
+ if (!isNaN(si)) { if (this.checked) sigChk[si] = true; else delete sigChk[si]; }
+ };
+ });
+
updateDelBtn();
}
function updateDelBtn() {
var el = document.getElementById('plc-del-btn');
if (!el) return;
- var n = countChecked(ledChecked) + countChecked(btnChecked);
+ var n = countChecked(stInChk) + countChecked(mxInChk) + countChecked(stOutChk) + countChecked(mxOutChk);
el.textContent = I18n.t('plc_debug.del_sel') + (n ? ' (' + n + ')' : '');
el.disabled = !n;
}
return {
render: function(c) {
- ledChecked = {}; btnChecked = {}; sigChecked = {};
+ stInChk = {}; mxInChk = {}; stOutChk = {}; mxOutChk = {}; sigChk = {};
refreshData();
c.innerHTML = '
'
- + '