feat(web): show WS connection status indicator in topbar

Green dot + "已连接" when socket is open; red dot + "连接断开,重连中…"
on close/error. Reconnect timer (2s) already in place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-25 12:41:50 +08:00
parent b832d98196
commit b28ac23520
5 changed files with 35 additions and 3 deletions

View File

@ -7,6 +7,9 @@
<div class="topbar-actions"> <div class="topbar-actions">
<button type="button" class="secondary" id="clearEquipmentFilter">设备筛选: 全部</button> <button type="button" class="secondary" id="clearEquipmentFilter">设备筛选: 全部</button>
<button type="button" class="secondary" id="openApiDoc">API.md</button> <button type="button" class="secondary" id="openApiDoc">API.md</button>
<div class="status" id="statusText">Ready</div> <div class="status" id="statusText">
<span class="ws-dot" id="wsDot"></span>
<span id="wsLabel">连接中…</span>
</div>
</div> </div>
</header> </header>

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PLC Control</title> <title>PLC Control</title>
<link rel="stylesheet" href="/ui/styles.css?v=20260325b" /> <link rel="stylesheet" href="/ui/styles.css?v=20260325c" />
</head> </head>
<body> <body>
<div data-partial="/ui/html/topbar.html"></div> <div data-partial="/ui/html/topbar.html"></div>
@ -22,6 +22,6 @@
<div data-partial="/ui/html/modals.html"></div> <div data-partial="/ui/html/modals.html"></div>
<div data-partial="/ui/html/api-doc-drawer.html"></div> <div data-partial="/ui/html/api-doc-drawer.html"></div>
<script type="module" src="/ui/js/index.js?v=20260325b"></script> <script type="module" src="/ui/js/index.js?v=20260325c"></script>
</body> </body>
</html> </html>

View File

@ -2,6 +2,8 @@ const byId = (id) => document.getElementById(id);
export const dom = { export const dom = {
statusText: byId("statusText"), statusText: byId("statusText"),
wsDot: byId("wsDot"),
wsLabel: byId("wsLabel"),
tabOps: byId("tabOps"), tabOps: byId("tabOps"),
tabConfig: byId("tabConfig"), tabConfig: byId("tabConfig"),
opsUnitList: byId("opsUnitList"), opsUnitList: byId("opsUnitList"),

View File

@ -55,11 +55,22 @@ export function stopLogs() {
} }
} }
function setWsStatus(connected) {
if (dom.wsDot) {
dom.wsDot.className = `ws-dot ${connected ? "connected" : "disconnected"}`;
}
if (dom.wsLabel) {
dom.wsLabel.textContent = connected ? "已连接" : "连接断开,重连中…";
}
}
export function startPointSocket() { export function startPointSocket() {
const protocol = location.protocol === "https:" ? "wss" : "ws"; const protocol = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`); const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);
state.pointSocket = ws; state.pointSocket = ws;
ws.onopen = () => setWsStatus(true);
ws.onmessage = (event) => { ws.onmessage = (event) => {
try { try {
const payload = JSON.parse(event.data); const payload = JSON.parse(event.data);
@ -110,6 +121,9 @@ export function startPointSocket() {
}; };
ws.onclose = () => { ws.onclose = () => {
setWsStatus(false);
window.setTimeout(startPointSocket, 2000); window.setTimeout(startPointSocket, 2000);
}; };
ws.onerror = () => setWsStatus(false);
} }

View File

@ -51,7 +51,20 @@ body {
.status { .status {
font-size: 12px; font-size: 12px;
color: var(--text-3); color: var(--text-3);
display: flex;
align-items: center;
gap: 5px;
} }
.ws-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-3);
flex-shrink: 0;
transition: background 0.3s;
}
.ws-dot.connected { background: #22c55e; }
.ws-dot.disconnected { background: #ef4444; }
.topbar-actions { .topbar-actions {
display: flex; display: flex;