diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 45a5745..3f3236f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,9 +2,9 @@ "permissions": { "defaultMode": "acceptEdits", "allow": [ - "Read(./)", - "Edit(./)", - "Write(./)", + "Read", + "Edit", + "Write", "Bash(pnpm build *)", "Bash(pnpm install:*)", diff --git a/package.json b/package.json index 4020df6..91c74ed 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "version:major": "node scripts/bump-version.mjs major", "publish": "node scripts/publish.mjs", "deploy": "npm run tauri-build && npm run publish", + "type-check": "vue-tsc --noEmit", "lint": "eslint src", "lint:fix": "eslint src --fix", "commit": "git-cz", diff --git a/src/App.vue b/src/App.vue index 3e4c4d6..e6dddd7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,17 +14,17 @@ import MonitorControl from "@/components/MonitorControl.vue"; import SettingsPanel from "@/components/SettingsPanel.vue"; import StatusHeader from "@/components/StatusHeader.vue"; import UpdateDialog from "@/components/UpdateDialog.vue"; -import { useUpdater } from "@/composables/useUpdater"; import { useAppStore } from "@/stores/app"; import { useLogStore } from "@/stores/log"; import { useMonitorStore } from "@/stores/monitor"; import { useNetworkStore } from "@/stores/network"; +import { useUpdaterStore } from "@/stores/updater"; const activeTab = ref("monitor"); const store = useMonitorStore(); const appStore = useAppStore(); const logStore = useLogStore(); -const updater = useUpdater(); +const updater = useUpdaterStore(); const networkStore = useNetworkStore(); /** 通知动作事件的取消监听函数 */ @@ -88,7 +88,7 @@ onMounted(async () => { getVersion() .then((v) => { - updater.currentVersion.value = v; + updater.currentVersion = v; }) .catch(() => {}); diff --git a/src/components/SettingsPanel.vue b/src/components/SettingsPanel.vue index b1bdad9..e0be53d 100644 --- a/src/components/SettingsPanel.vue +++ b/src/components/SettingsPanel.vue @@ -3,11 +3,11 @@ import { CloudSyncOutlined } from "@ant-design/icons-vue"; import dayjs, { type Dayjs } from "dayjs"; import { debounce } from "lodash-es"; import { computed, watch } from "vue"; -import { useUpdater } from "@/composables/useUpdater"; import { useAppStore } from "@/stores/app"; +import { useUpdaterStore } from "@/stores/updater"; const appStore = useAppStore(); -const { currentVersion, isCheckingUpdates, isUpdating, checkForUpdates } = useUpdater(); +const { currentVersion, isCheckingUpdates, isUpdating, checkForUpdates } = useUpdaterStore(); // ===== 检查间隔:内部用秒,TimePicker 用 Dayjs ===== const intervalTime = computed({ diff --git a/src/components/UpdateDialog.vue b/src/components/UpdateDialog.vue index 6abf7e5..5cbecb1 100644 --- a/src/components/UpdateDialog.vue +++ b/src/components/UpdateDialog.vue @@ -1,22 +1,28 @@ diff --git a/src/composables/useMonitor.ts b/src/composables/useMonitor.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/composables/useNotification.ts b/src/composables/useNotification.ts index 27c3698..f75b3b5 100644 --- a/src/composables/useNotification.ts +++ b/src/composables/useNotification.ts @@ -6,6 +6,7 @@ import { import { storeToRefs } from "pinia"; import { useAppStore } from "@/stores/app"; import { useLogStore } from "@/stores/log"; +import { getErrorMessage } from "@/utils/common"; /** 通知 Composable */ export function useNotification() { @@ -44,8 +45,8 @@ export function useNotification() { await invoke("send_clickable_notification", { title, body }); playNotificationSound(); addLog("INFO", `通知: ${title} - ${body}`, "NOTIFICATION"); - } catch (e: any) { - addLog("ERROR", `发送通知失败: ${e}`, "NOTIFICATION"); + } catch (e: unknown) { + addLog("ERROR", `发送通知失败: ${getErrorMessage(e)}`, "NOTIFICATION"); } } } diff --git a/src/composables/useTray.ts b/src/composables/useTray.ts index 76d946c..28a3d5e 100644 --- a/src/composables/useTray.ts +++ b/src/composables/useTray.ts @@ -17,6 +17,7 @@ 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 是否正在监测 @@ -161,8 +162,8 @@ export function useTray( 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"); + } catch (e: unknown) { + addLog("WARN", `更新托盘图标失败: ${getErrorMessage(e)}`, "SYSTEM"); } } @@ -228,8 +229,8 @@ export function useTray( try { const menu = await buildTrayMenu(); await trayIconInstance.setMenu(menu); - } catch (e: any) { - addLog("WARN", `更新托盘菜单失败: ${e.message || e}`, "SYSTEM"); + } catch (e: unknown) { + addLog("WARN", `更新托盘菜单失败: ${getErrorMessage(e)}`, "SYSTEM"); } } @@ -264,8 +265,8 @@ export function useTray( }, }); addLog("INFO", "系统托盘创建成功", "SYSTEM"); - } catch (e: any) { - addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM"); + } catch (e: unknown) { + addLog("ERROR", `系统托盘创建失败: ${getErrorMessage(e)}`, "SYSTEM"); } } diff --git a/src/stores/app.ts b/src/stores/app.ts index 3237930..7ada284 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -6,6 +6,7 @@ import { import { defineStore, acceptHMRUpdate } from "pinia"; import { ref } from "vue"; import { useLogStore } from "@/stores/log"; +import { getErrorMessage } from "@/utils/common"; export const useAppStore = defineStore("app", () => { const message = ref(""); // 界面状态提示消息 @@ -45,8 +46,8 @@ export const useAppStore = defineStore("app", () => { config.value = { ...config.value, ...data }; addLog("INFO", "已加载保存的配置", "CONFIG"); } - } catch (e: any) { - addLog("ERROR", `加载配置失败: ${e.message || e}`, "CONFIG"); + } catch (e: unknown) { + addLog("ERROR", `加载配置失败: ${getErrorMessage(e)}`, "CONFIG"); } } @@ -71,8 +72,8 @@ export const useAppStore = defineStore("app", () => { `自启动设置: ${config.value.auto_start ? "启用" : "禁用"}`, "CONFIG", ); - } catch (e: any) { - addLog("ERROR", `自启动设置失败: ${e.message || e}`, "CONFIG"); + } catch (e: unknown) { + addLog("ERROR", `自启动设置失败: ${getErrorMessage(e)}`, "CONFIG"); } saveConfigToStorage(); setMessage("配置更新成功"); diff --git a/src/stores/monitor.ts b/src/stores/monitor.ts index 8b87738..1484f0a 100644 --- a/src/stores/monitor.ts +++ b/src/stores/monitor.ts @@ -6,7 +6,7 @@ import { useTray } from "@/composables/useTray"; import { LOGIN_URL, CHECK_URL, WORK_ORDER_URL } from "@/constants/api"; import { useAppStore } from "@/stores/app"; import { useLogStore } from "@/stores/log"; -import { formatTime } from "@/utils/common"; +import { formatTime, getErrorMessage } from "@/utils/common"; export interface TicketCounts { pending: number; @@ -73,7 +73,7 @@ export const useMonitorStore = defineStore("monitor", () => { }); const setCookie = resp.headers.get("set-cookie") || ""; - const match = setCookie.match(/ASP\.NET_SessionId=([^;]+)/); + const match = /ASP\.NET_SessionId=([^;]+)/.exec(setCookie); if (match) sessionId = match[1]; addLog("DEBUG", `SessionId: ${sessionId || "(未提取)"}`, "LOGIN"); @@ -91,8 +91,8 @@ export const useMonitorStore = defineStore("monitor", () => { addLog("ERROR", `登录失败: ${data.msg}`, "LOGIN"); return false; } - } catch (e: any) { - addLog("ERROR", `登录请求失败: ${e.message || e}`, "LOGIN"); + } catch (e: unknown) { + addLog("ERROR", `登录请求失败: ${getErrorMessage(e)}`, "LOGIN"); return false; } } @@ -123,65 +123,94 @@ export const useMonitorStore = defineStore("monitor", () => { addLog("WARN", `待审核 API 返回异常: ${result.msg}`, "SCHEDULER"); return 0; } - } catch (e: any) { - addLog("ERROR", `待审核 API 检查失败: ${e.message || e}`, "SCHEDULER"); + } catch (e: unknown) { + addLog( + "ERROR", + `待审核 API 检查失败: ${getErrorMessage(e)}`, + "SCHEDULER", + ); return 0; } } + /** 发送工单状态检查请求 */ + async function performStatusCheckRequest() { + const body: Record = {}; + if (userGid) body.UserGID = userGid; + const resp = await fetch(CHECK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `ASP.NET_SessionId=${sessionId}`, + }, + body: JSON.stringify(body), + }); + return await resp.json(); + } + + /** 处理检查到的工单数据并更新状态 */ + async function updateTicketCountsAndLog(data: any) { + const pending = data.PendingCount || 0; + const stayclose = data.StaycloseCount || 0; + const confirm = data.ConfirmCount || 0; + const workOrderCount = await apiCheckWorkOrders(); + + const counts = { + pending, + stayclose, + confirm, + workOrderCount, + lastCheck: formatTime(new Date()), + }; + ticketCounts.value = counts; + + addLog( + "INFO", + `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, + "SCHEDULER", + ); + return counts; + } + + /** 发送工单提醒通知 */ + async function sendTicketNotifications(counts: TicketCounts) { + const msgs: string[] = []; + if (counts.pending > 0) msgs.push(`待处理工单: ${counts.pending}`); + if (counts.stayclose > 0) msgs.push(`待关闭工单: ${counts.stayclose}`); + if (counts.confirm > 0) msgs.push(`待确认工单: ${counts.confirm}`); + if (counts.workOrderCount > 0) + msgs.push(`待审核: ${counts.workOrderCount}`); + + if (msgs.length > 0) { + await notify("工单提醒", msgs.join(",")); + } + } + /** 拉取工单状态计数,触发通知;isRetry=true 时不再递归重登 */ async function apiCheckStatus(isRetry = false): Promise { + // 1. 预检查:如果会话失效且是密码模式,尝试重登 if (!userGid && loginMode.value === "password") { addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER"); if (!isRetry) await tryReLogin(); return; } + try { - const body: Record = {}; - if (userGid) body.UserGID = userGid; - const resp = await fetch(CHECK_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Cookie: `ASP.NET_SessionId=${sessionId}`, - }, - body: JSON.stringify(body), - }); - const result = await resp.json(); + const result = await performStatusCheckRequest(); if (result.success && result.data) { - const d = result.data; - const pending = d.PendingCount || 0; - const stayclose = d.StaycloseCount || 0; - const confirm = d.ConfirmCount || 0; - const workOrderCount = await apiCheckWorkOrders(); - ticketCounts.value = { - pending, - stayclose, - confirm, - workOrderCount, - lastCheck: formatTime(new Date()), - }; - 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(",")); - } + // 2. 处理成功检查 + const counts = await updateTicketCountsAndLog(result.data); + await sendTicketNotifications(counts); } else { + // 3. 处理业务失败 addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER"); if (!isRetry && isSessionExpiredMsg(result.msg)) { await tryReLogin(); } } - } catch (e: any) { - addLog("ERROR", `API 检查失败: ${e.message || e}`, "SCHEDULER"); + } catch (e: unknown) { + // 4. 处理网络/系统异常 + addLog("ERROR", `API 检查失败: ${getErrorMessage(e)}`, "SCHEDULER"); } } @@ -242,8 +271,8 @@ export const useMonitorStore = defineStore("monitor", () => { tokenSessionId.value = data.tokenSessionId || ""; addLog("INFO", "已加载保存的凭据", "CONFIG"); } - } catch (e: any) { - addLog("ERROR", `加载凭据失败: ${e.message || e}`, "CONFIG"); + } catch (e: unknown) { + addLog("ERROR", `加载凭据失败: ${getErrorMessage(e)}`, "CONFIG"); } } @@ -266,8 +295,8 @@ export const useMonitorStore = defineStore("monitor", () => { localStorage.removeItem("crm_credentials"); addLog("INFO", "已清除本地凭据", "CONFIG"); } - } catch (e: any) { - addLog("ERROR", `保存凭据失败: ${e.message || e}`, "CONFIG"); + } catch (e: unknown) { + addLog("ERROR", `保存凭据失败: ${getErrorMessage(e)}`, "CONFIG"); } } diff --git a/src/composables/useUpdater.ts b/src/stores/updater.ts similarity index 65% rename from src/composables/useUpdater.ts rename to src/stores/updater.ts index 16ee70d..b33486f 100644 --- a/src/composables/useUpdater.ts +++ b/src/stores/updater.ts @@ -1,42 +1,42 @@ import { relaunch } from "@tauri-apps/plugin-process"; import { check, type Update } from "@tauri-apps/plugin-updater"; +import { acceptHMRUpdate, defineStore } from "pinia"; import { ref } from "vue"; import { useLogStore } from "@/stores/log"; +import { getErrorMessage } from "@/utils/common"; -// ===== 模块级单例状态 ===== -/** 是否显示更新对话框 */ -const showUpdateDialog = ref(false); -/** 新版本信息 */ -const updateInfo = ref<{ - version: string; - notes: string; - date: string; -} | null>(null); -/** 下载进度 */ -const updateProgress = ref<{ total: number; downloaded: number } | null>(null); -/** 是否正在更新中 */ -const isUpdating = ref(false); -/** 当前应用版本号 */ -const currentVersion = ref(""); -/** 是否正在检查更新 */ -const isCheckingUpdates = ref(false); - -/** 待安装的更新对象 */ -let pendingUpdate: Update | null = null; - -/** - * 版本更新 Composable(单例模式,模块级状态共享) - */ -export function useUpdater() { +export const useUpdaterStore = defineStore("updater", () => { const { addLog } = useLogStore(); + /** 是否显示更新对话框 */ + const showUpdateDialog = ref(false); + /** 新版本信息 */ + const updateInfo = ref<{ + version: string; + notes: string; + date: string; + } | null>(null); + /** 下载进度 */ + const updateProgress = ref<{ total: number; downloaded: number } | null>( + null, + ); + /** 是否正在更新中 */ + const isUpdating = ref(false); + /** 当前应用版本号 */ + const currentVersion = ref(""); + /** 是否正在检查更新 */ + const isCheckingUpdates = ref(false); + + /** 待安装的更新对象 */ + let pendingUpdate: Update | null = null; + /** 检查是否有可用的应用更新,发现新版本时展示更新对话框 */ async function checkForUpdates() { if (isCheckingUpdates.value || isUpdating.value) return; isCheckingUpdates.value = true; try { const update = await check(); - if (update?.available) { + if (update) { pendingUpdate = update; updateInfo.value = { version: update.version, @@ -48,8 +48,8 @@ export function useUpdater() { } else { addLog("INFO", "当前已是最新版本", "UPDATER"); } - } catch (e: any) { - addLog("WARN", `检查更新失败: ${e.message || e}`, "UPDATER"); + } catch (e: unknown) { + addLog("WARN", `检查更新失败: ${getErrorMessage(e)}`, "UPDATER"); } finally { isCheckingUpdates.value = false; } @@ -76,8 +76,8 @@ export function useUpdater() { } }); await relaunch(); - } catch (e: any) { - addLog("ERROR", `更新安装失败: ${e.message || e}`, "UPDATER"); + } catch (e: unknown) { + addLog("ERROR", `更新安装失败: ${getErrorMessage(e)}`, "UPDATER"); isUpdating.value = false; } } @@ -98,4 +98,8 @@ export function useUpdater() { confirmUpdate, dismissUpdate, }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useUpdaterStore, import.meta.hot)); } diff --git a/src/utils/common.ts b/src/utils/common.ts index 0487b91..4bd47e9 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,5 +1,10 @@ import dayjs from "dayjs"; +/** 从 unknown 类型的 catch 错误中提取可读的错误信息 */ +export function getErrorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + /** 将 Date 对象格式化为 'YYYY-MM-DD HH:mm:ss' 字符串 */ export function formatTime(date: Date): string { return dayjs(date).format("YYYY-MM-DD HH:mm:ss"); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 5fcee7b..6e10ee7 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -9,8 +9,8 @@ interface ImportMeta { } declare module "*.vue" { - import type { DefineComponent } from "vue"; + import type { Component } from "vue"; - const component: DefineComponent<{}, {}, any>; + const component: Component; export default component; }