687 lines
28 KiB
JavaScript
687 lines
28 KiB
JavaScript
/**
|
||
* 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 ''
|
||
+ '<div class="toolbar">'
|
||
+ '<div class="field">'
|
||
+ '<label>短地址 saddr</label>'
|
||
+ '<input type="text" id="tb-saddr" placeholder="例: iec.run_cnt">'
|
||
+ '</div>'
|
||
+ '<div class="field">'
|
||
+ '<label>操作</label>'
|
||
+ '<select id="tb-curd">'
|
||
+ '<option value="add">add 添加</option>'
|
||
+ '<option value="del">del 删除</option>'
|
||
+ '</select>'
|
||
+ '</div>'
|
||
+ '<button class="btn btn-primary" id="tb-exec">执行</button>'
|
||
+ '<button class="btn btn-outline" id="tb-clear">清空输入</button>'
|
||
+ '</div>';
|
||
}
|
||
|
||
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 = '<div class="table-card"><div class="table-wrapper"><table class="signal-table">';
|
||
html += '<thead><tr>';
|
||
for (var i = 0; i < headers.length; i++) {
|
||
html += '<th>' + headers[i] + '</th>';
|
||
}
|
||
html += '</tr></thead><tbody id="' + containerId + '">';
|
||
if (rowsHtml) {
|
||
html += rowsHtml;
|
||
} else {
|
||
html += '<tr class="empty-row"><td colspan="' + headers.length + '">' + (emptyText || '暂无数据,请先添加信号') + '</td></tr>';
|
||
}
|
||
html += '</tbody></table></div></div>';
|
||
return html;
|
||
}
|
||
|
||
function fmtVal(v) {
|
||
if (v === null || v === undefined || v === '') return '-';
|
||
return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
/* ========================================================================
|
||
* 1. LoginPage — 登录页
|
||
* ======================================================================== */
|
||
var LoginPage = {
|
||
render: function(container) {
|
||
container.innerHTML = ''
|
||
+ '<div class="login-card">'
|
||
+ '<h1>RTU 远程终端监控系统</h1>'
|
||
+ '<p class="login-subtitle">Remote Terminal Unit Web Monitor</p>'
|
||
+ '<div class="form-group">'
|
||
+ '<label>用户名</label>'
|
||
+ '<input type="text" id="login-user" placeholder="暂无需认证" disabled>'
|
||
+ '</div>'
|
||
+ '<div class="form-group">'
|
||
+ '<label>密码</label>'
|
||
+ '<input type="password" id="login-pass" placeholder="暂无需认证" disabled>'
|
||
+ '</div>'
|
||
+ '<button class="login-btn" id="login-btn">登 录</button>'
|
||
+ '<p class="login-hint">当前版本无需认证,点击登录即可进入系统</p>'
|
||
+ '</div>';
|
||
|
||
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 = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
|
||
html += '<div class="dashboard-grid">';
|
||
for (var i = 0; i < cards.length; i++) {
|
||
var c = cards[i];
|
||
html += '<a class="dash-card ' + c.cls + '" href="' + c.hash + '">'
|
||
+ '<div class="card-icon">' + c.icon + '</div>'
|
||
+ '<div class="card-title">' + c.title + '</div>'
|
||
+ '<div class="card-desc">' + c.desc + '</div>'
|
||
+ '</a>';
|
||
}
|
||
html += '</div>';
|
||
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 += '<tr>'
|
||
+ '<td>' + (i + 1) + '</td>'
|
||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||
+ '<td><input class="val-input" id="mod-' + i + '" placeholder="修改值"></td>'
|
||
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
|
||
+ '</tr>';
|
||
}
|
||
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 = ''
|
||
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
|
||
+ 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 = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>';
|
||
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 += '<tr>'
|
||
+ '<td>' + (i + 1) + '</td>'
|
||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||
+ '</tr>';
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
return {
|
||
render: function(container) {
|
||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val'];
|
||
container.innerHTML = ''
|
||
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
|
||
+ 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 = '<tr class="empty-row"><td colspan="5">暂无链接注册数据,请先添加信号</td></tr>';
|
||
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 += '<tr>'
|
||
+ '<td>' + (i + 1) + '</td>'
|
||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||
+ '<td>' + ctrlLabel + '</td>'
|
||
+ '<td><button class="btn btn-sm btn-success" data-idx="' + i + '" data-val="1">控合</button></td>'
|
||
+ '<td><button class="btn btn-sm btn-danger" data-idx="' + i + '" data-val="0">控分</button></td>'
|
||
+ '</tr>';
|
||
}
|
||
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 = ''
|
||
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
|
||
+ 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 = '<tr class="empty-row"><td colspan="8">暂无遥控数据,请先添加信号</td></tr>';
|
||
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 += '<tr>'
|
||
+ '<td>' + (i + 1) + '</td>'
|
||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||
+ '<td>' + ctrlLabel + '</td>'
|
||
+ '<td>' + fmtVal(item.min) + '</td>'
|
||
+ '<td>' + fmtVal(item.max) + '</td>'
|
||
+ '<td>' + fmtVal(item.step) + '</td>'
|
||
+ '<td>' + fmtVal(item.unit) + '</td>'
|
||
+ '<td><input class="val-input" id="ao-mod-' + i + '" placeholder="修改值"></td>'
|
||
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
|
||
+ '</tr>';
|
||
}
|
||
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 = ''
|
||
+ '<div class="page-header"><h2>参数管理</h2><p>ao — 模拟输出参数设定,支持单值修改</p></div>'
|
||
+ 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 = '<tr class="empty-row"><td colspan="12">暂无参数数据,请先添加信号</td></tr>';
|
||
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 += '<tr class="expand-row" data-pidx="' + i + '">'
|
||
+ '<td>' + (i + 1) + '</td>'
|
||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||
+ '<td>' + ctrlLabel + '</td>'
|
||
+ '<td>' + zoneCount + '</td>'
|
||
+ '<td><span class="expand-icon' + (isOpen ? ' open' : '') + '">' + icon + '</span> 展开</td>'
|
||
+ '</tr>';
|
||
|
||
// 展开的定值区子表
|
||
if (isOpen) {
|
||
rows += '<tr class="zone-sub-row" id="zone-sub-' + i + '"><td colspan="7">'
|
||
+ '<table class="zone-sub-table">'
|
||
+ '<thead><tr>'
|
||
+ '<th>定值区 ID</th><th>val</th><th>default_val</th>'
|
||
+ '<th>min</th><th>max</th><th>step</th><th>unit</th>'
|
||
+ '<th>修改值</th><th>修改</th>'
|
||
+ '</tr></thead><tbody>';
|
||
|
||
for (var j = 0; j < zones.length; j++) {
|
||
var zone = zones[j];
|
||
rows += '<tr>'
|
||
+ '<td>' + fmtVal(zone.id) + '</td>'
|
||
+ '<td>' + fmtVal(zone.val) + '</td>'
|
||
+ '<td>' + fmtVal(zone.default_val) + '</td>'
|
||
+ '<td>' + fmtVal(item.min) + '</td>'
|
||
+ '<td>' + fmtVal(item.max) + '</td>'
|
||
+ '<td>' + fmtVal(item.step) + '</td>'
|
||
+ '<td>' + fmtVal(item.unit) + '</td>'
|
||
+ '<td><input class="val-input" id="pm-' + i + '-' + j + '" placeholder="修改值"></td>'
|
||
+ '<td><button class="btn btn-sm btn-info" data-pidx="' + i + '" data-zidx="' + j + '">修改</button></td>'
|
||
+ '</tr>';
|
||
}
|
||
|
||
rows += '</tbody></table></td></tr>';
|
||
}
|
||
}
|
||
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 = ''
|
||
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
|
||
+ 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 = '<tr class="empty-row"><td colspan="7">暂无定值数据,请先添加信号</td></tr>';
|
||
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, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function renderLog() {
|
||
var el = document.getElementById('monitor-log');
|
||
if (!el) return;
|
||
|
||
var records = MonitorRing.getAll();
|
||
if (records.length === 0) {
|
||
el.innerHTML = '<div class="monitor-empty">暂无数据交互,请先操作信号页面...</div>';
|
||
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 += '<div class="mon-item' + (isOpen ? ' open' : '') + '" data-midx="' + i + '">';
|
||
html += '<div class="mon-item-head">';
|
||
html += '<span class="mon-time">[' + formatTime(r.time) + ']</span>';
|
||
html += '<span class="mon-dir ' + dirClass + '">' + dirLabel + '</span>';
|
||
html += '<span class="mon-addr">' + source + ' ' + arrow + ' ' + target + '</span>';
|
||
html += '<span class="mon-summary">' + escapeHtml(summary) + '</span>';
|
||
html += '<span class="mon-expand-icon">' + (isOpen ? '▼' : '▶') + '</span>';
|
||
html += '</div>';
|
||
if (isOpen) {
|
||
html += '<div class="mon-item-body"><pre>' + escapeHtml(r.data) + '</pre></div>';
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
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 = ''
|
||
+ '<div class="page-header"><h2>数据监控</h2><p>实时查看 WebSocket 上下行 JSON 数据,15 分钟自动清理历史,点击条目展开详情</p></div>'
|
||
+ '<div class="monitor-stats">'
|
||
+ '<span class="stat-label">状态:</span>'
|
||
+ '<span class="stat-value" id="monitor-ws-status">' + WsClient.getStatus() + '</span>'
|
||
+ '<span class="stat-label" style="margin-left:16px">已记录:</span>'
|
||
+ '<span class="stat-value" id="monitor-count">0</span>'
|
||
+ '<span class="stat-label">条</span>'
|
||
+ '<button class="btn btn-sm btn-outline" id="monitor-clear" style="margin-left:auto">清空记录</button>'
|
||
+ '</div>'
|
||
+ '<div class="monitor-log" id="monitor-log"></div>';
|
||
|
||
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 = {};
|
||
}
|
||
};
|
||
})();
|