<新增> 1、前端对应增加命令终端与数据监控,命令终端支持自定义命令
This commit is contained in:
parent
6ef960525c
commit
742d9b36a0
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 > </span>'
|
+ '<span class="terminal-prompt">cmd > </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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue