<新增> 1、前端对应增加命令终端与数据监控,命令终端支持自定义命令

This commit is contained in:
ypc 2026-06-18 11:09:14 +08:00
parent 6ef960525c
commit 742d9b36a0
8 changed files with 3731 additions and 2653 deletions

View File

@ -129,7 +129,9 @@ uint8_t dc_get_data_type_len(uint8_t data_type);
void dc_set_signal_val_from_str(void *p_data, uint8_t data_type, const std::string &str); void dc_set_signal_val_from_str(void *p_data, uint8_t data_type, const std::string &str);
uint32_t dc_get_signal_count(const char *type);
int dc_get_signal_info_by_id(const char *type, uint32_t id, std::string &saddr, std::string &desc, std::string &data_type, uint8_t &ctrl_type, std::vector<std::string> &link_saddrs);
typedef struct typedef struct
{ {

View File

@ -2242,6 +2242,41 @@ LOCAL void dc_show_signals(stru_signal_map &dc_signal_map)
cout << endl; cout << endl;
} }
uint32_t dc_get_signal_count(const char *type)
{
if(nullptr == type) return 0;
if(0 == strcmp(type, "out")) return g_datacenter.signal_out.signal_id;
if(0 == strcmp(type, "in")) return g_datacenter.signal_in.signal_id;
if(0 == strcmp(type, "yk")) return g_datacenter.signal_yk.signal_id;
if(0 == strcmp(type, "ao")) return g_datacenter.signal_ao.signal_id;
if(0 == strcmp(type, "param")) return g_datacenter.signal_param.signal_id;
return 0;
}
int dc_get_signal_info_by_id(const char *type, uint32_t id, std::string &saddr, std::string &desc, std::string &data_type, uint8_t &ctrl_type, std::vector<std::string> &link_saddrs)
{
if(nullptr == type) return -1;
stru_signal *p_signal = nullptr;
if(0 == strcmp(type, "out")) p_signal = dc_find_signal_by_id(id, g_datacenter.signal_out);
if(0 == strcmp(type, "in")) p_signal = dc_find_signal_by_id(id, g_datacenter.signal_in);
if(0 == strcmp(type, "yk")) p_signal = dc_find_signal_by_id(id, g_datacenter.signal_yk);
if(0 == strcmp(type, "ao")) p_signal = dc_find_signal_by_id(id, g_datacenter.signal_ao);
if(0 == strcmp(type, "param")) p_signal = dc_find_signal_by_id(id, g_datacenter.signal_param);
if(nullptr == p_signal)
{
MY_LOG_E("signal id %d not found", id);
return -1;
}
saddr = p_signal->saddr;
desc = p_signal->desc;
data_type = dc_get_data_type_str_by_id(p_signal->data_type);
ctrl_type = p_signal->ctrl_type;
link_saddrs = p_signal->link_saddrs;
return 0;
}
LOCAL void dc_show_out_signals() LOCAL void dc_show_out_signals()
{ {
using namespace std; using namespace std;

File diff suppressed because it is too large Load Diff

View File

@ -569,6 +569,53 @@ LOCAL void ws_send_large_text(unsigned long conn_id, const char *type, const std
} }
} }
LOCAL std::string ws_build_dc_data_json(const std::vector<const char*> &types)
{
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "dc_data");
for(size_t si = 0; si < types.size(); si++)
{
cJSON *arr = cJSON_CreateArray();
uint32_t count = dc_get_signal_count(types[si]);
for(uint32_t j = 0; j < count; j++)
{
uint32_t id;
uint8_t ctrl_type;
std::string saddr, desc, data_type;
std::vector<std::string> link_saddrs;
if(0 != dc_get_signal_info_by_id(types[si], j, saddr, desc, data_type, ctrl_type, link_saddrs))
{
continue;
}
cJSON *item = cJSON_CreateObject();
cJSON_AddNumberToObject(item, "id", j);
cJSON_AddStringToObject(item, "saddr", saddr.c_str());
cJSON_AddStringToObject(item, "desc", desc.c_str());
cJSON_AddStringToObject(item, "type", data_type.c_str());
cJSON_AddNumberToObject(item, "ctrl_type", ctrl_type);
std::string link_str;
for(size_t l = 0; l < link_saddrs.size(); l++)
{
if(l > 0) link_str += ", ";
link_str += link_saddrs[l];
}
cJSON_AddStringToObject(item, "link_saddrs", link_str.c_str());
cJSON_AddItemToArray(arr, item);
}
cJSON_AddItemToObject(resp, types[si], arr);
}
char *p_tx = cJSON_Print(resp);
std::string result;
if(p_tx)
{
result = p_tx;
free(p_tx);
}
cJSON_Delete(resp);
return result;
}
LOCAL std::string ws_cmd_exec(const char *cmd_line) LOCAL std::string ws_cmd_exec(const char *cmd_line)
{ {
@ -680,12 +727,60 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
cJSON *data = cJSON_GetObjectItem(root, "data"); cJSON *data = cJSON_GetObjectItem(root, "data");
if(data && cJSON_IsString(data) && data->valuestring) if(data && cJSON_IsString(data) && data->valuestring)
{ {
bool is_dc = (0 == strncmp(data->valuestring, "datacenter", 10));
std::string result = ws_cmd_exec(data->valuestring); std::string result = ws_cmd_exec(data->valuestring);
ws_send_binary(c->id, result.data(), result.size()); ws_send_binary(c->id, result.data(), result.size());
if(is_dc)
{
std::vector<const char*> dc_types;
const char *sub = data->valuestring + 10;
while(*sub == ' ' || *sub == '\t') sub++;
if(0 == strcmp(sub, "all") || 0 == strncmp(sub, "all ", 4))
{
dc_types = {"out", "in", "yk", "ao", "param"};
}
else if(0 == strcmp(sub, "out") || 0 == strncmp(sub, "out ", 4)) dc_types = {"out"};
else if(0 == strcmp(sub, "in") || 0 == strncmp(sub, "in ", 3)) dc_types = {"in"};
else if(0 == strcmp(sub, "yk") || 0 == strncmp(sub, "yk ", 3)) dc_types = {"yk"};
else if(0 == strcmp(sub, "ao") || 0 == strncmp(sub, "ao ", 3)) dc_types = {"ao"};
else if(0 == strcmp(sub, "param") || 0 == strncmp(sub, "param ", 6)) dc_types = {"param"};
else dc_types = {"out", "in", "yk", "ao", "param"};
std::string dc_json = ws_build_dc_data_json(dc_types);
if(!dc_json.empty())
{
ws_send_binary(c->id, dc_json.data(), dc_json.size());
}
}
} }
cJSON_Delete(root); cJSON_Delete(root);
return; return;
} }
if(0 == strcmp(type->valuestring, "get_cmds"))
{
unsigned int count = 0;
stru_cmd *cmds = cmd_manager_get_commands(&count);
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "cmd_list");
cJSON *arr = cJSON_CreateArray();
for(unsigned int i = 0; i < count; i++)
{
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "name", cmds[i].name);
cJSON_AddStringToObject(item, "desc", cmds[i].desc);
cJSON_AddItemToArray(arr, item);
}
cJSON_AddItemToObject(resp, "cmds", arr);
char *p_tx = cJSON_Print(resp);
if(p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(resp);
cJSON_Delete(root);
return;
}
} }
cJSON *curd = cJSON_GetObjectItem(root, "curd"); cJSON *curd = cJSON_GetObjectItem(root, "curd");

