diff --git a/src/pPowerMangerHost/HostSim.cpp b/src/pPowerMangerHost/HostSim.cpp index 4d3d268..c79a874 100644 --- a/src/pPowerMangerHost/HostSim.cpp +++ b/src/pPowerMangerHost/HostSim.cpp @@ -111,6 +111,9 @@ bool HostSim::OnStartUp() m_webServer->setOnMessage([this](const string& msg) { this->handleWebMessage(msg); }); + m_webServer->setOnOpen([this]() { + return this->buildSnapshot(); + }); m_webServer->start(m_webPort); RegisterVariables(); diff --git a/src/pPowerMangerHost/WebServer.cpp b/src/pPowerMangerHost/WebServer.cpp index 6f65b89..c05dcb2 100644 --- a/src/pPowerMangerHost/WebServer.cpp +++ b/src/pPowerMangerHost/WebServer.cpp @@ -36,6 +36,12 @@ void WebServer::handleEvent(struct mg_connection* c, int ev, void* ev_data) { } else if (ev == MG_EV_WS_OPEN) { std::lock_guard lock(m_wsMutex); m_wsConnections.push_back(c); + if (m_onOpen) { + std::string welcome = m_onOpen(); + if (!welcome.empty()) { + mg_ws_send(c, welcome.c_str(), welcome.size(), WEBSOCKET_OP_TEXT); + } + } } else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) { std::lock_guard lock(m_wsMutex); for (auto it = m_wsConnections.begin(); it != m_wsConnections.end(); ++it) { @@ -66,6 +72,10 @@ void WebServer::setOnMessage(std::function handler) { m_onMessage = handler; } +void WebServer::setOnOpen(std::function handler) { + m_onOpen = handler; +} + bool WebServer::start(int port) { if (m_running) return true; m_port = port; diff --git a/src/pPowerMangerHost/WebServer.h b/src/pPowerMangerHost/WebServer.h index b2f94e5..0951541 100644 --- a/src/pPowerMangerHost/WebServer.h +++ b/src/pPowerMangerHost/WebServer.h @@ -24,6 +24,9 @@ public: // 收到网页 WS 消息时的回调 void setOnMessage(std::function handler); + // 新 WebSocket 客户端连接时回调,返回需要立即推送给该客户端的初始文本(如快照) + void setOnOpen(std::function handler); + private: static bool serverThreadCB(void* pParam); bool serverThreadFunc(); @@ -37,6 +40,7 @@ private: std::mutex m_wsMutex; std::vector m_wsConnections; std::function m_onMessage; + std::function m_onOpen; }; #endif // WEBSERVER_H diff --git a/src/pPowerMangerHost/web/index.html b/src/pPowerMangerHost/web/index.html index d12cb56..257e00f 100644 --- a/src/pPowerMangerHost/web/index.html +++ b/src/pPowerMangerHost/web/index.html @@ -14,6 +14,7 @@ --ok: #22c55e; --warn: #f59e0b; --err: #ef4444; + --muted: #64748b; } * { box-sizing: border-box; } @@ -33,46 +34,51 @@ background: var(--panel); border-bottom: 1px solid var(--border); } + header h1 { font-size: 16px; margin: 0; flex: 1; } - header h1 { font-size: 16px; margin: 0; } - - .dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--err); - } + .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--err); } .dot.on { background: var(--ok); } - main { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - padding: 16px; - max-width: 1500px; - margin: 0 auto; + nav#tabs { + display: flex; + gap: 4px; + padding: 8px 20px 0; + background: var(--panel); + border-bottom: 1px solid var(--border); + } + nav#tabs button { + background: transparent; + color: #94a3b8; + border: none; + border-bottom: 2px solid transparent; + padding: 8px 14px; + cursor: pointer; + font-size: 13px; + } + nav#tabs button:hover { color: var(--fg); } + nav#tabs button.active { + color: var(--acc); + border-bottom-color: var(--acc); } - @media (max-width: 900px) { - main { grid-template-columns: 1fr; } - } + main { padding: 16px; max-width: 1500px; margin: 0 auto; } + + .page { display: none; } + .page.active { display: block; } section { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px; + margin-bottom: 16px; } - section.wide { - margin: 0 16px 16px; - max-width: 1468px; - } - section h2 { font-size: 14px; margin: 0 0 10px; color: var(--acc); } + section h3 { font-size: 14px; margin: 10px 0 4px; color: #cbd5e1; } .row { display: flex; @@ -81,11 +87,8 @@ align-items: center; margin-bottom: 8px; } - - label { - font-size: 12px; - color: #94a3b8; - } + label { font-size: 12px; color: #94a3b8; } + .muted { color: var(--muted); font-size: 12px; } select, input, textarea, button { background: #0b1220; @@ -95,6 +98,11 @@ padding: 6px 8px; font-size: 13px; } + button { cursor: pointer; background: #0ea5e9; border-color: #0ea5e9; color: #fff; } + button:hover { filter: brightness(1.1); } + button.sec { background: #334155; border-color: #475569; } + button.danger { background: #b91c1c; border-color: #dc2626; } + button:disabled { opacity: .4; cursor: not-allowed; } textarea { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; @@ -103,17 +111,46 @@ resize: vertical; } - button { - cursor: pointer; - background: #0ea5e9; - border-color: #0ea5e9; - color: #fff; + /* 统计卡片 */ + .stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; } + .stat { + background: #0b1220; + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; } - button:hover { filter: brightness(1.1); } - button.sec { - background: #334155; - border-color: #475569; + .stat .stat-label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 4px; } + .stat .stat-value { font-size: 20px; font-weight: 600; } + .stat-value.ok { color: var(--ok); } + .stat-value.warn { color: var(--warn); } + .stat-value.err { color: var(--err); } + .stat-value.acc { color: var(--acc); } + + /* 状态徽章 */ + .badge { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + font-size: 13px; + border: 1px solid var(--border); } + .badge.on { color: var(--ok); border-color: var(--ok); } + .badge.off { color: #94a3b8; border-color: #475569; } + .badge.meter { color: var(--acc); border-color: var(--acc); } + .badge.power { color: #a78bfa; border-color: #a78bfa; } + .badge.standby { color: var(--warn); border-color: var(--warn); } + .badge.raw { color: var(--fg); } + + .breaker { + display: inline-block; + padding: 4px 10px; + margin: 2px; + border-radius: 6px; + font-size: 13px; + border: 1px solid var(--border); + } + .breaker.on { color: var(--ok); border-color: var(--ok); background: rgba(34,197,94,.08); } + .breaker.off { color: #64748b; } .chip { display: inline-block; @@ -125,40 +162,80 @@ font-size: 12px; } .chip:hover { border-color: var(--acc); } + .chip.active { background: #0ea5e9; border-color: #0ea5e9; color: #fff; } - .var-item { + /* 表格 */ + table.kv, table.dev-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + } + table.kv th, table.kv td, table.dev-table th, table.dev-table td { border: 1px solid var(--border); - border-radius: 8px; - margin-bottom: 8px; - overflow: hidden; + padding: 6px 10px; + text-align: left; + vertical-align: middle; } - .var-item summary { - cursor: pointer; - padding: 8px 10px; + table.kv th { color: #94a3b8; font-weight: 500; } + table.kv td.k { color: #cbd5e1; white-space: nowrap; } + table.kv td.v { font-family: ui-monospace, monospace; word-break: break-all; } + + .dev-group { margin-bottom: 10px; } + .dev-group table.dev-table select { width: 130px; } + + /* 反馈子标签 */ + nav.sub-tabs { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 12px; + } + nav.sub-tabs button { + background: #0b1220; + border: 1px solid var(--border); + color: #94a3b8; + border-radius: 999px; + padding: 5px 12px; font-size: 13px; - background: #16202f; - user-select: none; + cursor: pointer; } - .var-item pre { + nav.sub-tabs button:hover { color: var(--fg); border-color: var(--acc); } + nav.sub-tabs button.active { + background: #0ea5e9; + border-color: #0ea5e9; + color: #fff; + } + + .fb-head { + display: flex; + align-items: baseline; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); + } + .fb-head .fb-key { font-family: ui-monospace, monospace; font-size: 13px; color: var(--acc); } + .fb-head .fb-title { font-size: 15px; font-weight: 600; } + .fb-head .fb-time { margin-left: auto; color: #64748b; font-size: 12px; } + .fb-body .raw-toggle { margin-top: 10px; } + + pre { margin: 0; padding: 10px; - font-size: 12px; + font-size: 13px; overflow-x: auto; background: #0b1220; color: #a5f3fc; white-space: pre-wrap; word-break: break-all; - } - - .muted { - color: #64748b; - font-size: 12px; + border-radius: 6px; } #log { font-size: 12px; color: #94a3b8; - max-height: 180px; + max-height: 400px; overflow-y: auto; } #log .send { color: var(--acc); } @@ -166,13 +243,12 @@ #chart { width: 100%; - height: 280px; + height: 320px; display: block; background: #0b1220; border: 1px solid var(--border); border-radius: 8px; } - .serie { display: inline-flex; align-items: center; @@ -184,267 +260,323 @@ font-size: 12px; background: #0b1220; } - .serie .swatch { - width: 10px; - height: 10px; - border-radius: 2px; - display: inline-block; - } - .serie .rm { - cursor: pointer; - color: #94a3b8; - font-weight: bold; - } + .serie .swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; } + .serie .rm { cursor: pointer; color: #94a3b8; font-weight: bold; } .serie .rm:hover { color: var(--err); }
-

pPowerManger 上位机模拟 (Web UI)

+

pPowerManger 上位机模拟

连接中…
+ + +
-
-

指令下发

-
- - -
-
- - -
-
- -
- -
- - -
+ +
+
+

主控指令(外部通信 uExternComm_*)

+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
-

快捷指令

-
+
+

系统状态概览 (uPower_pmState_fb)

+
等待数据…
+
-

通用 MOOS 通知

-
- - - -
-
+
+

快捷指令

+
+ + + +
+

任务启动 (uPlan_taskStart_st)

+
+

通用 MOOS 通知

+
+ + + +
+
+ -
-

反馈监控(pPowerManger 发往上位机的全部消息)

-
-
-
+ +
+
+

设备指令 uPower_equState_cmd

+
+ + + + +
+
+
+
+
+ + +
+
+

反馈监控

+ +
+
+
+
+ + +
+
+

连续数据曲线

+
+ + + + +
+
+ +
+
+ + +
+
+

操作日志

+
+ +
+
+
+
-
-

连续数据曲线

-
- - - - -
-
- -
- -
-

操作日志

-
-
-