/** * pages.js — 页面渲染器 * * 每个页面对象实现: * render(container) — 渲染 HTML 到容器 * onEnter() — 页面激活时调用 * onLeave() — 页面离开时调用 * onData(signalType, arr) — 接收下行数据更新表格 * * 术语映射: * out → 注册 | in → 链接注册 | yk → 遥控 | ao → 参数 | param → 定值 */ var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' }; /* ======================================================================== * 共用工具 * ======================================================================== */ function renderTable(containerId, headers, rowsHtml, emptyText) { var html = '
'; html += ''; for (var i = 0; i < headers.length; i++) { html += ''; } html += ''; if (rowsHtml) { html += rowsHtml; } else { html += ''; } html += '
' + headers[i] + '
' + (emptyText || '暂无数据,请先添加信号') + '
'; return html; } function fmtVal(v) { if (v === null || v === undefined || v === '') return '-'; return String(v).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* ------------------------------------------------------------------------ * XML 工具函数(导入导出) * ------------------------------------------------------------------------ */ function escapeXml(str) { if (typeof str !== 'string') return String(str); return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } /** * 导出所有5种信号类型(out/in/yk/ao/param)到一个XML文件 */ function exportAllToXml() { var types = ['out', 'in', 'yk', 'ao', 'param']; var typeLabels = { out:'注册', in:'链接注册', yk:'遥控', ao:'参数', param:'定值' }; var hasData = false; var xml = '\n\n'; for (var t = 0; t < types.length; t++) { var st = types[t]; var data = null; if (typeof App !== 'undefined' && App.getSignalData) { data = App.getSignalData(st); } if (!data || !Array.isArray(data) || data.length === 0) { xml += ' \n'; continue; } hasData = true; xml += ' \n'; for (var i = 0; i < data.length; i++) { xml += ' \n'; } xml += ' \n'; } xml += '\n'; if (!hasData) { alert('当前没有任何信号数据,无法导出'); return; } var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'signals_all_' + Date.now() + '.xml'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * 从 XML 文件导入所有5种信号类型,按 type 分组批量执行 add */ function importAllFromXml() { var input = document.createElement('input'); input.type = 'file'; input.accept = '.xml'; input.style.display = 'none'; document.body.appendChild(input); input.onchange = function(e) { var file = e.target.files[0]; if (!file) { document.body.removeChild(input); return; } var reader = new FileReader(); reader.onload = function(ev) { document.body.removeChild(input); try { var parser = new DOMParser(); var xmlDoc = parser.parseFromString(ev.target.result, 'text/xml'); var parseError = xmlDoc.querySelector('parsererror'); if (parseError) { alert('XML 解析失败:' + parseError.textContent); return; } var typeNodes = xmlDoc.querySelectorAll('signals'); if (typeNodes.length === 0) { alert('XML 文件中没有 元素'); return; } var validTypes = { out: '注册', in: '链接注册', yk: '遥控', ao: '参数', param: '定值' }; var totalCount = 0; var typeCounts = {}; for (var ti = 0; ti < typeNodes.length; ti++) { var signalsEl = typeNodes[ti]; var fileType = signalsEl.getAttribute('type'); if (!fileType || !validTypes[fileType]) continue; var signals = []; var children = signalsEl.children; for (var si = 0; si < children.length; si++) { if (children[si].tagName !== 'signal') continue; var saddr = children[si].getAttribute('saddr'); if (!saddr || !saddr.trim()) continue; var noAttr = children[si].getAttribute('no'); signals.push({ no: noAttr !== null ? parseInt(noAttr) : si, saddr: saddr.trim() }); } if (signals.length > 0) { typeCounts[fileType] = signals.length; totalCount += signals.length; } } if (totalCount === 0) { alert('XML 文件中没有有效的信号数据'); return; } var msg = '即将导入以下信号:\n'; for (var t in typeCounts) { msg += ' ' + validTypes[t] + ' (' + t + '): ' + typeCounts[t] + ' 个\n'; } msg += '\n共 ' + totalCount + ' 个信号,是否继续?'; if (!confirm(msg)) { return; } for (var ti2 = 0; ti2 < typeNodes.length; ti2++) { var signalsEl2 = typeNodes[ti2]; var fileType2 = signalsEl2.getAttribute('type'); if (!fileType2 || !validTypes[fileType2]) continue; var children2 = signalsEl2.children; for (var si2 = 0; si2 < children2.length; si2++) { if (children2[si2].tagName !== 'signal') continue; var saddr2 = children2[si2].getAttribute('saddr'); if (!saddr2 || !saddr2.trim()) continue; WsClient.send({ saddr: saddr2.trim(), signal_type: fileType2, curd: 'add', setting_zone: '0', signal_data: '' }); } } alert('导入完成,共发送 ' + totalCount + ' 个 add 请求'); } catch (ex) { alert('导入失败:' + ex.message); } }; reader.readAsText(file); }; input.click(); } /* ======================================================================== * 共用工具栏 HTML * ======================================================================== */ function makeToolbar(signalType, showSet) { var typeLabel = signalType === 'ao' ? '参数' : (signalType === 'param' ? '定值' : signalType); return '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '' + '
'; } function bindToolbar(signalType, getDataFn) { var execBtn = document.getElementById('tb-exec'); var clearBtn = document.getElementById('tb-clear'); var saddrInput = document.getElementById('tb-saddr'); var curdSelect = document.getElementById('tb-curd'); if (!execBtn || !clearBtn) return; execBtn.onclick = function() { var saddr = saddrInput.value.trim(); var curd = curdSelect.value; if (!saddr) { alert('请输入短地址 saddr'); return; } if (!WsClient.send({ saddr: saddr, signal_type: signalType, curd: curd, setting_zone: '0', signal_data: '' })) { alert('WebSocket 未连接'); } }; clearBtn.onclick = function() { saddrInput.value = ''; }; } /* ======================================================================== * 1. LoginPage — 登录页 * ======================================================================== */ var LoginPage = { render: function(container) { container.innerHTML = '' + '
' + '

