import { ref } from "vue"; import { defineStore } from "pinia"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { TrayIcon } from "@tauri-apps/api/tray"; import { Image } from "@tauri-apps/api/image"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { fetch } from "@tauri-apps/plugin-http"; import { isPermissionGranted, requestPermission, } from "@tauri-apps/plugin-notification"; import { enable as enableAutostart, disable as disableAutostart, isEnabled as isAutostartEnabled, } 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"; 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`; export interface LogEntry { timestamp: string; level: string; message: string; category: string; } export interface TicketCounts { pending: number; stayclose: number; confirm: number; lastCheck: string; } export const useMonitorStore = defineStore("monitor", () => { // ===== 响应式数据 ===== const username = ref(""); const password = ref(""); const rememberPassword = ref(false); const loginMode = ref<"password" | "token">("password"); const tokenSessionId = ref(""); const message = ref(""); const isLoading = ref(false); const isLoggedIn = ref(false); const isMonitoring = ref(false); const isPostponed = ref(false); const autoStartEnabled = ref(false); const logs = ref([]); const ticketCounts = ref({ pending: 0, stayclose: 0, confirm: 0, lastCheck: "", }); const networkStatus = ref({ is_connected: false, api_reachable: false, response_time: null as number | null, last_check: "", }); const config = ref({ check_interval: 60, auto_start: false, auto_monitor: false, show_notifications: true, notification_sound: true, network_timeout: 30, close_action: "ask" as "ask" | "minimize" | "close", handle_pause_minutes: 20, }); const showCloseDialog = ref(false); // ===== 内部状态 ===== let sessionId = ""; let userGid = ""; let userName = ""; let monitorTimer: ReturnType | null = null; let postponeTimer: ReturnType | null = null; let unlistenNotification: UnlistenFn | null = null; let unlistenClose: UnlistenFn | 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")}`; } function addLog(level: string, msg: string, category: string) { logs.value.push({ timestamp: formatTime(new Date()), level, message: msg, category }); if (logs.value.length > 1000) logs.value.shift(); } function clearLogs() { logs.value = []; message.value = "日志已清除"; } function clearMessage() { message.value = ""; } // ===== 通知声音 ===== function playNotificationSound() { if (!config.value.notification_sound) return; try { const ctx = new AudioContext(); const oscillator = ctx.createOscillator(); const gain = ctx.createGain(); oscillator.connect(gain); gain.connect(ctx.destination); oscillator.frequency.setValueAtTime(880, ctx.currentTime); oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15); gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4); oscillator.start(ctx.currentTime); oscillator.stop(ctx.currentTime + 0.4); } catch { /* 静默失败 */ } } // ===== 通知 ===== async function notify(title: string, body: string) { if (!config.value.show_notifications) return; let granted = await isPermissionGranted(); if (!granted) { const permission = await requestPermission(); granted = permission === "granted"; } if (granted) { try { await invoke("send_clickable_notification", { title, body }); playNotificationSound(); addLog("INFO", `通知: ${title} - ${body}`, "NOTIFICATION"); } catch (e: any) { addLog("ERROR", `发送通知失败: ${e}`, "NOTIFICATION"); } } } // ===== API ===== async function apiLogin(): Promise { try { const body = JSON.stringify({ Account: username.value, PassWord: password.value }); const resp = await fetch(LOGIN_URL, { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": String(new TextEncoder().encode(body).length), Host: "crm.yunvip123.com", "Cache-Control": "no-cache", Cookie: "", }, body, credentials: "omit", }); const setCookie = resp.headers.get("set-cookie") || ""; const match = setCookie.match(/ASP\.NET_SessionId=([^;]+)/); if (match) sessionId = match[1]; addLog("DEBUG", `SessionId: ${sessionId || "(未提取)"}`, "LOGIN"); const data = await resp.json(); if (data.success && data.data) { userGid = data.data.GID; userName = data.data.SU_UserName || "用户"; if (!sessionId) { addLog("WARN", "未能从响应头提取SessionId", "LOGIN"); } isLoggedIn.value = true; addLog("INFO", `登录成功,用户: ${userName}, GID: ${userGid}`, "LOGIN"); return true; } else { addLog("ERROR", `登录失败: ${data.msg}`, "LOGIN"); return false; } } catch (e: any) { addLog("ERROR", `登录请求失败: ${e.message || e}`, "LOGIN"); return false; } } async function apiCheckStatus(isRetry = false): Promise { 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(); if (result.success && result.data) { const d = result.data; const pending = d.PendingCount || 0; const stayclose = d.StaycloseCount || 0; const confirm = d.ConfirmCount || 0; ticketCounts.value = { pending, stayclose, confirm, lastCheck: formatTime(new Date()), }; addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER"); if (pending > 0 || stayclose > 0 || confirm > 0) { const msgs: string[] = []; if (pending > 0) msgs.push(`待处理工单: ${pending}`); if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`); if (confirm > 0) msgs.push(`待确认工单: ${confirm}`); await notify("工单提醒", msgs.join(",")); } } else { addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER"); if (!isRetry && isSessionExpiredMsg(result.msg)) { await tryReLogin(); } } } catch (e: any) { addLog("ERROR", `API 检查失败: ${e.message || e}`, "SCHEDULER"); } } function isSessionExpiredMsg(msg: string | undefined): boolean { if (!msg) return false; const keywords = ["session", "expired", "unauthorized", "登录", "过期", "失效", "超时"]; const lower = msg.toLowerCase(); return keywords.some((k) => lower.includes(k)); } async function tryReLogin(): Promise { if (loginMode.value === "token") { addLog("ERROR", "Token 已失效,监控已停止,请更换新的 Token 后重新开始", "SESSION"); message.value = "Token 已失效,监控已停止"; stopMonitoring(); return; } if (!username.value || !password.value) { addLog("WARN", "无法自动重登录:缺少凭据", "SESSION"); return; } addLog("INFO", "会话可能已过期,正在自动重新登录...", "SESSION"); const ok = await apiLogin(); if (ok) { addLog("INFO", "自动重新登录成功,继续检查", "SESSION"); await apiCheckStatus(true); } else { addLog("ERROR", "自动重新登录失败,请手动重新登录", "SESSION"); message.value = "会话已过期且自动重登录失败,请手动操作"; } } // ===== 网络检测 ===== async function checkNetworkStatus() { isLoading.value = true; let isConnected = false; let apiReachable = false; let responseTime: number | null = null; try { const resp = await fetch("https://www.baidu.com", { method: "GET", connectTimeout: config.value.network_timeout * 1000, }); isConnected = resp.ok; } catch { /* 网络不通 */ } if (isConnected) { try { const apiStart = Date.now(); const resp = await fetch(LOGIN_URL, { method: "HEAD", connectTimeout: config.value.network_timeout * 1000, }); responseTime = Date.now() - apiStart; apiReachable = resp.status < 500; } catch { /* API 不可达 */ } } networkStatus.value = { is_connected: isConnected, api_reachable: apiReachable, response_time: responseTime, last_check: formatTime(new Date()), }; addLog("INFO", `网络检查完成 - 连接: ${isConnected}, API可达: ${apiReachable}`, "NETWORK"); message.value = "网络状态检查完成"; isLoading.value = false; } // ===== 凭据存储 ===== function loadCredentials() { try { const saved = localStorage.getItem("crm_credentials"); if (saved) { const data = JSON.parse(saved); username.value = data.username || ""; password.value = data.password || ""; rememberPassword.value = data.remember || false; loginMode.value = data.loginMode || "password"; tokenSessionId.value = data.tokenSessionId || ""; addLog("INFO", "已加载保存的凭据", "CONFIG"); } } catch (e: any) { addLog("ERROR", `加载凭据失败: ${e.message || e}`, "CONFIG"); } } function saveCredentials() { try { if (rememberPassword.value) { localStorage.setItem( "crm_credentials", JSON.stringify({ username: username.value, password: password.value, remember: true, loginMode: loginMode.value, tokenSessionId: tokenSessionId.value, }) ); addLog("INFO", "凭据已保存到本地", "CONFIG"); } else { localStorage.removeItem("crm_credentials"); addLog("INFO", "已清除本地凭据", "CONFIG"); } } catch (e: any) { addLog("ERROR", `保存凭据失败: ${e.message || e}`, "CONFIG"); } } function loadConfig() { try { const saved = localStorage.getItem("crm_config"); if (saved) { const data = JSON.parse(saved); config.value = { ...config.value, ...data }; addLog("INFO", "已加载保存的配置", "CONFIG"); } } catch (e: any) { addLog("ERROR", `加载配置失败: ${e.message || e}`, "CONFIG"); } } function saveConfigToStorage() { try { localStorage.setItem("crm_config", JSON.stringify(config.value)); } catch { /* ignore */ } } function saveSettings() { if (loginMode.value === "password") { if (!username.value.trim() || !password.value.trim()) { message.value = "请输入用户名和密码"; return; } } else { if (!tokenSessionId.value.trim()) { message.value = "请输入 Token (ASP.NET_SessionId)"; return; } } saveCredentials(); message.value = "设置保存成功"; addLog("INFO", "用户凭据已保存", "CONFIG"); } function exportLogs() { if (logs.value.length === 0) { message.value = "暂无日志可导出"; return; } const lines = logs.value.map( (l) => `[${l.timestamp}] [${l.level}] [${l.category}] ${l.message}` ); const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `monitor-logs-${formatTime(new Date()).replaceAll(/[: ]/g, "-")}.txt`; a.click(); URL.revokeObjectURL(url); addLog("INFO", `已导出 ${logs.value.length} 条日志`, "SYSTEM"); message.value = "日志导出成功"; } // ===== 监控控制 ===== async function startMonitoring() { if (loginMode.value === "password") { if (!username.value.trim() || !password.value.trim()) { message.value = "请先设置用户名和密码"; return; } isLoading.value = true; const ok = await apiLogin(); if (!ok) { message.value = "登录失败,无法开始监控"; isLoading.value = false; return; } } else { if (!tokenSessionId.value.trim()) { message.value = "请先输入 Token (ASP.NET_SessionId)"; return; } isLoading.value = true; sessionId = tokenSessionId.value.trim(); isLoggedIn.value = true; addLog("INFO", `使用 Token 登录,SessionId: ${sessionId.substring(0, 8)}...`, "LOGIN"); } isMonitoring.value = true; await apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); addLog("INFO", "监控已启动", "MONITOR"); message.value = loginMode.value === "password" ? `监控已开始,欢迎 ${userName}!` : "监控已开始(Token 登录)"; isLoading.value = false; } function stopMonitoring() { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; } isPostponed.value = false; isMonitoring.value = false; isLoggedIn.value = false; sessionId = ""; userGid = ""; addLog("INFO", "监控已停止", "MONITOR"); message.value = "监控已停止"; } function postponeMonitoring() { if (!isMonitoring.value) return; if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); } isPostponed.value = true; 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 = "推迟结束,监控已自动恢复"; }, 60 * 60 * 1000); } function pauseForHandle() { if (!isMonitoring.value) return; if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); } isPostponed.value = true; const minutes = config.value.handle_pause_minutes; addLog("INFO", `点击"去处理",监控已暂停 ${minutes} 分钟`, "MONITOR"); message.value = `监控已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`; postponeTimer = setTimeout(() => { isPostponed.value = false; postponeTimer = null; apiCheckStatus(); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); addLog("INFO", "暂停结束,监控已自动恢复", "MONITOR"); message.value = "暂停结束,监控已自动恢复"; }, minutes * 60 * 1000); } async function manualCheck() { if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; } isLoading.value = true; await apiCheckStatus(); message.value = "手动检查完成"; isLoading.value = false; } async function testNotification() { await notify("工单提醒(测试)", "待处理工单: 3,待关闭工单: 1,待确认工单: 2"); } // ===== 配置更新 ===== async function updateConfig() { isLoading.value = true; try { if (config.value.auto_start) await enableAutostart(); else await disableAutostart(); autoStartEnabled.value = config.value.auto_start; addLog("INFO", `自启动设置: ${config.value.auto_start ? "启用" : "禁用"}`, "CONFIG"); } catch (e: any) { addLog("ERROR", `自启动设置失败: ${e.message || e}`, "CONFIG"); } if (isMonitoring.value && monitorTimer) { clearInterval(monitorTimer); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); addLog("INFO", `检查间隔已更新为: ${config.value.check_interval}秒`, "CONFIG"); } saveConfigToStorage(); message.value = "配置更新成功"; isLoading.value = false; } // ===== 系统托盘 ===== async function setupTray() { try { const mainWindow = getCurrentWindow(); const menu = 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: "quit", text: "退出", action: async () => { await mainWindow.destroy(); }, }), ], }); const iconPath = await resolveResource("icons/32x32.png"); const icon = await Image.fromPath(iconPath); await TrayIcon.new({ icon, 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(); } }, }); addLog("INFO", "系统托盘创建成功", "SYSTEM"); } catch (e: any) { addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM"); } } async function minimizeToTray() { try { await getCurrentWindow().hide(); } catch (e: any) { message.value = `最小化失败: ${e}`; } } async function handleCloseAction(action: "minimize" | "close", remember: boolean) { showCloseDialog.value = false; if (remember) { config.value.close_action = action; saveConfigToStorage(); addLog("INFO", `关闭行为已设为: ${action === "minimize" ? "最小化到托盘" : "直接退出"}`, "SYSTEM"); } if (action === "minimize") { await getCurrentWindow().hide(); } else { await getCurrentWindow().destroy(); } } // ===== 生命周期(由 App.vue 调用)===== async function initialize() { loadConfig(); loadCredentials(); await setupTray(); try { autoStartEnabled.value = await isAutostartEnabled(); config.value.auto_start = autoStartEnabled.value; } catch { /* ignore */ } unlistenNotification = await listen("notification-action", (event) => { if (event.payload === "postpone_1h") postponeMonitoring(); else if (event.payload === "go_handle") pauseForHandle(); }); unlistenClose = await listen("close-requested", async () => { const action = config.value.close_action; if (action === "minimize") { await getCurrentWindow().hide(); } else if (action === "close") { await getCurrentWindow().destroy(); } else { showCloseDialog.value = true; } }); addLog("INFO", "应用程序启动完成", "SYSTEM"); checkNetworkStatus(); if (config.value.auto_monitor && rememberPassword.value) { const canAutoStart = loginMode.value === "token" ? !!tokenSessionId.value : !!(username.value && password.value); if (canAutoStart) { addLog("INFO", "自动登录并开始监控...", "SYSTEM"); await startMonitoring(); } } } function cleanup() { if (monitorTimer) clearInterval(monitorTimer); if (postponeTimer) clearTimeout(postponeTimer); unlistenNotification?.(); unlistenClose?.(); } return { // 状态 username, password, rememberPassword, loginMode, tokenSessionId, message, isLoading, isLoggedIn, isMonitoring, isPostponed, autoStartEnabled, logs, networkStatus, config, ticketCounts, showCloseDialog, // 方法 initialize, cleanup, addLog, clearLogs, clearMessage, saveSettings, exportLogs, startMonitoring, stopMonitoring, manualCheck, testNotification, checkNetworkStatus, updateConfig, minimizeToTray, handleCloseAction, }; });