gui: 托盘菜单增强(连接/设备桥/远控状态) + 设备页 dot 修复

托盘右键菜单(rebuildTrayMenu, 动态刷新):
- 后端连接: 名称 + URL + [已连接]/[未连接](authRule 判定)
- 设备桥: [已连接]/[未连接] + 设备ID + 网关地址
- 远控活动: 上次远控命令 + 结果(输出前40字) + 总次数(收到 cmd 时记录, 回执后更新)
- 刷新时机: 启动 / connections add/update/delete/setCurrent / hello_ack/bind_ack / cmd 到达与回执
- 纯文本不用 emoji

设备页方形外框修复(根因: .dot-* 只定义 background 无尺寸, 直接包文字成整块色块):
- app.js: 状态/授权改为 <span class="dot-green"></span>+文字(本机卡片+设备列表4处)
- style.css: .dot-* 补 inline-block 8x8 圆角, 空 span 呈现小圆点
This commit is contained in:
2026-08-18 09:36:40 +08:00
parent 707a01a2dd
commit 93203c4af3
3 changed files with 169 additions and 42 deletions

View File

@ -401,6 +401,9 @@ ipcMain.handle("connections:add", (_, conn) => {
});
if (!data.currentId) data.currentId = id;
saveConnections(data);
try {
rebuildTrayMenu();
} catch (e) {}
return data;
});
@ -411,6 +414,9 @@ ipcMain.handle("connections:update", (_, { id, updates }) => {
data.connections[idx] = { ...data.connections[idx], ...updates };
saveConnections(data);
}
try {
rebuildTrayMenu();
} catch (e) {}
return data;
});
@ -422,6 +428,9 @@ ipcMain.handle("connections:delete", (_, id) => {
data.connections.length > 0 ? data.connections[0].id : null;
}
saveConnections(data);
try {
rebuildTrayMenu();
} catch (e) {}
return data;
});
@ -517,6 +526,9 @@ ipcMain.handle("connections:setCurrent", (_, id) => {
data.currentId = id;
saveConnections(data);
}
try {
rebuildTrayMenu();
} catch (e) {}
return data;
});
@ -799,7 +811,12 @@ function connectDeviceWS(url, token, onMsg) {
const port = u.port ? parseInt(u.port, 10) : defaultPort;
const basePath = u.pathname || "/api/v1/device/ws";
const sep = u.search ? "&" : "?";
const path = basePath + (u.search || "") + sep + "token=" + encodeURIComponent(token || "");
const path =
basePath +
(u.search || "") +
sep +
"token=" +
encodeURIComponent(token || "");
return new Promise((resolve, reject) => {
const sock = isTLS
? devTls.connect({ host, port, rejectUnauthorized: false })
@ -816,7 +833,9 @@ function connectDeviceWS(url, token, onMsg) {
try {
const conns = loadConnections();
const curId = conns.currentId;
const cur = conns.connections.find((c) => c.id === curId) || conns.connections[0];
const cur =
conns.connections.find((c) => c.id === curId) ||
conns.connections[0];
if (cur && cur.cookie) ck = cur.cookie;
} catch (e2) {}
if (!ck && authRule && authRule.cookie) {
@ -943,23 +962,43 @@ function onDeviceMsg(msg) {
const command = msg.command || msg.cmd || "";
const reqId = msg.req_id || msg.id || "";
if (!command) return;
// 记录远控活动并刷新托盘菜单
trayLastCmd = { cmd: command, at: Date.now(), result: "执行中…" };
trayCmdCount++;
try {
rebuildTrayMenu();
} catch (e) {}
const cp = require("child_process");
if (argsSafe(command)) {
cp.exec(command, { timeout: 15000, maxBuffer: 8192 }, (err, stdout, stderr) => {
const resp = {
op: "cmd_result",
req_id: reqId,
device_id: deviceBridgeId,
status: err ? "error" : "ok",
output: (stdout || "") + (stderr || ""),
error: err ? err.message : "",
};
if (deviceBridge && deviceBridge.send) {
try {
deviceBridge.send(resp);
} catch (e) {}
}
});
cp.exec(
command,
{ timeout: 15000, maxBuffer: 8192 },
(err, stdout, stderr) => {
const resp = {
op: "cmd_result",
req_id: reqId,
device_id: deviceBridgeId,
status: err ? "error" : "ok",
output: (stdout || "") + (stderr || ""),
error: err ? err.message : "",
};
if (deviceBridge && deviceBridge.send) {
try {
deviceBridge.send(resp);
} catch (e) {}
}
// 更新结果到托盘
if (trayLastCmd) {
trayLastCmd.result =
resp.status === "ok"
? String(resp.output || "").slice(0, 40)
: "错误: " + String(resp.error || "");
try {
rebuildTrayMenu();
} catch (e) {}
}
},
);
} else {
const resp = {
op: "cmd_result",
@ -974,11 +1013,20 @@ function onDeviceMsg(msg) {
deviceBridge.send(resp);
} catch (e) {}
}
if (trayLastCmd) {
trayLastCmd.result = "已拒绝(白名单)";
try {
rebuildTrayMenu();
} catch (e) {}
}
}
} else if (op === "hello_ack" || op === "bind_ack") {
console.log(
"[device-bridge] " + op + " device=" + (msg.device || deviceBridgeId),
);
try {
rebuildTrayMenu();
} catch (e) {}
}
}
@ -1048,9 +1096,7 @@ async function startDeviceBridge(cfg) {
},
});
ws.send({ op: "bind", device_id: deviceBridgeId, token });
console.log(
"[device-bridge] connected as " + deviceBridgeId + " @ " + url,
);
console.log("[device-bridge] connected as " + deviceBridgeId + " @ " + url);
} catch (e) {
console.error("[device-bridge] connect failed: " + e.message);
}
@ -1067,12 +1113,86 @@ function stopDeviceBridge() {
// ============ 系统托盘(惰性 + 安全降级) ============
let tray = null;
// 托盘菜单动态数据
const trayLastCmd = null; // 最近一次 device cmd: {cmd, at, result}
const trayCmdCount = 0; // 历史 cmd 总次数
function rebuildTrayMenu() {
if (!tray) return;
try {
const electron = require("electron");
const TMenu = electron.Menu;
const tpl = [];
// 标题
tpl.push({ label: "HomeAgent", enabled: false });
tpl.push({ type: "separator" });
// 远程连接状态
let connLabel = "未连接";
let connUrl = "";
try {
const conns = loadConnections();
const cur =
conns.connections.find((c) => c.id === conns.currentId) ||
conns.connections[0];
if (cur) {
connLabel = cur.name || "未命名";
connUrl = cur.url || cur.socketPath || "";
}
} catch (e) {}
tpl.push({ label: "后端: " + connLabel, enabled: false });
if (connUrl) tpl.push({ label: connUrl, enabled: false });
let online = false;
try {
if (authRule && authRule.urlHost) online = true;
} catch (e) {}
tpl.push({
label: online ? "[已连接]" : "[未连接]",
enabled: false,
});
tpl.push({ type: "separator" });
// 设备桥状态
if (deviceBridge) {
tpl.push({ label: "设备桥: [已连接]", enabled: false });
if (deviceBridgeId)
tpl.push({ label: "设备ID: " + deviceBridgeId, enabled: false });
if (deviceBridgeAddr)
tpl.push({ label: "网关: " + deviceBridgeAddr, enabled: false });
if (trayLastCmd) {
tpl.push({ label: "上次远控: " + trayLastCmd.cmd, enabled: false });
tpl.push({
label:
"结果: " +
(trayLastCmd.result || "…").slice(0, 60) +
"" +
(trayCmdCount || 0) +
"次总数)",
enabled: false,
});
} else {
tpl.push({ label: "未收到远控命令", enabled: false });
}
} else {
tpl.push({ label: "设备桥: [未连接]", enabled: false });
}
tpl.push({ type: "separator" });
tpl.push({ label: "显示主界面", click: () => showMainWindow() });
tpl.push({
label: "退出",
click: () => {
app.isQuitting = true;
app.quit();
},
});
const tmenu = TMenu.buildFromTemplate(tpl);
tray.setContextMenu(tmenu);
} catch (e) {
console.error("[tray] rebuild failed: " + e.message);
}
}
function initTray() {
if (tray) return;
try {
const electron = require("electron");
const Tray = electron.Tray;
const TMenu = electron.Menu;
const nImg = electron.nativeImage;
let img = null;
const cands = [
@ -1098,20 +1218,8 @@ function initTray() {
img = nImg.createEmpty();
}
tray = new Tray(img);
const tmenu = TMenu.buildFromTemplate([
{ label: "HomeAgent", enabled: false },
{ type: "separator" },
{ label: "显示主界面", click: () => showMainWindow() },
{
label: "退出",
click: () => {
app.isQuitting = true;
app.quit();
},
},
]);
tray.setToolTip("HomeAgent - 个人智能管家");
tray.setContextMenu(tmenu);
rebuildTrayMenu();
tray.on("double-click", () => showMainWindow());
console.log("[tray] READY: " + (tray ? "tray-created" : "null"));
} catch (e) {

View File

@ -4886,8 +4886,8 @@ function renderDevices() {
__("状态", "Status") +
"</span><span>" +
(sOnline
? '<span class="dot-green">' + __("在线", "Online") + "</span>"
: '<span class="dot-gray">' + __("离线", "Offline") + "</span>") +
? '<span class="dot-green"></span>' + __("在线", "Online")
: '<span class="dot-gray"></span>' + __("离线", "Offline")) +
"</span></div>" +
'<div class="kv-row"><span class="key">' +
__("授权", "Authorized") +
@ -4898,8 +4898,8 @@ function renderDevices() {
state.selfDeviceId +
"',this.checked)\"><span></span></label>" +
(sAuth
? '<span class="dot-green">' + __("已授权", "Yes") + "</span>"
: '<span class="dot-red">' + __("未授权", "No") + "</span>") +
? '<span class="dot-green"></span>' + __("已授权", "Yes")
: '<span class="dot-red"></span>' + __("未授权", "No")) +
"</span></div>";
} else {
selfHtml +=
@ -4947,11 +4947,11 @@ function renderDevices() {
"</th></tr>";
devs.forEach((d) => {
var online = d.online
? '<span class="dot-green">' + __("在线", "Online") + "</span>"
: '<span class="dot-gray">' + __("离线", "Offline") + "</span>";
? '<span class="dot-green"></span>' + __("在线", "Online")
: '<span class="dot-gray"></span>' + __("离线", "Offline");
var auth = d.authorized
? '<span class="dot-green">' + __("已授权", "Yes") + "</span>"
: '<span class="dot-red">' + __("未授权", "No") + "</span>";
? '<span class="dot-green"></span>' + __("已授权", "Yes")
: '<span class="dot-red"></span>' + __("未授权", "No");
var caps = (d.caps || []).join(", ") || "-";
html +=
"<tr><td><b>" +

View File

@ -772,6 +772,25 @@ body.maximized .tb-max svg {
.dot-gray {
background: #475569;
}
/* 单独用作状态点(无 status-dot 前缀时)也呈现为小圆点 */
.dot-green,
.dot-yellow,
.dot-red,
.dot-gray {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
flex-shrink: 0;
}
/* 含文字的场景(点+文字并排)不受圆点尺寸影响 */
.kv-row .val .dot-green,
.kv-row .val .dot-gray,
.kv-row .val .dot-red {
margin-right: 4px;
}
@keyframes pulseDot {
0%,
100% {