RTU 远程终端监控系统

' + '' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '' + '
'; document.getElementById('login-btn').onclick = function() { localStorage.setItem('rtu_logged_in', '1'); window.location.hash = '#dashboard'; }; }, onEnter: function() { document.body.classList.add('login-page'); }, onLeave: function() { document.body.classList.remove('login-page'); } }; /* ======================================================================== * 2. DashboardPage — 功能入口主页 * ======================================================================== */ var DashboardPage = { render: function(container) { var cards = [ { cls: 'out', icon: '📡', title: '注册', desc: 'out — 状态量信号注册与修改', hash: '#out' }, { cls: 'in', icon: '📊', title: '链接注册', desc: 'in — 测量量信号链接注册与监视', hash: '#in' }, { cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' }, { cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' }, { cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' }, { cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' }, { cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' } ]; var html = ''; html += '
'; for (var i = 0; i < cards.length; i++) { var c = cards[i]; html += '' + '
' + c.icon + '
' + '
' + c.title + '
' + '
' + c.desc + '
' + '
'; } html += '
'; html += '
' + '

📦 批量操作

' + '

导入/导出所有信号类型 (out/in/yk/ao/param) 到一个 XML 文件

' + '' + '' + '
'; container.innerHTML = html; document.getElementById('btn-export-all').onclick = exportAllToXml; document.getElementById('btn-import-all').onclick = importAllFromXml; }, onEnter: function() {}, onLeave: function() {} }; /* ======================================================================== * 3. OutPage — 注册管理 (out) * ======================================================================== */ var OutPage = (function() { var data = []; function renderTableHtml() { if (data.length === 0) return ''; var rows = ''; for (var i = 0; i < data.length; i++) { var item = data[i]; rows += '' + '' + (i + 1) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + fmtVal(item.val) + '' + '' + '' + ''; } return rows; } function bindModifyButtons() { var buttons = document.querySelectorAll('#out-tbody .btn-info'); for (var k = 0; k < buttons.length; k++) { buttons[k].onclick = function() { var idx = parseInt(this.getAttribute('data-idx')); var input = document.getElementById('mod-' + idx); var val = input.value.trim(); if (val === '') { alert('请输入修改值'); return; } var item = data[idx]; WsClient.send({ curd:'set', signal_type:'out', saddr:item.saddr, signal_data:val, setting_zone:'0' }); }; } } return { render: function(container) { var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改']; container.innerHTML = '' + '' + makeToolbar('out') + renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号'); bindToolbar('out', function() { return data; }); bindModifyButtons(); }, onEnter: function() {}, onLeave: function() { data = []; }, onData: function(arr) { data = arr || []; var tbody = document.getElementById('out-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无注册数据,请先添加信号'; return; } tbody.innerHTML = renderTableHtml(); bindModifyButtons(); } }; })(); /* ======================================================================== * 4. InPage — 链接注册管理 (in) * ======================================================================== */ var InPage = (function() { var data = []; function renderTableHtml() { if (data.length === 0) return ''; var rows = ''; for (var i = 0; i < data.length; i++) { var item = data[i]; rows += '' + '' + (i + 1) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + fmtVal(item.val) + '' + ''; } return rows; } return { render: function(container) { var headers = ['#', 'saddr', 'desc', '数据类型', 'val']; container.innerHTML = '' + '' + makeToolbar('in') + renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号'); bindToolbar('in', function() { return data; }); }, onEnter: function() {}, onLeave: function() { data = []; }, onData: function(arr) { data = arr || []; var tbody = document.getElementById('in-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无链接注册数据,请先添加信号'; return; } tbody.innerHTML = renderTableHtml(); } }; })(); /* ======================================================================== * 5. YkPage — 遥控管理 (yk) * ======================================================================== */ var YkPage = (function() { var data = []; function renderTableHtml() { if (data.length === 0) return ''; var rows = ''; for (var i = 0; i < data.length; i++) { var item = data[i]; var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type; rows += '' + '' + (i + 1) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + fmtVal(item.val) + '' + '' + ctrlLabel + '' + '' + '' + ''; } return rows; } function bindButtons() { var buttons = document.querySelectorAll('#yk-tbody .btn-sm'); for (var k = 0; k < buttons.length; k++) { buttons[k].onclick = function() { var idx = parseInt(this.getAttribute('data-idx')); var val = this.getAttribute('data-val'); var item = data[idx]; WsClient.send({ curd: 'set', signal_type: 'yk', saddr: item.saddr, signal_data: val, setting_zone: '0' }); }; } } return { render: function(container) { var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', '控合', '控分']; container.innerHTML = '' + '' + makeToolbar('yk') + renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号'); bindToolbar('yk', function() { return data; }); bindButtons(); }, onEnter: function() {}, onLeave: function() { data = []; }, onData: function(arr) { data = arr || []; var tbody = document.getElementById('yk-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无遥控数据,请先添加信号'; return; } tbody.innerHTML = renderTableHtml(); bindButtons(); } }; })(); /* ======================================================================== * 6. AoPage — 参数管理 (ao, 单值修改) * ======================================================================== */ var AoPage = (function() { var data = []; function renderTableHtml() { if (data.length === 0) return ''; var rows = ''; for (var i = 0; i < data.length; i++) { var item = data[i]; var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type; rows += '' + '' + (i + 1) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + fmtVal(item.val) + '' + '' + ctrlLabel + '' + '' + fmtVal(item.min) + '' + '' + fmtVal(item.max) + '' + '' + fmtVal(item.step) + '' + '' + fmtVal(item.unit) + '' + '' + '' + ''; } return rows; } function bindModifyButtons() { var buttons = document.querySelectorAll('#ao-tbody .btn-info'); for (var k = 0; k < buttons.length; k++) { buttons[k].onclick = function() { var idx = parseInt(this.getAttribute('data-idx')); var input = document.getElementById('ao-mod-' + idx); var val = input.value.trim(); if (val === '') { alert('请输入修改值'); return; } var item = data[idx]; WsClient.send({ curd: 'set', signal_type: 'ao', saddr: item.saddr, signal_data: val, setting_zone: '0' }); }; } } return { render: function(container) { var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改']; container.innerHTML = '' + '' + makeToolbar('ao') + renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号'); bindToolbar('ao', function() { return data; }); bindModifyButtons(); }, onEnter: function() {}, onLeave: function() { data = []; }, onData: function(arr) { data = arr || []; var tbody = document.getElementById('ao-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无参数数据,请先添加信号'; return; } tbody.innerHTML = renderTableHtml(); bindModifyButtons(); } }; })(); /* ======================================================================== * 7. ParamPage — 定值管理 (param, 多定值区展开) * ======================================================================== */ var ParamPage = (function() { var data = []; var expandedRows = {}; // 记录展开状态 function renderTableHtml() { if (data.length === 0) return ''; var rows = ''; for (var i = 0; i < data.length; i++) { var item = data[i]; var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type; var zones = item.setting_zone_list || []; var zoneCount = zones.length; var isOpen = expandedRows[i] === true; var icon = isOpen ? '▼' : '▶'; // 主行(可点击展开) rows += '' + '' + (i + 1) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + ctrlLabel + '' + '' + zoneCount + '' + '' + icon + ' 展开' + ''; // 展开的定值区子表 if (isOpen) { rows += '' + '' + '' + '' + '' + '' + ''; for (var j = 0; j < zones.length; j++) { var zone = zones[j]; rows += '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; } rows += '
定值区 IDvaldefault_valminmaxstepunit修改值修改
' + fmtVal(zone.id) + '' + fmtVal(zone.val) + '' + fmtVal(zone.default_val) + '' + fmtVal(item.min) + '' + fmtVal(item.max) + '' + fmtVal(item.step) + '' + fmtVal(item.unit) + '
'; } } return rows; } function bindEvents() { // 展开/收起 var expandRows = document.querySelectorAll('#param-tbody .expand-row'); for (var k = 0; k < expandRows.length; k++) { expandRows[k].onclick = function() { var idx = parseInt(this.getAttribute('data-pidx')); expandedRows[idx] = !expandedRows[idx]; refreshTable(); }; } // 修改按钮 var modButtons = document.querySelectorAll('#param-tbody .btn-info'); for (var m = 0; m < modButtons.length; m++) { modButtons[m].onclick = function(e) { e.stopPropagation(); var pidx = parseInt(this.getAttribute('data-pidx')); var zidx = parseInt(this.getAttribute('data-zidx')); var input = document.getElementById('pm-' + pidx + '-' + zidx); var val = input.value.trim(); if (val === '') { alert('请输入修改值'); return; } var item = data[pidx]; var zoneId = item.setting_zone_list[zidx].id; WsClient.send({ curd: 'set', signal_type: 'param', saddr: item.saddr, signal_data: val, setting_zone: zoneId }); }; } } function refreshTable() { var tbody = document.getElementById('param-tbody'); if (!tbody) return; tbody.innerHTML = renderTableHtml(); bindEvents(); } return { render: function(container) { expandedRows = {}; var headers = ['#', 'saddr', 'desc', '数据类型', 'ctrl_type', '定值区数', '展开']; container.innerHTML = '' + '' + makeToolbar('param') + renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号'); bindToolbar('param', function() { return data; }); bindEvents(); }, onEnter: function() {}, onLeave: function() { data = []; expandedRows = {}; }, onData: function(arr) { data = arr || []; expandedRows = {}; // 新数据到达时重置展开状态 var tbody = document.getElementById('param-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无定值数据,请先添加信号'; return; } tbody.innerHTML = renderTableHtml(); bindEvents(); } }; })(); /* ======================================================================== * 8. MonitorPage — 数据交互监控(可展开列表,15 分钟留存) * ======================================================================== */ var MonitorPage = (function() { var refreshTimer = null; var expandedSet = {}; // 记录展开状态 {index: true} function formatTime(ts) { var d = new Date(ts); var h = String(d.getHours()).padStart(2, '0'); var m = String(d.getMinutes()).padStart(2, '0'); var s = String(d.getSeconds()).padStart(2, '0'); var ms = String(d.getMilliseconds()).padStart(3, '0'); return h + ':' + m + ':' + s + '.' + ms; } function buildSummary(dir, raw) { try { var obj = JSON.parse(raw); if (dir === '↑') { // 上行:提取 curd, signal_type, saddr var parts = []; if (obj.curd) parts.push(obj.curd); if (obj.signal_type) parts.push(obj.signal_type); if (obj.saddr) parts.push(obj.saddr); return parts.length > 0 ? parts.join(' ') : raw.substring(0, 60); } else { // 下行:统计各信号类型数量 var summaryParts = []; var types = ['out', 'in', 'yk', 'ao', 'param']; for (var t = 0; t < types.length; t++) { var arr = obj[types[t]]; if (Array.isArray(arr) && arr.length > 0) { summaryParts.push(types[t] + '×' + arr.length); } } return summaryParts.length > 0 ? summaryParts.join(', ') : raw.substring(0, 60); } } catch (e) { return raw.substring(0, 60); } } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderLog() { var el = document.getElementById('monitor-log'); if (!el) return; var records = MonitorRing.getAll(); if (records.length === 0) { el.innerHTML = '
暂无数据交互,请先操作信号页面...
'; document.getElementById('monitor-count').textContent = '0'; return; } // 保存滚动状态:用户是否在底部 var prevScrollTop = el.scrollTop; var prevClientHeight = el.clientHeight; var prevScrollHeight = el.scrollHeight; var wasAtBottom = (prevScrollTop + prevClientHeight >= prevScrollHeight - 10); var html = ''; for (var i = 0; i < records.length; i++) { var r = records[i]; var isOpen = expandedSet[i] === true; var summary = buildSummary(r.dir, r.data); var dirClass = r.dir === '↑' ? 'mon-up' : 'mon-down'; var dirLabel = r.dir === '↑' ? '↑ 发送' : '↓ 接收'; var source = r.dir === '↑' ? '客户端' : 'RTU Server'; var target = r.dir === '↑' ? 'RTU Server' : '客户端'; var arrow = r.dir === '↑' ? '→' : '←'; html += '
'; html += '
'; html += '[' + formatTime(r.time) + ']'; html += '' + dirLabel + ''; html += '' + source + ' ' + arrow + ' ' + target + ''; html += '' + escapeHtml(summary) + ''; html += '' + (isOpen ? '▼' : '▶') + ''; html += '
'; if (isOpen) { html += '
' + escapeHtml(r.data) + '
'; } html += '
'; } el.innerHTML = html; // 绑定点击展开/收起 var heads = el.querySelectorAll('.mon-item-head'); for (var k = 0; k < heads.length; k++) { heads[k].onclick = function() { var item = this.parentElement; var idx = parseInt(item.getAttribute('data-midx')); expandedSet[idx] = !expandedSet[idx]; renderLog(); }; } // 恢复滚动位置:在底部才跟随,否则保持原位 if(wasAtBottom) { el.scrollTop = el.scrollHeight; } else if(prevScrollHeight > 0) { var ratio = prevScrollTop / prevScrollHeight; el.scrollTop = Math.round(ratio * el.scrollHeight); } document.getElementById('monitor-count').textContent = records.length; } return { render: function(container) { expandedSet = {}; container.innerHTML = '' + '' + '
' + '状态:' + '' + WsClient.getStatus() + '' + '已记录:' + '0' + '' + '' + '
' + '
'; document.getElementById('monitor-clear').onclick = function() { MonitorRing.clear(); expandedSet = {}; renderLog(); }; renderLog(); }, onEnter: function() { refreshTimer = setInterval(renderLog, 500); }, onLeave: function() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } expandedSet = {}; } }; })(); /* ======================================================================== * 命令终端页面 * ======================================================================== */ var TerminalPage = (function() { var history = []; var historyIdx = 0; var cmdList = []; function append(text, cls) { var el = document.getElementById('term-output'); if (!el) return; var div = document.createElement('div'); div.className = cls || ''; div.textContent = text; el.appendChild(div); el.scrollTop = el.scrollHeight; } function renderCmdList() { var el = document.getElementById('term-cmd-list'); if (!el) return; if (cmdList.length === 0) { el.innerHTML = '等待命令列表...'; return; } var html = ''; for (var i = 0; i < cmdList.length; i++) { var cmd = cmdList[i]; html += '' + cmd.name + ''; } el.innerHTML = html; var tags = el.querySelectorAll('.cmd-tag'); for (var k = 0; k < tags.length; k++) { tags[k].onclick = function() { var input = document.getElementById('term-input'); if (input) { input.value = this.getAttribute('data-cmd') + ' '; input.focus(); } }; } } return { render: function(container) { history = []; historyIdx = 0; container.innerHTML = '' + '' + '
' + '
' + '
' + '
' + 'cmd > ' + '' + '
' + '
'; var input = document.getElementById('term-input'); if (!input) return; input.onkeydown = function(e) { if (e.key === 'Enter') { var cmd = input.value.trim(); if (cmd) { history.push(cmd); historyIdx = history.length; append('cmd > ' + cmd, 'term-sent'); if (!WsClient.send({type:'cmd', data:cmd})) { append('[未连接]', 'term-err'); } } input.value = ''; } else if (e.key === 'ArrowUp') { e.preventDefault(); if (history.length > 0) { historyIdx = Math.max(0, historyIdx - 1); input.value = history[historyIdx]; } } else if (e.key === 'ArrowDown') { e.preventDefault(); if (historyIdx < history.length - 1) { historyIdx++; input.value = history[historyIdx]; } else { historyIdx = history.length; input.value = ''; } } }; renderCmdList(); append('输入 help 查看所有命令', 'term-info'); }, onData: function(data) { if (data && data.cmds) { cmdList = data.cmds; renderCmdList(); return; } if (data && typeof data === 'string') { append(data, 'term-resp'); } else if (data && data.type === 'cmd_list') { cmdList = data.cmds; renderCmdList(); } }, onBinary: function(ab) { var decoder = new TextDecoder('utf-8'); var text = decoder.decode(ab); if (text) append(text, 'term-resp'); }, onEnter: function() { var input = document.getElementById('term-input'); if (input) setTimeout(function() { input.focus(); }, 100); renderCmdList(); if (cmdList.length === 0) { WsClient.send({type:'get_cmds'}); } }, onLeave: function() { } }; })(); /* ======================================================================== * 数据中心页面 * ======================================================================== */ var DatacenterPage = (function() { var allData = {}; var activeTab = 'out'; var searchText = ''; var COLUMNS = ['id', 'saddr', 'desc', '数据类型', 'ctrl_type', 'link_saddrs']; function renderTable() { var data = allData[activeTab] || []; var tbody = document.getElementById('dc-tbody'); if (!tbody) return; if (data.length === 0) { tbody.innerHTML = '暂无数据'; document.getElementById('dc-count').textContent = '0'; return; } var rows = data; if (searchText) { var st = searchText.toLowerCase(); rows = data.filter(function(item) { return (item.saddr && item.saddr.toLowerCase().indexOf(st) !== -1) || (item.desc && item.desc.toLowerCase().indexOf(st) !== -1); }); } document.getElementById('dc-count').textContent = rows.length + ' / ' + data.length; if (rows.length === 0) { tbody.innerHTML = '无匹配结果'; return; } var html = ''; for (var i = 0; i < rows.length; i++) { var item = rows[i]; html += '' + '' + fmtVal(item.id) + '' + '' + fmtVal(item.saddr) + '' + '' + fmtVal(item.desc) + '' + '' + fmtVal(item.type) + '' + '' + fmtVal(CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type) + '' + '' + fmtVal(item.link_saddrs) + '' + ''; } tbody.innerHTML = html; } function renderHeaders() { var thead = document.getElementById('dc-thead'); if (!thead) return; var html = ''; for (var i = 0; i < COLUMNS.length; i++) { html += '' + COLUMNS[i] + ''; } thead.innerHTML = html; } function renderTabs() { var types = ['out', 'in', 'yk', 'ao', 'param']; var labels = { out: '注册 (out)', in: '链接注册 (in)', yk: '遥控 (yk)', ao: '参数 (ao)', param: '定值 (param)' }; var el = document.getElementById('dc-tab-bar'); if (!el) return; var html = ''; for (var i = 0; i < types.length; i++) { var t = types[i]; var cls = t === activeTab ? 'dc-tab active' : 'dc-tab'; html += '' + labels[t] + ''; } el.innerHTML = html; var tabs = el.querySelectorAll('.dc-tab'); for (var k = 0; k < tabs.length; k++) { tabs[k].onclick = function() { activeTab = this.getAttribute('data-tab'); searchText = ''; document.getElementById('dc-search').value = ''; renderTabs(); renderHeaders(); renderTable(); }; } } return { render: function(container) { allData = {}; activeTab = 'out'; searchText = ''; container.innerHTML = '' + '' + '
' + '
' + '' + '0' + '
' + '
' + '
' + '
'; renderTabs(); renderHeaders(); renderTable(); var searchInput = document.getElementById('dc-search'); if (searchInput) { searchInput.oninput = function() { searchText = this.value; renderTable(); }; } }, onEnter: function() { }, onLeave: function() { allData = {}; }, onData: function(data) { if (!data || typeof data !== 'object') return; if (data.out) allData.out = data.out; if (data.in) allData.in = data.in; if (data.yk) allData.yk = data.yk; if (data.ao) allData.ao = data.ao; if (data.param) allData.param = data.param; if (data.out || data.in || data.yk || data.ao || data.param) { renderTable(); } } }; })();