mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
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:
176
cmd/gui/main.js
176
cmd/gui/main.js
@ -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) {
|
||||
|
||||
@ -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>" +
|
||||
|
||||
@ -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% {
|
||||
|
||||
Reference in New Issue
Block a user