From 92055dd6a0dba399a4eebb2a54f7ae8bcddc51c2 Mon Sep 17 00:00:00 2001 From: tsl Date: Mon, 23 Mar 2026 09:10:32 +0800 Subject: [PATCH] =?UTF-8?q?feat():=20=E6=B7=BB=E5=8A=A0=E5=BE=85=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- CLAUDE.md | 4 +- README_APP.md | 6 +- data/GetWorkOrderListPage.js | 100 +++++++ package-lock.json | 4 +- package.json | 2 +- public/gong.svg | 8 + src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/capabilities/default.json | 1 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3524 -> 407 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 14102 -> 555 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3524 -> 407 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 3377 -> 401 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 9081 -> 404 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3377 -> 401 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 7971 -> 387 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 18900 -> 637 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7971 -> 387 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 12392 -> 467 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 29506 -> 1051 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 12392 -> 467 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 16751 -> 641 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 40510 -> 1526 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16751 -> 641 bytes src-tauri/icons/128x128.png | Bin 3512 -> 452 bytes src-tauri/icons/128x128@2x.png | Bin 7012 -> 824 bytes src-tauri/icons/32x32.png | Bin 974 -> 353 bytes src-tauri/icons/64x64.png | Bin 0 -> 440 bytes src-tauri/icons/Square107x107Logo.png | Bin 2863 -> 358 bytes src-tauri/icons/Square142x142Logo.png | Bin 3858 -> 488 bytes src-tauri/icons/Square150x150Logo.png | Bin 3966 -> 522 bytes src-tauri/icons/Square284x284Logo.png | Bin 7737 -> 929 bytes src-tauri/icons/Square30x30Logo.png | Bin 903 -> 340 bytes src-tauri/icons/Square310x310Logo.png | Bin 8591 -> 1015 bytes src-tauri/icons/Square44x44Logo.png | Bin 1299 -> 381 bytes src-tauri/icons/Square71x71Logo.png | Bin 2011 -> 324 bytes src-tauri/icons/Square89x89Logo.png | Bin 2468 -> 364 bytes src-tauri/icons/StoreLogo.png | Bin 1523 -> 405 bytes src-tauri/icons/icon.icns | Bin 98451 -> 15291 bytes src-tauri/icons/icon.ico | Bin 86642 -> 3370 bytes src-tauri/icons/icon.png | Bin 14183 -> 1969 bytes src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 283 bytes src-tauri/icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 372 bytes src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 372 bytes src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 421 bytes src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 342 bytes src-tauri/icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 435 bytes src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 435 bytes src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 366 bytes src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 372 bytes src-tauri/icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 421 bytes src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 421 bytes src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 429 bytes src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 5821 bytes src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 429 bytes src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 577 bytes src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 347 bytes src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 487 bytes src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 543 bytes src-tauri/src/lib.rs | 2 +- src-tauri/src/notification.rs | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.vue | 10 +- src/App2.vue | 71 ----- src/components/LoginForm.vue | 4 +- src/components/MonitorControl.vue | 62 +++- src/components/SettingsPanel.vue | 101 ++++--- src/components/StatusHeader.vue | 2 +- src/composables/useMonitor.ts | 67 ++++- src/stores/monitor.ts | 281 ++++++++++++++---- 71 files changed, 541 insertions(+), 195 deletions(-) create mode 100644 data/GetWorkOrderListPage.js create mode 100644 public/gong.svg create mode 100644 src-tauri/icons/64x64.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png delete mode 100644 src/App2.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 41097e6..fdb0cdf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -38,7 +38,8 @@ "Bash(start \"\" \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\\\\tauri-app_0.1.0_x64-setup.exe\")", "Bash(pnpm run build)", "Bash(pnpm add pinia)", - "Bash(pnpm add dayjs)" + "Bash(pnpm add dayjs)", + "Bash(pnpm tauri icon public/gong.svg)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 4e7843b..d507e16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -这是一个基于 **Tauri 2.x + Vue 3** 的工单监控桌面应用,用于监控 CRM 系统(crm.yunvip123.com)的待处理工单并发送系统通知。 +这是一个基于 **Tauri 2.x + Vue 3** 的工单监测桌面应用,用于监测 CRM 系统(crm.yunvip123.com)的待处理工单并发送系统通知。 ## 常用命令 @@ -33,7 +33,7 @@ cd src-tauri && cargo clippy **前端驱动架构**:所有业务逻辑(API 调用、定时任务、通知)都在 Vue 前端实现,Rust 后端仅负责系统级功能。 ### 前端(src/) -- `src/App.vue` - 主界面,包含全部业务逻辑:用户认证、监控调度、通知、网络检测、配置管理、系统托盘 +- `src/App.vue` - 主界面,包含全部业务逻辑:用户认证、监测调度、通知、网络检测、配置管理、系统托盘 - 使用 Vue 3 Composition API(` @@ -43,7 +49,7 @@ onUnmounted(() => store.cleanup()); diff --git a/src/App2.vue b/src/App2.vue deleted file mode 100644 index 9686596..0000000 --- a/src/App2.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/LoginForm.vue b/src/components/LoginForm.vue index 0d38e1e..a2487fe 100644 --- a/src/components/LoginForm.vue +++ b/src/components/LoginForm.vue @@ -55,10 +55,10 @@ const store = useMonitorStore(); 保存设置 - 开始监控 + 开始监测 - 停止监控 + 停止监测 diff --git a/src/components/MonitorControl.vue b/src/components/MonitorControl.vue index f90430c..e6ad3f1 100644 --- a/src/components/MonitorControl.vue +++ b/src/components/MonitorControl.vue @@ -1,4 +1,7 @@ - {{ store.isPostponed ? "已推迟1小时" : "监控中" }} + {{ store.isPostponed ? "已推迟1小时" : "监测中" }} ([]); const ticketCounts = ref({ - pending: 0, stayclose: 0, confirm: 0, lastCheck: "", + pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "", }); const networkStatus = ref({ is_connected: false, @@ -172,6 +174,37 @@ export function useMonitor() { } } + async function apiCheckWorkOrders(): Promise { + try { + const resp = await fetch(WORK_ORDER_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `ASP.NET_SessionId=${sessionId}`, + }, + body: JSON.stringify({ + isSelect: 8, + isShow: 3, + IsExport: 0, + PageIndex: 1, + PageSize: 20, + }), + }); + const result = await resp.json(); + if (result.success && result.data) { + const count = result.data.DataCount || 0; + addLog("INFO", `待审核检查完成 - 数据条数: ${count}`, "SCHEDULER"); + return count; + } else { + addLog("WARN", `待审核 API 返回异常: ${result.msg}`, "SCHEDULER"); + return 0; + } + } catch (e: any) { + addLog("ERROR", `待审核 API 检查失败: ${e.message || e}`, "SCHEDULER"); + return 0; + } + } + async function apiCheckStatus(isRetry = false): Promise { if (!userGid) { addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER"); @@ -193,16 +226,18 @@ export function useMonitor() { const pending = d.PendingCount || 0; const stayclose = d.StaycloseCount || 0; const confirm = d.ConfirmCount || 0; + const workOrderCount = await apiCheckWorkOrders(); ticketCounts.value = { - pending, stayclose, confirm, + pending, stayclose, confirm, workOrderCount, lastCheck: formatTime(new Date()), }; - addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER"); - if (pending > 0 || stayclose > 0 || confirm > 0) { + addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER"); + if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) { const msgs: string[] = []; if (pending > 0) msgs.push(`待处理工单: ${pending}`); if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`); if (confirm > 0) msgs.push(`待确认工单: ${confirm}`); + if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`); await notify("工单提醒", msgs.join(",")); } } else { @@ -362,7 +397,7 @@ export function useMonitor() { message.value = "日志导出成功"; } - // ===== 监控控制 ===== + // ===== 监测控制 ===== async function startMonitoring() { if (!username.value.trim() || !password.value.trim()) { message.value = "请先设置用户名和密码"; @@ -371,15 +406,15 @@ export function useMonitor() { isLoading.value = true; const ok = await apiLogin(); if (!ok) { - message.value = "登录失败,无法开始监控"; + message.value = "登录失败,无法开始监测"; isLoading.value = false; return; } isMonitoring.value = true; await apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); - addLog("INFO", "监控已启动", "MONITOR"); - message.value = `监控已开始,欢迎 ${userName}!`; + addLog("INFO", "监测已启动", "MONITOR"); + message.value = `监测已开始,欢迎 ${userName}!`; isLoading.value = false; } @@ -391,8 +426,8 @@ export function useMonitor() { isLoggedIn.value = false; sessionId = ""; userGid = ""; - addLog("INFO", "监控已停止", "MONITOR"); - message.value = "监控已停止"; + addLog("INFO", "监测已停止", "MONITOR"); + message.value = "监测已停止"; } function postponeMonitoring() { @@ -400,20 +435,20 @@ export function useMonitor() { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); } isPostponed.value = true; - addLog("INFO", "监控已推迟1小时", "MONITOR"); - message.value = "监控已推迟1小时,将在1小时后自动恢复"; + addLog("INFO", "监测已推迟1小时", "MONITOR"); + message.value = "监测已推迟1小时,将在1小时后自动恢复"; postponeTimer = setTimeout(() => { isPostponed.value = false; postponeTimer = null; apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); - addLog("INFO", "推迟结束,监控已自动恢复", "MONITOR"); - message.value = "推迟结束,监控已自动恢复"; + addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR"); + message.value = "推迟结束,监测已自动恢复"; }, 60 * 60 * 1000); } async function manualCheck() { - if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; } + if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; } isLoading.value = true; await apiCheckStatus(); message.value = "手动检查完成"; @@ -522,7 +557,7 @@ export function useMonitor() { checkNetworkStatus(); if (config.value.auto_monitor && rememberPassword.value && username.value && password.value) { - addLog("INFO", "自动登录并开始监控...", "SYSTEM"); + addLog("INFO", "自动登录并开始监测...", "SYSTEM"); await startMonitoring(); } }); diff --git a/src/stores/monitor.ts b/src/stores/monitor.ts index 91ef6a7..3ece053 100644 --- a/src/stores/monitor.ts +++ b/src/stores/monitor.ts @@ -1,5 +1,6 @@ -import { ref } from "vue"; -import { defineStore } from "pinia"; +import { ref, watch } from "vue"; +import dayjs from "dayjs"; +import { defineStore, acceptHMRUpdate } from "pinia"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { TrayIcon } from "@tauri-apps/api/tray"; import { Image } from "@tauri-apps/api/image"; @@ -16,12 +17,13 @@ import { } from "@tauri-apps/plugin-autostart"; import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { resolveResource } from "@tauri-apps/api/path"; +import { openUrl } from "@tauri-apps/plugin-opener"; export const APP_NAME = "工单系统监测"; const API_BASE = "https://crm.yunvip123.com/api"; const LOGIN_URL = `${API_BASE}/SystemUser/Login`; const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`; +const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`; export interface LogEntry { timestamp: string; @@ -34,6 +36,7 @@ export interface TicketCounts { pending: number; stayclose: number; confirm: number; + workOrderCount: number; lastCheck: string; } @@ -52,7 +55,7 @@ export const useMonitorStore = defineStore("monitor", () => { const autoStartEnabled = ref(false); const logs = ref([]); const ticketCounts = ref({ - pending: 0, stayclose: 0, confirm: 0, lastCheck: "", + pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "", }); const networkStatus = ref({ is_connected: false, @@ -64,6 +67,7 @@ export const useMonitorStore = defineStore("monitor", () => { check_interval: 60, auto_start: false, auto_monitor: false, + silent_start: false, show_notifications: true, notification_sound: true, network_timeout: 30, @@ -71,6 +75,7 @@ export const useMonitorStore = defineStore("monitor", () => { handle_pause_minutes: 20, }); const showCloseDialog = ref(false); + const postponeResumeTime = ref(null); // ===== 内部状态 ===== let sessionId = ""; @@ -80,10 +85,11 @@ export const useMonitorStore = defineStore("monitor", () => { let postponeTimer: ReturnType | null = null; let unlistenNotification: UnlistenFn | null = null; let unlistenClose: UnlistenFn | null = null; + let trayIconInstance: TrayIcon | null = null; // ===== 工具函数 ===== function formatTime(date: Date): string { - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`; + return dayjs(date).format("YYYY-MM-DD HH:mm:ss"); } function addLog(level: string, msg: string, category: string) { @@ -179,6 +185,37 @@ export const useMonitorStore = defineStore("monitor", () => { } } + async function apiCheckWorkOrders(): Promise { + try { + const resp = await fetch(WORK_ORDER_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `ASP.NET_SessionId=${sessionId}`, + }, + body: JSON.stringify({ + isSelect: 8, + isShow: 3, + IsExport: 0, + PageIndex: 1, + PageSize: 20, + }), + }); + const result = await resp.json(); + if (result.success && result.data) { + const count = result.data.DataCount || 0; + addLog("INFO", `待审核检查完成 - 数据条数: ${count}`, "SCHEDULER"); + return count; + } else { + addLog("WARN", `待审核 API 返回异常: ${result.msg}`, "SCHEDULER"); + return 0; + } + } catch (e: any) { + addLog("ERROR", `待审核 API 检查失败: ${e.message || e}`, "SCHEDULER"); + return 0; + } + } + async function apiCheckStatus(isRetry = false): Promise { if (!userGid && loginMode.value === "password") { addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER"); @@ -202,16 +239,18 @@ export const useMonitorStore = defineStore("monitor", () => { const pending = d.PendingCount || 0; const stayclose = d.StaycloseCount || 0; const confirm = d.ConfirmCount || 0; + const workOrderCount = await apiCheckWorkOrders(); ticketCounts.value = { - pending, stayclose, confirm, + pending, stayclose, confirm, workOrderCount, lastCheck: formatTime(new Date()), }; - addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER"); - if (pending > 0 || stayclose > 0 || confirm > 0) { + addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER"); + if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) { const msgs: string[] = []; if (pending > 0) msgs.push(`待处理工单: ${pending}`); if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`); if (confirm > 0) msgs.push(`待确认工单: ${confirm}`); + if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`); await notify("工单提醒", msgs.join(",")); } } else { @@ -234,8 +273,8 @@ export const useMonitorStore = defineStore("monitor", () => { async function tryReLogin(): Promise { if (loginMode.value === "token") { - addLog("ERROR", "Token 已失效,监控已停止,请更换新的 Token 后重新开始", "SESSION"); - message.value = "Token 已失效,监控已停止"; + addLog("ERROR", "Token 已失效,监测已停止,请更换新的 Token 后重新开始", "SESSION"); + message.value = "Token 已失效,监测已停止"; stopMonitoring(); return; } @@ -392,7 +431,7 @@ export const useMonitorStore = defineStore("monitor", () => { message.value = "日志导出成功"; } - // ===== 监控控制 ===== + // ===== 监测控制 ===== async function startMonitoring() { if (loginMode.value === "password") { if (!username.value.trim() || !password.value.trim()) { @@ -402,7 +441,7 @@ export const useMonitorStore = defineStore("monitor", () => { isLoading.value = true; const ok = await apiLogin(); if (!ok) { - message.value = "登录失败,无法开始监控"; + message.value = "登录失败,无法开始监测"; isLoading.value = false; return; } @@ -419,10 +458,10 @@ export const useMonitorStore = defineStore("monitor", () => { isMonitoring.value = true; await apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); - addLog("INFO", "监控已启动", "MONITOR"); + addLog("INFO", "监测已启动", "MONITOR"); message.value = loginMode.value === "password" - ? `监控已开始,欢迎 ${userName}!` - : "监控已开始(Token 登录)"; + ? `监测已开始,欢迎 ${userName}!` + : "监测已开始(Token 登录)"; isLoading.value = false; } @@ -430,12 +469,13 @@ export const useMonitorStore = defineStore("monitor", () => { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; } isPostponed.value = false; + postponeResumeTime.value = null; isMonitoring.value = false; isLoggedIn.value = false; sessionId = ""; userGid = ""; - addLog("INFO", "监控已停止", "MONITOR"); - message.value = "监控已停止"; + addLog("INFO", "监测已停止", "MONITOR"); + message.value = "监测已停止"; } function postponeMonitoring() { @@ -443,15 +483,17 @@ export const useMonitorStore = defineStore("monitor", () => { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); } isPostponed.value = true; - addLog("INFO", "监控已推迟1小时", "MONITOR"); - message.value = "监控已推迟1小时,将在1小时后自动恢复"; + postponeResumeTime.value = Date.now() + 60 * 60 * 1000; + addLog("INFO", "监测已推迟1小时", "MONITOR"); + message.value = "监测已推迟1小时,将在1小时后自动恢复"; postponeTimer = setTimeout(() => { isPostponed.value = false; + postponeResumeTime.value = null; postponeTimer = null; apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); - addLog("INFO", "推迟结束,监控已自动恢复", "MONITOR"); - message.value = "推迟结束,监控已自动恢复"; + addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR"); + message.value = "推迟结束,监测已自动恢复"; }, 60 * 60 * 1000); } @@ -461,20 +503,33 @@ export const useMonitorStore = defineStore("monitor", () => { if (postponeTimer) { clearTimeout(postponeTimer); } isPostponed.value = true; const minutes = config.value.handle_pause_minutes; - addLog("INFO", `点击"去处理",监控已暂停 ${minutes} 分钟`, "MONITOR"); - message.value = `监控已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`; + postponeResumeTime.value = Date.now() + minutes * 60 * 1000; + addLog("INFO", `点击"去处理",监测已暂停 ${minutes} 分钟`, "MONITOR"); + message.value = `监测已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`; postponeTimer = setTimeout(() => { isPostponed.value = false; + postponeResumeTime.value = null; postponeTimer = null; apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); - addLog("INFO", "暂停结束,监控已自动恢复", "MONITOR"); - message.value = "暂停结束,监控已自动恢复"; + addLog("INFO", "暂停结束,监测已自动恢复", "MONITOR"); + message.value = "暂停结束,监测已自动恢复"; }, minutes * 60 * 1000); } + function resumeMonitoring() { + if (!isMonitoring.value || !isPostponed.value) return; + if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; } + isPostponed.value = false; + postponeResumeTime.value = null; + apiCheckStatus(); + monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); + addLog("INFO", "手动恢复监测", "MONITOR"); + message.value = "监测已恢复"; + } + async function manualCheck() { - if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; } + if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; } isLoading.value = true; await apiCheckStatus(); message.value = "手动检查完成"; @@ -507,41 +562,143 @@ export const useMonitorStore = defineStore("monitor", () => { } // ===== 系统托盘 ===== - async function setupTray() { + + /** 用 Canvas 渲染彩色圆形背景 + 白色"工"字,返回 RGBA 像素数据 */ + function makeTrayIconRGBA(r: number, g: number, b: number, size = 32): Uint8Array { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d")!; + + // 圆形背景 + ctx.clearRect(0, 0, size, size); + ctx.beginPath(); + ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = `rgb(${r},${g},${b})`; + ctx.fill(); + + // 白色"工"字 + 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 imageData = ctx.getImageData(0, 0, size, size); + return new Uint8Array(imageData.data.buffer); + } + + /** 根据当前应用状态更新托盘图标和提示文字 */ + async function updateTrayIcon() { + if (!trayIconInstance) return; try { - const mainWindow = getCurrentWindow(); - const menu = await Menu.new({ - items: [ - await MenuItem.new({ - id: "show", - text: "显示面板", - action: async () => { + 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 = `${APP_NAME} - 未开始`; + } else if (isPostponed.value) { + // 已暂停/推迟 — 橙色 + color = [255, 152, 0]; + tooltip = `${APP_NAME} - 已暂停`; + } else if (hasTickets) { + // 有待处理工单 — 红色 + const total = ticketCounts.value.pending + ticketCounts.value.stayclose + ticketCounts.value.confirm + ticketCounts.value.workOrderCount; + color = [244, 67, 54]; + tooltip = `${APP_NAME} - 有 ${total} 条工单待处理!`; + } else { + // 监测中,无异常 — 绿色 + color = [76, 175, 80]; + tooltip = `${APP_NAME} - 监测中`; + } + + const icon = await Image.new(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 MenuItem.new({ - id: "quit", - text: "退出", - action: async () => { await mainWindow.destroy(); }, - }), - ], - }); + await startMonitoring(); + } + }, + }), + await MenuItem.new({ + id: "open_website", + text: "打开工单网站", + action: async () => { + await openUrl("https://crm.yunvip123.com"); + }, + }), + await MenuItem.new({ + id: "quit", + text: "退出", + action: async () => { await mainWindow.destroy(); }, + }), + ], + }); + } - const iconPath = await resolveResource("icons/32x32.png"); - const icon = await Image.fromPath(iconPath); + async function updateTrayMenu() { + if (!trayIconInstance) return; + try { + const menu = await buildTrayMenu(); + await trayIconInstance.setMenu(menu); + } catch (e: any) { + addLog("WARN", `更新托盘菜单失败: ${e.message || e}`, "SYSTEM"); + } + } - await TrayIcon.new({ + async function setupTray() { + try { + const menu = await buildTrayMenu(); + + // 初始状态:灰色(未开始) + const icon = await Image.new(makeTrayIconRGBA(158, 158, 158), 32, 32); + + trayIconInstance = await TrayIcon.new({ icon, - tooltip: APP_NAME, + tooltip: `${APP_NAME} - 未开始`, menu, menuOnLeftClick: false, action: async (event) => { if (event.type === "Click" && event.button === "Left") { - await mainWindow.unminimize(); - await mainWindow.show(); - await mainWindow.setFocus(); + const win = getCurrentWindow(); + await win.unminimize(); + await win.show(); + await win.setFocus(); } }, }); @@ -597,6 +754,14 @@ export const useMonitorStore = defineStore("monitor", () => { } }); addLog("INFO", "应用程序启动完成", "SYSTEM"); + + if (config.value.silent_start) { + try { + await getCurrentWindow().hide(); + addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM"); + } catch { /* ignore */ } + } + checkNetworkStatus(); if (config.value.auto_monitor && rememberPassword.value) { @@ -604,7 +769,7 @@ export const useMonitorStore = defineStore("monitor", () => { ? !!tokenSessionId.value : !!(username.value && password.value); if (canAutoStart) { - addLog("INFO", "自动登录并开始监控...", "SYSTEM"); + addLog("INFO", "自动登录并开始监测...", "SYSTEM"); await startMonitoring(); } } @@ -617,18 +782,28 @@ export const useMonitorStore = defineStore("monitor", () => { unlistenClose?.(); } + // 监听状态变化,自动更新托盘图标和菜单 + watch([isMonitoring, isPostponed, ticketCounts], () => { + updateTrayIcon(); + updateTrayMenu(); + }, { deep: true }); + return { // 状态 username, password, rememberPassword, loginMode, tokenSessionId, message, isLoading, - isLoggedIn, isMonitoring, isPostponed, autoStartEnabled, + isLoggedIn, isMonitoring, isPostponed, postponeResumeTime, autoStartEnabled, logs, networkStatus, config, ticketCounts, showCloseDialog, // 方法 initialize, cleanup, addLog, clearLogs, clearMessage, saveSettings, exportLogs, - startMonitoring, stopMonitoring, manualCheck, + startMonitoring, stopMonitoring, resumeMonitoring, manualCheck, testNotification, checkNetworkStatus, updateConfig, minimizeToTray, handleCloseAction, }; }); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useMonitorStore, import.meta.hot)); +}