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

279 lines
8.8 KiB
TypeScript
Raw Normal View History

2026-03-31 17:08:30 +08:00
/**
* 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";
/**
* @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: any) {
addLog("WARN", `更新托盘图标失败: ${e.message || 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: any) {
addLog("WARN", `更新托盘菜单失败: ${e.message || 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: any) {
addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM");
}
}
/** 隐藏主窗口(最小化到系统托盘)。 */
async function minimizeToTray() {
await getCurrentWindow().hide();
}
return { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray };
}