View File

@ -243,6 +243,7 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
.dash-card.ao { border-top: 4px solid #4CAF50; } .dash-card.ao { border-top: 4px solid #4CAF50; }
.dash-card.param { border-top: 4px solid #9C27B0; } .dash-card.param { border-top: 4px solid #9C27B0; }
.dash-card.monitor { border-top: 4px solid #607D8B; } .dash-card.monitor { border-top: 4px solid #607D8B; }
.dash-card.terminal { border-top: 4px solid #00BCD4; }
/* === Page Header === */ /* === Page Header === */
.page-header { .page-header {
@ -589,3 +590,87 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
.term-resp { color: #d4d4d4; } .term-resp { color: #d4d4d4; }
.term-err { color: #f44747; } .term-err { color: #f44747; }
.term-info { color: #6a9955; } .term-info { color: #6a9955; }
/* === 命令终端 — 命令列表 === */
.terminal-cmd-list {
padding: 8px 10px;
background: #252526;
border-bottom: 1px solid #3c3c3c;
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 120px;
overflow-y: auto;
}
.cmd-list-hint {
color: #6a9955;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.cmd-tag {
display: inline-block;
padding: 3px 10px;
background: #3c3c3c;
color: #d4d4d4;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.cmd-tag:hover {
background: #4CAF50;
color: #fff;
}
/* === 数据中心页面 === */
.datacenter-toolbar {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 12px 20px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.dc-tab-bar {
display: flex;
gap: 4px;
}
.dc-tab {
padding: 6px 16px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border);
transition: all 0.2s;
}
.dc-tab:hover {
background: #f0f0f0;
}
.dc-tab.active {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
.dc-search {
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 13px;
width: 200px;
}
.dc-search:focus {
outline: none;
border-color: var(--primary);
}
.dc-count {
font-size: 12px;
color: var(--text-muted);
margin-left: auto;
}

View File

@ -14,14 +14,15 @@ var App = (function() {
/* ---- 侧边栏导航 ---- */ /* ---- 侧边栏导航 ---- */
var navLinks = [ var navLinks = [
{ hash: '#dashboard', label: '🏠 功能导航' }, { hash: '#dashboard', label: '🏠 功能导航' },
{ hash: '#out', label: '📡 注册 (out)' }, { hash: '#out', label: '📡 注册 (out)' },
{ hash: '#in', label: '📊 链接注册 (in)' }, { hash: '#in', label: '📊 链接注册 (in)' },
{ hash: '#yk', label: '⚡ 遥控 (yk)' }, { hash: '#yk', label: '⚡ 遥控 (yk)' },
{ hash: '#ao', label: '🔧 参数 (ao)' }, { hash: '#ao', label: '🔧 参数 (ao)' },
{ hash: '#param', label: '⚙ 定值 (param)' }, { hash: '#param', label: '⚙ 定值 (param)' },
{ hash: '#monitor', label: '📋 数据监控' }, { hash: '#datacenter', label: '📊 数据中心' },
{ hash: '#terminal', label: '💻 命令终端' } { hash: '#monitor', label: '📋 数据监控' },
{ hash: '#terminal', label: '💻 命令终端' }
]; ];
function buildNav() { function buildNav() {
@ -111,7 +112,7 @@ var App = (function() {
} }
// 页面进入后立即推送缓存数据(如有) // 页面进入后立即推送缓存数据(如有)
var stMap = { '#out': 'out', '#in': 'in', '#yk': 'yk', '#ao': 'ao', '#param': 'param' }; var stMap = { '#out': 'out', '#in': 'in', '#yk': 'yk', '#ao': 'ao', '#param': 'param', '#datacenter': 'dc_data', '#terminal': 'cmd_list' };
var st = stMap[hash]; var st = stMap[hash];
if (st && page.onData && lastDataCache[st]) { if (st && page.onData && lastDataCache[st]) {
page.onData(lastDataCache[st]); page.onData(lastDataCache[st]);
@ -138,8 +139,28 @@ var App = (function() {
/* ---- WS 下行数据分发 ---- */ /* ---- WS 下行数据分发 ---- */
function onWsMessage(data, raw) { function onWsMessage(data, raw) {
if (raw instanceof ArrayBuffer && currentPage && currentPage.onBinary) { if (raw instanceof ArrayBuffer) {
currentPage.onBinary(raw); var decoder = new TextDecoder('utf-8');
var text = decoder.decode(raw);
try {
var parsed = JSON.parse(text);
if (parsed && parsed.type === 'dc_data') {
var cache = lastDataCache['dc_data'] || {};
if (parsed.out) cache.out = parsed.out;
if (parsed.in) cache.in = parsed.in;
if (parsed.yk) cache.yk = parsed.yk;
if (parsed.ao) cache.ao = parsed.ao;
if (parsed.param) cache.param = parsed.param;
lastDataCache['dc_data'] = cache;
if (currentHash === '#datacenter' && currentPage && currentPage.onData) {
currentPage.onData(parsed);
}
return;
}
} catch(e) {}
if (currentPage && currentPage.onBinary) {
currentPage.onBinary(raw);
}
return; return;
} }
@ -150,6 +171,22 @@ var App = (function() {
return; return;
} }
if (data.type === 'cmd_list') {
lastDataCache['cmd_list'] = data;
if (currentHash === '#terminal' && currentPage && currentPage.onData) {
currentPage.onData(data);
}
return;
}
if (data.type === 'dc_data') {
lastDataCache['dc_data'] = data;
if (currentPage && currentPage.onData && currentHash === '#datacenter') {
currentPage.onData(data);
}
return;
}
var signalTypes = ['out', 'in', 'yk', 'ao', 'param']; var signalTypes = ['out', 'in', 'yk', 'ao', 'param'];
for (var i = 0; i < signalTypes.length; i++) { for (var i = 0; i < signalTypes.length; i++) {
var st = signalTypes[i]; var st = signalTypes[i];
@ -183,6 +220,7 @@ var App = (function() {
registerPage('#yk', YkPage); registerPage('#yk', YkPage);
registerPage('#ao', AoPage); registerPage('#ao', AoPage);
registerPage('#param', ParamPage); registerPage('#param', ParamPage);
registerPage('#datacenter', DatacenterPage);
registerPage('#monitor', MonitorPage); registerPage('#monitor', MonitorPage);
registerPage('#terminal', TerminalPage); registerPage('#terminal', TerminalPage);

View File

@ -130,7 +130,8 @@ var DashboardPage = {
{ cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' }, { cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' },
{ cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' }, { cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' },
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' }, { cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' } { cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' }
]; ];
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>'; var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
@ -692,6 +693,7 @@ var MonitorPage = (function() {
var TerminalPage = (function() { var TerminalPage = (function() {
var history = []; var history = [];
var historyIdx = 0; var historyIdx = 0;
var cmdList = [];
function append(text, cls) { function append(text, cls) {
var el = document.getElementById('term-output'); var el = document.getElementById('term-output');
@ -703,6 +705,32 @@ var TerminalPage = (function() {
el.scrollTop = el.scrollHeight; 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 { return {
render: function(container) { render: function(container) {
history = []; history = [];
@ -710,6 +738,7 @@ var TerminalPage = (function() {
container.innerHTML = '' container.innerHTML = ''
+ '<div class="page-header"><h2>命令终端</h2><p>输入 RTU 命令Enter 发送,↑↓ 历史记录</p></div>' + '<div class="page-header"><h2>命令终端</h2><p>输入 RTU 命令Enter 发送,↑↓ 历史记录</p></div>'
+ '<div class="terminal-wrap">' + '<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-output" id="term-output"></div>'
+ '<div class="terminal-input-line">' + '<div class="terminal-input-line">'
+ '<span class="terminal-prompt">cmd &gt; </span>' + '<span class="terminal-prompt">cmd &gt; </span>'
@ -750,11 +779,22 @@ var TerminalPage = (function() {
} }
}; };
renderCmdList();
append('输入 help 查看所有命令', 'term-info'); append('输入 help 查看所有命令', 'term-info');
}, },
onData: function(data) { onData: function(data) {
if (data) append(data, 'term-resp'); 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) { onBinary: function(ab) {
@ -766,9 +806,150 @@ var TerminalPage = (function() {
onEnter: function() { onEnter: function() {
var input = document.getElementById('term-input'); var input = document.getElementById('term-input');
if (input) setTimeout(function() { input.focus(); }, 100); if (input) setTimeout(function() { input.focus(); }, 100);
renderCmdList();
if (cmdList.length === 0) {
WsClient.send({type:'get_cmds'});
}
}, },
onLeave: function() { 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 = '<tr class="empty-row"><td colspan="6">暂无数据</td></tr>';
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 = '<tr class="empty-row"><td colspan="6">无匹配结果</td></tr>';
return;
}
var html = '';
for (var i = 0; i < rows.length; i++) {
var item = rows[i];
html += '<tr>'
+ '<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;
}
function renderHeaders() {
var thead = document.getElementById('dc-thead');
if (!thead) return;
var html = '';
for (var i = 0; i < COLUMNS.length; i++) {
html += '<th>' + COLUMNS[i] + '</th>';
}
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 += '<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 = '';
document.getElementById('dc-search').value = '';
renderTabs();
renderHeaders();
renderTable();
};
}
}
return {
render: function(container) {
allData = {};
activeTab = 'out';
searchText = '';
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="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;
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();
}
}
};
})();

View File

@ -43,6 +43,7 @@ var WsClient = (function() {
clearTimeout(reconnectTimer); clearTimeout(reconnectTimer);
reconnectTimer = null; reconnectTimer = null;
} }
ws.send(JSON.stringify({ type: 'get_cmds' }));
}; };
ws.onclose = function() { ws.onclose = function() {