RTU/test/web_root/js/pages.js

1384 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* pages.js — 页面渲染器
*
* 每个页面对象实现:
* render(container) — 渲染 HTML 到容器
* onEnter() — 页面激活时调用
* onLeave() — 页面离开时调用
* onData(signalType, arr) — 接收下行数据更新表格
*
* 术语映射:
* out → 注册 | in → 链接注册 | yk → 遥控 | ao → 参数 | param → 定值
*/
var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' };
/* 每种信号类型的最大注册数量 */
var MAX_SIGNALS_PER_TYPE = 500;
/**
* 获取指定信号类型已注册的数量
* @param {string} signalType - out/in/yk/ao/param
* @returns {number}
*/
function getRegisteredCount(signalType) {
var data = (typeof App !== 'undefined' && App.getSignalData)
? App.getSignalData(signalType) : null;
return (data && Array.isArray(data)) ? data.length : 0;
}
/* ========================================================================
* 共用工具
* ======================================================================== */
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ------------------------------------------------------------------------
* XML 工具函数(导入导出)
* ------------------------------------------------------------------------ */
function escapeXml(str) {
if (typeof str !== 'string') return String(str);
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
/**
* 导出所有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 = '<?xml version="1.0" encoding="UTF-8"?>\n<root>\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 += ' <signals type="' + st + '"/>\n';
continue;
}
hasData = true;
xml += ' <signals type="' + st + '">\n';
for (var i = 0; i < data.length; i++) {
xml += ' <signal no="' + i + '" saddr="' + escapeXml(data[i].saddr) + '"/>\n';
}
xml += ' </signals>\n';
}
xml += '</root>\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 文件中没有 <signals> 元素');
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 ''
+ '<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, 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 (curd === 'add') {
var cur = getRegisteredCount(signalType);
if (cur >= MAX_SIGNALS_PER_TYPE) {
alert(signalType + ' 已注册 ' + cur + ' 个,最多允许 ' + MAX_SIGNALS_PER_TYPE + ' 个,请先删除旧信号');
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 = ''
+ '<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' },
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' },
{ cls: 'plc', icon: '🔌', title: 'PLC调试', desc: '指示灯/按键绑定 out 信号调试', hash: '#plc_debug' },
{ cls: 'plc_cfg', icon: '🧮', title: 'PLC逻辑配置', desc: 'PLC 逻辑图可视化查看与在线编辑', hash: '#plc_config' }
];
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>';
html += '<div class="batch-toolbar">'
+ '<h3>📦 批量操作</h3>'
+ '<p>导入/导出所有信号类型 (out/in/yk/ao/param) 到一个 XML 文件</p>'
+ '<button class="btn btn-info" id="btn-export-all" style="margin-right:12px">📤 批量导出所有信号</button>'
+ '<button class="btn btn-warn" id="btn-import-all">📥 批量导入所有信号</button>'
+ '</div>';
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 = [];
var selected = {}; /* {idx: true} 行选中状态 */
function getCheckedItems() {
var items = [];
for (var k in selected) {
if (selected.hasOwnProperty(k) && data[parseInt(k)]) {
items.push(data[parseInt(k)]);
}
}
return items;
}
function updateDelBtn() {
var btn = document.getElementById('out-del-btn');
if (!btn) return;
var checked = document.querySelectorAll('#out-tbody .row-cb:checked');
var cnt = checked.length;
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
btn.disabled = (cnt === 0);
}
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var checked = !!selected[i];
rows += '<tr>'
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell"><span id="out-val-' + i + '">' + fmtVal(item.val) + '</span></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' });
};
}
/* 绑定行复选框 */
var cbs = document.querySelectorAll('#out-tbody .row-cb');
for (var j = 0; j < cbs.length; j++) {
cbs[j].onclick = function(e) {
e.stopPropagation();
var idx = this.getAttribute('data-idx');
if (this.checked) selected[idx] = true; else delete selected[idx];
updateDelBtn();
};
}
}
function doDeleteChecked() {
var items = getCheckedItems();
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
for (var i = 0; i < items.length; i++) {
WsClient.send({ curd:'del', signal_type:'out', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
}
selected = {};
updateDelBtn();
}
return {
render: function(container) {
selected = {};
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
container.innerHTML = ''
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
+ makeToolbar('out')
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="out-del-btn" disabled>🗑 删除选中</button></div>'
+ renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号');
bindToolbar('out', function() { return data; });
bindModifyButtons();
var delBtn = document.getElementById('out-del-btn');
if (delBtn) delBtn.onclick = doDeleteChecked;
updateDelBtn();
},
onEnter: function() {},
onLeave: function() { data = []; selected = {}; },
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="8">暂无注册数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
var oldRowCnt = tbody.querySelectorAll('tr:not(.empty-row)').length;
if (oldRowCnt !== data.length) {
tbody.innerHTML = renderTableHtml();
bindModifyButtons();
} else {
var valCells = tbody.querySelectorAll('.val-cell');
for (var v = 0; v < data.length && v < valCells.length; v++) {
valCells[v].textContent = fmtVal(data[v].val);
}
}
updateDelBtn();
}
};
})();
/* ========================================================================
* 4. InPage — 链接注册管理 (in)
* ======================================================================== */
var InPage = (function() {
var data = [];
var selected = {};
function getCheckedItems() {
var items = [];
for (var k in selected) {
if (selected.hasOwnProperty(k) && data[parseInt(k)]) items.push(data[parseInt(k)]);
}
return items;
}
function updateDelBtn() {
var btn = document.getElementById('in-del-btn');
if (!btn) return;
var cnt = 0;
for (var k in selected) { if (selected.hasOwnProperty(k)) cnt++; }
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
btn.disabled = (cnt === 0);
}
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var checked = !!selected[i];
rows += '<tr>'
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell"><span id="in-val-' + i + '">' + fmtVal(item.val) + '</span></td>'
+ '</tr>';
}
return rows;
}
function bindCb() {
var cbs = document.querySelectorAll('#in-tbody .row-cb');
for (var j = 0; j < cbs.length; j++) {
cbs[j].onclick = function(e) {
e.stopPropagation();
var idx = this.getAttribute('data-idx');
if (this.checked) selected[idx] = true; else delete selected[idx];
updateDelBtn();
};
}
}
function doDeleteChecked() {
var items = getCheckedItems();
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
for (var i = 0; i < items.length; i++) {
WsClient.send({ curd:'del', signal_type:'in', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
}
selected = {};
updateDelBtn();
}
return {
render: function(container) {
selected = {};
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val'];
container.innerHTML = ''
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
+ makeToolbar('in')
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="in-del-btn" disabled>🗑 删除选中</button></div>'
+ renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号');
bindToolbar('in', function() { return data; });
bindCb();
var delBtn = document.getElementById('in-del-btn');
if (delBtn) delBtn.onclick = doDeleteChecked;
updateDelBtn();
},
onEnter: function() {},
onLeave: function() { data = []; selected = {}; },
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="6">暂无链接注册数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
tbody.innerHTML = renderTableHtml();
bindCb();
updateDelBtn();
}
};
})();
/* ========================================================================
* 5. YkPage — 遥控管理 (yk)
* ======================================================================== */
var YkPage = (function() {
var data = [];
var selected = {};
function getCheckedItems() {
var items = [];
for (var k in selected) { if (selected.hasOwnProperty(k) && data[parseInt(k)]) items.push(data[parseInt(k)]); }
return items;
}
function updateDelBtn() {
var btn = document.getElementById('yk-del-btn');
if (!btn) return;
var cnt = 0;
for (var k in selected) { if (selected.hasOwnProperty(k)) cnt++; }
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
btn.disabled = (cnt === 0);
}
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var checked = !!selected[i];
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
rows += '<tr>'
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
+ '<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="yk-mod-' + i + '" placeholder="修改值"></td>'
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</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' });
};
}
var cbs = document.querySelectorAll('#yk-tbody .row-cb');
for (var j = 0; j < cbs.length; j++) {
cbs[j].onclick = function(e) { e.stopPropagation(); var idx = this.getAttribute('data-idx'); if (this.checked) selected[idx] = true; else delete selected[idx]; updateDelBtn(); };
}
}
function doDeleteChecked() {
var items = getCheckedItems();
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
for (var i = 0; i < items.length; i++) {
WsClient.send({ curd:'del', signal_type:'yk', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
}
selected = {};
updateDelBtn();
}
return {
render: function(container) {
selected = {};
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改'];
container.innerHTML = ''
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
+ makeToolbar('yk')
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="yk-del-btn" disabled>🗑 删除选中</button></div>'
+ renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号');
bindToolbar('yk', function() { return data; });
bindButtons();
var delBtn = document.getElementById('yk-del-btn');
if (delBtn) delBtn.onclick = doDeleteChecked;
updateDelBtn();
},
onEnter: function() {},
onLeave: function() { data = []; selected = {}; },
onData: function(arr) {
data = arr || [];
// 不重置 selected保留用户勾选
var tbody = document.getElementById('yk-tbody');
if (!tbody) return;
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="9">暂无遥控数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
tbody.innerHTML = renderTableHtml();
bindButtons();
updateDelBtn();
}
};
})();
/* ========================================================================
* 6. AoPage — 参数管理 (ao, 单值修改)
* ======================================================================== */
var AoPage = (function() {
var data = [];
var selected = {};
function getCheckedItems() { var items=[]; for(var k in selected){if(selected.hasOwnProperty(k)&&data[parseInt(k)])items.push(data[parseInt(k)]);} return items; }
function updateDelBtn() { var btn=document.getElementById('ao-del-btn'); if(!btn)return; var cnt=0; for(var k in selected){if(selected.hasOwnProperty(k))cnt++;} btn.textContent='🗑 删除选中'+(cnt>0?'('+cnt+')':''); btn.disabled=(cnt===0); }
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i]; var checked = !!selected[i];
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
rows += '<tr>'
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
+ '<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 bindAll() {
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'});};}
var cbs = document.querySelectorAll('#ao-tbody .row-cb');
for (var j = 0; j < cbs.length; j++) { cbs[j].onclick=function(e){e.stopPropagation();var idx=this.getAttribute('data-idx');if(this.checked)selected[idx]=true;else delete selected[idx];updateDelBtn();};}
}
function doDeleteChecked() { var items=getCheckedItems(); if(items.length===0){alert('请先勾选要删除的信号');return;} if(!confirm('确定删除 '+items.length+' 个信号?'))return; for(var i=0;i<items.length;i++){WsClient.send({curd:'del',signal_type:'ao',saddr:items[i].saddr,setting_zone:'0',signal_data:''});} selected={}; updateDelBtn(); }
return {
render: function(container) {
selected = {};
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')
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="ao-del-btn" disabled>🗑 删除选中</button></div>'
+ renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号');
bindToolbar('ao', function() { return data; });
bindAll();
var delBtn = document.getElementById('ao-del-btn');
if (delBtn) delBtn.onclick = doDeleteChecked;
updateDelBtn();
},
onEnter: function() {},
onLeave: function() { data = []; selected = {}; },
onData: function(arr) {
data = arr || [];
// 不重置 selected保留用户勾选
var tbody = document.getElementById('ao-tbody');
if (!tbody) return;
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="13">暂无参数数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
tbody.innerHTML = renderTableHtml();
bindAll();
updateDelBtn();
}
};
})();
/* ========================================================================
* 7. ParamPage — 定值管理 (param, 多定值区展开)
* ======================================================================== */
var ParamPage = (function() {
var data = [];
var expandedRows = {};
var selected = {};
function getCheckedItems() { var items=[]; for(var k in selected){if(selected.hasOwnProperty(k)&&data[parseInt(k)])items.push(data[parseInt(k)]);} return items; }
function updateDelBtn() { var btn=document.getElementById('param-del-btn'); if(!btn)return; var cnt=0; for(var k in selected){if(selected.hasOwnProperty(k))cnt++;} btn.textContent='🗑 删除选中'+(cnt>0?'('+cnt+')':''); btn.disabled=(cnt===0); }
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 ? '▼' : '▶';
var checked = !!selected[i];
// 主行(可点击展开)
rows += '<tr class="expand-row" data-pidx="' + i + '">'
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
+ '<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="8">'
+ '<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(e) {
if (e.target.tagName === 'INPUT') return; // 不拦截勾选框
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});};
}
var cbs = document.querySelectorAll('#param-tbody .row-cb');
for (var j = 0; j < cbs.length; j++) {
cbs[j].onclick = function(e) { e.stopPropagation(); var idx = this.getAttribute('data-idx'); if(this.checked)selected[idx]=true; else delete selected[idx]; updateDelBtn(); };
}
}
function doDeleteChecked() { var items=getCheckedItems(); if(items.length===0){alert('请先勾选要删除的信号');return;} if(!confirm('确定删除 '+items.length+' 个信号?'))return; for(var i=0;i<items.length;i++){WsClient.send({curd:'del',signal_type:'param',saddr:items[i].saddr,setting_zone:'0',signal_data:''});} selected={}; expandedRows={}; updateDelBtn(); refreshTable(); }
function refreshTable() {
var tbody = document.getElementById('param-tbody');
if (!tbody) return;
tbody.innerHTML = renderTableHtml();
bindEvents();
}
return {
render: function(container) {
expandedRows = {};
selected = {};
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'ctrl_type', '定值区数', '展开'];
container.innerHTML = ''
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
+ makeToolbar('param')
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="param-del-btn" disabled>🗑 删除选中</button></div>'
+ renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号');
bindToolbar('param', function() { return data; });
bindEvents();
var delBtn = document.getElementById('param-del-btn');
if (delBtn) delBtn.onclick = doDeleteChecked;
updateDelBtn();
},
onEnter: function() {},
onLeave: function() { data = []; expandedRows = {}; selected = {}; },
onData: function(arr) {
data = arr || [];
expandedRows = {};
// 不重置 selected保留用户勾选
var tbody = document.getElementById('param-tbody');
if (!tbody) return;
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="8">暂无定值数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
tbody.innerHTML = renderTableHtml();
bindEvents();
updateDelBtn();
}
};
})();
/* ========================================================================
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 = {};
}
};
})();
/* ========================================================================
* 命令终端页面
* ======================================================================== */
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 = '<span class="cmd-list-hint">等待命令列表...</span>';
return;
}
var html = '';
for (var i = 0; i < cmdList.length; i++) {
var cmd = cmdList[i];
html += '<span class="cmd-tag" data-cmd="' + cmd.name + '" title="' + cmd.desc + '">' + cmd.name + '</span>';
}
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 = ''
+ '<div class="page-header"><h2>命令终端</h2><p>输入 RTU 命令Enter 发送,↑↓ 历史记录</p></div>'
+ '<div class="terminal-wrap">'
+ '<div class="terminal-cmd-list" id="term-cmd-list"></div>'
+ '<div class="terminal-output" id="term-output"></div>'
+ '<div class="terminal-input-line">'
+ '<span class="terminal-prompt">cmd &gt; </span>'
+ '<input type="text" class="terminal-input" id="term-input" autofocus spellcheck="false">'
+ '</div>'
+ '</div>';
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 selectedSet = {}; // {saddr: true}
var COLUMNS = ['', 'id', 'saddr', 'desc', '数据类型', 'ctrl_type', 'link_saddrs'];
function getVisibleData() {
var data = allData[activeTab] || [];
if (!searchText) return data;
var st = searchText.toLowerCase();
return data.filter(function(item) {
return (item.saddr && item.saddr.toLowerCase().indexOf(st) !== -1)
|| (item.desc && item.desc.toLowerCase().indexOf(st) !== -1);
});
}
function onSelectAll() {
var visible = getVisibleData();
var cb = document.getElementById('dc-cb-all');
if (cb && cb.checked && visible.length > 0) {
for (var i = 0; i < visible.length; i++) {
selectedSet[visible[i].saddr] = true;
}
} else {
for (var i = 0; i < visible.length; i++) {
delete selectedSet[visible[i].saddr];
}
}
renderTable();
updateSelectionBar();
}
function onToggleRow(saddr) {
if (selectedSet[saddr]) {
delete selectedSet[saddr];
} else {
selectedSet[saddr] = true;
}
renderTable();
updateSelectionBar();
}
function getSelectedCount() {
var count = 0;
for (var k in selectedSet) {
if (selectedSet.hasOwnProperty(k)) count++;
}
return count;
}
function getSelectedItems() {
var result = [];
var fullData = allData[activeTab] || [];
for (var i = 0; i < fullData.length; i++) {
if (selectedSet[fullData[i].saddr]) {
result.push(fullData[i]);
}
}
return result;
}
function updateSelectionBar() {
var bar = document.getElementById('dc-sel-bar');
if (!bar) return;
var count = getSelectedCount();
if (count === 0) {
bar.style.display = 'none';
} else {
bar.style.display = 'flex';
bar.querySelector('.dc-sel-label').textContent = '已选 ' + count + ' 个信号';
}
}
function doAddSelected() {
var items = getSelectedItems();
if (items.length === 0) {
alert('请先勾选要添加的信号');
return;
}
var typeLabels = { out:'注册', in:'链接注册', yk:'遥控', ao:'参数', param:'定值' };
var label = typeLabels[activeTab] || activeTab;
var cur = getRegisteredCount(activeTab);
if (cur + items.length > MAX_SIGNALS_PER_TYPE) {
alert(label + ' 已有 ' + cur + ' 个,本次选择 ' + items.length + ' 个,合计 ' + (cur + items.length) + ' 个,超过上限 ' + MAX_SIGNALS_PER_TYPE + ' 个。\n请先删除旧信号或减少选择');
return;
}
if (!confirm('确定将 ' + items.length + ' 个信号添加到 ' + label + '')) return;
var succ = 0;
for (var i = 0; i < items.length; i++) {
var ok = WsClient.send({
saddr: items[i].saddr,
signal_type: activeTab,
curd: 'add',
setting_zone: '0',
signal_data: ''
});
if (ok) succ++;
}
alert('已发送 ' + succ + '/' + items.length + ' 个 add 请求到 ' + label);
}
function renderHeaders() {
var thead = document.getElementById('dc-thead');
if (!thead) return;
var visible = getVisibleData();
var allChecked = visible.length > 0;
for (var i = 0; i < visible.length && allChecked; i++) {
if (!selectedSet[visible[i].saddr]) allChecked = false;
}
var html = '<th style="width:36px"><input type="checkbox" id="dc-cb-all"' + (allChecked ? ' checked' : '') + '></th>';
for (var j = 1; j < COLUMNS.length; j++) {
html += '<th>' + COLUMNS[j] + '</th>';
}
thead.innerHTML = html;
var cbAll = document.getElementById('dc-cb-all');
if (cbAll) cbAll.onchange = onSelectAll;
}
function renderTable() {
var data = getVisibleData();
var tbody = document.getElementById('dc-tbody');
if (!tbody) return;
var fullData = allData[activeTab] || [];
document.getElementById('dc-count').textContent = data.length + ' / ' + fullData.length;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="' + COLUMNS.length + '">暂无数据</td></tr>';
return;
}
var html = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var checked = !!selectedSet[item.saddr];
html += '<tr>'
+ '<td><input type="checkbox" class="dc-cb-row"' + (checked ? ' checked' : '') + ' data-saddr="' + fmtVal(item.saddr) + '"></td>'
+ '<td>' + fmtVal(item.id) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td>' + fmtVal(CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type) + '</td>'
+ '<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + fmtVal(item.link_saddrs) + '">' + fmtVal(item.link_saddrs) + '</td>'
+ '</tr>';
}
tbody.innerHTML = html;
// 绑定行复选框事件
var cbs = tbody.querySelectorAll('.dc-cb-row');
for (var k = 0; k < cbs.length; k++) {
cbs[k].onclick = function(e) {
e.stopPropagation();
onToggleRow(this.getAttribute('data-saddr'));
};
}
}
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 += '<span class="' + cls + '" data-tab="' + t + '">' + labels[t] + '</span>';
}
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 = '';
selectedSet = {};
document.getElementById('dc-search').value = '';
renderTabs();
renderHeaders();
renderTable();
updateSelectionBar();
};
}
}
return {
render: function(container) {
allData = {};
activeTab = 'out';
searchText = '';
selectedSet = {};
container.innerHTML = ''
+ '<div class="page-header"><h2>数据中心</h2><p>勾选信号后点击"添加到"即可添加至对应配置页 | 查看所有信号数据,按类型分表,支持搜索</p></div>'
+ '<div class="datacenter-toolbar">'
+ '<div class="dc-tab-bar" id="dc-tab-bar"></div>'
+ '<input type="text" class="dc-search" id="dc-search" placeholder="搜索 saddr 或 desc...">'
+ '<span class="dc-count" id="dc-count">0</span>'
+ '</div>'
+ '<div class="dc-sel-bar" id="dc-sel-bar" style="display:none">'
+ '<span class="dc-sel-label">已选 0 个信号</span>'
+ '<button class="btn btn-primary" id="dc-btn-add" style="margin-left:16px">添加到配置页</button>'
+ '</div>'
+ '<div class="table-card"><div class="table-wrapper">'
+ '<table class="signal-table"><thead><tr id="dc-thead"></tr></thead><tbody id="dc-tbody"></tbody></table>'
+ '</div></div>';
renderTabs();
renderHeaders();
renderTable();
var searchInput = document.getElementById('dc-search');
if (searchInput) {
searchInput.oninput = function() {
searchText = this.value;
renderHeaders();
renderTable();
};
}
var addBtn = document.getElementById('dc-btn-add');
if (addBtn) addBtn.onclick = doAddSelected;
},
onEnter: function() {},
onLeave: function() {
allData = {};
selectedSet = {};
},
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) {
renderHeaders();
renderTable();
updateSelectionBar();
}
}
};
})();