work-order-monitor/src/composables/useTray.ts

280 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* useTray.ts — 系统托盘 Composable
*
* 负责创建、更新系统托盘图标与右键菜单。
* 图标颜色根据监测状态动态变化:
* 灰色 — 未开始监测
* 橙色 — 监测已暂停/推迟
* 红色 — 有待处理工单
* 绿色 — 监测中,无异常
*/
import { Image } from "@tauri-apps/api/image";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { TrayIcon } from "@tauri-apps/api/tray";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { openUrl } from "@tauri-apps/plugin-opener";
import { type Ref } from "vue";
import { CRM_URL } from "@/constants/app";
import { useLogStore } from "@/stores/log";
import { type TicketCounts } from "@/stores/monitor";
import { getErrorMessage } from "@/utils/common";
/**
* @param isMonitoring 是否正在监测
* @param isPostponed 是否已推迟/暂停
* @param ticketCounts 当前工单计数
* @param startMonitoring 开始监测(托盘菜单调用)
* @param stopMonitoring 停止监测(托盘菜单调用)
*/
export function useTray(
isMonitoring: Ref<boolean>,
isPostponed: Ref<boolean>,
ticketCounts: Ref<TicketCounts>,
startMonitoring: () => Promise<void>,
stopMonitoring: () => void,
) {
const { addLog } = useLogStore();
const appName = import.meta.env.VITE_APP_NAME;
// 当前托盘图标实例null 表示尚未初始化
let trayIconInstance: TrayIcon | null = null;
// SVG 图片缓存,避免重复加载
let cachedSvgImg: HTMLImageElement | null = null;
/**
* 加载 /gong.svg 并缓存,后续调用直接返回缓存结果。
*/
async function loadSvgImage(): Promise<HTMLImageElement> {
if (cachedSvgImg) return cachedSvgImg;
return new Promise((resolve, reject) => {
const img = new window.Image();
img.onload = () => {
cachedSvgImg = img;
resolve(img);
};
img.onerror = reject;
img.src = "/gong.svg";
});
}
/**
* 将 SVG 渲染到 Canvas对每个像素按亮度重新着色后返回 RGBA 原始数据。
*
* 着色规则:
* - 暗像素(图标笔画,亮度 < 128→ 白色,确保图标在彩色背景上清晰可见
* - 亮像素(背景) → 传入的 (r, g, b) 状态色
*
* SVG 加载失败时回退到纯色背景 + "工" 字文本方案。
*
* @param r 状态色红分量 (0-255)
* @param g 状态色绿分量 (0-255)
* @param b 状态色蓝分量 (0-255)
* @param size 图标边长(像素),默认 32
*/
async function makeTrayIconRGBA(
r: number,
g: number,
b: number,
size = 32,
): Promise<Uint8Array> {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;
try {
const svgImg = await loadSvgImage();
ctx.drawImage(svgImg, 0, 0, size, size);
const imageData = ctx.getImageData(0, 0, size, size);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// 根据亮度判断:暗像素(文字) → 白色,亮像素(背景) → 状态色
const brightness =
data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
if (brightness < 128) {
data[i] = data[i + 1] = data[i + 2] = 255;
} else {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
}
data[i + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
} catch {
// SVG 加载失败,回退到纯 Canvas 绘制
addLog("WARN", "SVG 图标加载失败,使用回退方案", "SYSTEM");
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = "#ffffff";
ctx.font = `bold ${Math.round(size * 0.62)}px "Microsoft YaHei","SimHei",sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("工", size / 2, size / 2 + 1);
}
const finalData = ctx.getImageData(0, 0, size, size);
return new Uint8Array(finalData.data.buffer);
}
/**
* 根据当前应用状态更新托盘图标颜色与 tooltip 文字。
*
* 状态优先级(高 → 低):
* 未监测 > 已暂停 > 有工单 > 正常监测中
*/
async function updateTrayIcon() {
if (!trayIconInstance) return;
try {
const hasTickets =
ticketCounts.value.pending > 0 ||
ticketCounts.value.stayclose > 0 ||
ticketCounts.value.confirm > 0 ||
ticketCounts.value.workOrderCount > 0;
let color: [number, number, number];
let tooltip: string;
if (!isMonitoring.value) {
// 未开始监测 — 灰色
color = [158, 158, 158];
tooltip = `${appName} - 未开始`;
} else if (isPostponed.value) {
// 已暂停/推迟 — 橙色
color = [255, 152, 0];
tooltip = `${appName} - 已暂停`;
} else if (hasTickets) {
// 有待处理工单 — 红色
const total =
ticketCounts.value.pending +
ticketCounts.value.stayclose +
ticketCounts.value.confirm +
ticketCounts.value.workOrderCount;
color = [244, 67, 54];
tooltip = `${appName} - 有 ${total} 条工单待处理!`;
} else {
// 监测中,无异常 — 绿色
color = [76, 175, 80];
tooltip = `${appName} - 监测中`;
}
const icon = await Image.new(await makeTrayIconRGBA(...color), 32, 32);
await trayIconInstance.setIcon(icon);
await trayIconInstance.setTooltip(tooltip);
} catch (e: unknown) {
addLog("WARN", `更新托盘图标失败: ${getErrorMessage(e)}`, "SYSTEM");
}
}
/**
* 构建托盘右键菜单,包含以下菜单项:
* - 显示面板:还原并聚焦主窗口
* - 开始/停止监测:根据当前状态切换
* - 打开工单网站:在默认浏览器中打开
* - 退出:销毁主窗口
*/
async function buildTrayMenu() {
const mainWindow = getCurrentWindow();
return await Menu.new({
items: [
await MenuItem.new({
id: "show",
text: "显示面板",
action: async () => {
await mainWindow.unminimize();
await mainWindow.show();
await mainWindow.setFocus();
},
}),
await MenuItem.new({
id: "toggle_monitor",
text: isMonitoring.value ? "停止监测" : "开始监测",
action: async () => {
if (isMonitoring.value) {
stopMonitoring();
} else {
// 开始监测前先还原窗口,确保登录流程可见
await mainWindow.unminimize();
await mainWindow.show();
await mainWindow.setFocus();
await startMonitoring();
}
},
}),
await MenuItem.new({
id: "open_website",
text: "打开工单网站",
action: async () => {
await openUrl(CRM_URL);
},
}),
await MenuItem.new({
id: "quit",
text: "退出",
action: async () => {
await mainWindow.destroy();
},
}),
],
});
}
/**
* 重新构建并替换托盘右键菜单。
* 在监测状态变化后调用,以同步"开始/停止监测"等动态菜单文字。
*/
async function updateTrayMenu() {
if (!trayIconInstance) return;
try {
const menu = await buildTrayMenu();
await trayIconInstance.setMenu(menu);
} catch (e: unknown) {
addLog("WARN", `更新托盘菜单失败: ${getErrorMessage(e)}`, "SYSTEM");
}
}
/**
* 初始化系统托盘:创建图标(初始灰色)、注册右键菜单、绑定左键单击事件。
* 应在应用启动时调用一次。
*/
async function setupTray() {
try {
const menu = await buildTrayMenu();
// 初始状态:灰色(未开始)
const icon = await Image.new(
await makeTrayIconRGBA(158, 158, 158),
32,
32,
);
trayIconInstance = await TrayIcon.new({
icon,
tooltip: `${appName} - 未开始`,
menu,
menuOnLeftClick: false,
action: async (event) => {
// 左键单击:还原并聚焦主窗口
if (event.type === "Click" && event.button === "Left") {
const win = getCurrentWindow();
await win.unminimize();
await win.show();
await win.setFocus();
}
},
});
addLog("INFO", "系统托盘创建成功", "SYSTEM");
} catch (e: unknown) {
addLog("ERROR", `系统托盘创建失败: ${getErrorMessage(e)}`, "SYSTEM");
}
}
/** 隐藏主窗口(最小化到系统托盘)。 */
async function minimizeToTray() {
await getCurrentWindow().hide();
}
return { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray };
}