/** * pages.js — 页面渲染器 * * 每个页面对象实现: * render(container) — 渲染 HTML 到容器 * onEnter() — 页面激活时调用 * onLeave() — 页面离开时调用 * onData(signalType, arr) — 接收下行数据更新表格 * * 术语映射: * out → 注册 | in → 链接注册 | yk → 遥控 | ao → 参数 | param → 定值 */ var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' }; /* ======================================================================== * 共用工具栏 HTML * ======================================================================== */ function makeToolbar(signalType, showSet) { var typeLabel = signalType === 'ao' ? '参数' : (signalType === 'param' ? '定值' : signalType); return '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '' + '
'; } function bindToolbar(signalType) { 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 = ''; }; } /* ======================================================================== * 表格渲染工具 * ======================================================================== */ 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, '"'); } /* ======================================================================== * 1. LoginPage — 登录页 * ======================================================================== */ var LoginPage = { render: function(container) { container.innerHTML = '' + '
' + '

RTU 远程终端监控系统

' + '

Remote Terminal Unit Web Monitor

' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '

当前版本无需认证,点击登录即可进入系统

' + '
'; 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' } ]; var html = ''; html += '
'; for (var i = 0; i < cards.length; i++) { var c = cards[i]; html += '' + '
' + c.icon + '
' + '
' + c.title + '
' + '
' + c.desc + '
' + '
'; } html += '
'; container.innerHTML = html; }, 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'); 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'); }, 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'); 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'); 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'); 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 = {}; } }; })();