2026-03-23 09:10:32 +08:00
|
|
|
|
import { ref, watch } from "vue";
|
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
import { defineStore, acceptHMRUpdate } from "pinia";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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";
|
2026-03-23 09:10:32 +08:00
|
|
|
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
2026-03-25 10:06:25 +08:00
|
|
|
|
import { check, type Update } from "@tauri-apps/plugin-updater";
|
|
|
|
|
|
import { relaunch } from "@tauri-apps/plugin-process";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
|
|
|
|
|
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`;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
|
|
|
|
|
export interface LogEntry {
|
|
|
|
|
|
timestamp: string;
|
|
|
|
|
|
level: string;
|
|
|
|
|
|
message: string;
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface TicketCounts {
|
|
|
|
|
|
pending: number;
|
|
|
|
|
|
stayclose: number;
|
|
|
|
|
|
confirm: number;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
workOrderCount: number;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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<LogEntry[]>([]);
|
|
|
|
|
|
const ticketCounts = ref<TicketCounts>({
|
2026-03-23 09:10:32 +08:00
|
|
|
|
pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
|
2026-03-10 14:41:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
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,
|
2026-03-23 09:10:32 +08:00
|
|
|
|
silent_start: false,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
show_notifications: true,
|
|
|
|
|
|
notification_sound: true,
|
|
|
|
|
|
network_timeout: 30,
|
|
|
|
|
|
close_action: "ask" as "ask" | "minimize" | "close",
|
2026-03-10 15:20:06 +08:00
|
|
|
|
handle_pause_minutes: 20,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
const showCloseDialog = ref(false);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
const postponeResumeTime = ref<number | null>(null);
|
2026-03-25 10:06:25 +08:00
|
|
|
|
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);
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== 内部状态 =====
|
|
|
|
|
|
let sessionId = "";
|
|
|
|
|
|
let userGid = "";
|
|
|
|
|
|
let userName = "";
|
|
|
|
|
|
let monitorTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
let postponeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
let unlistenNotification: UnlistenFn | null = null;
|
|
|
|
|
|
let unlistenClose: UnlistenFn | null = null;
|
2026-03-25 10:06:25 +08:00
|
|
|
|
let pendingUpdate: Update | null = null;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
let trayIconInstance: TrayIcon | null = null;
|
2026-03-25 10:06:25 +08:00
|
|
|
|
let cachedSvgImg: HTMLImageElement | null = null;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== 工具函数 =====
|
|
|
|
|
|
function formatTime(date: Date): string {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
|
2026-03-10 14:41:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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<boolean> {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
async function apiCheckWorkOrders(): Promise<number> {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
async function apiCheckStatus(isRetry = false): Promise<void> {
|
|
|
|
|
|
if (!userGid && loginMode.value === "password") {
|
|
|
|
|
|
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
|
|
|
|
|
|
if (!isRetry) await tryReLogin();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body: Record<string, string> = {};
|
|
|
|
|
|
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;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
const workOrderCount = await apiCheckWorkOrders();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
ticketCounts.value = {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
pending, stayclose, confirm, workOrderCount,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
lastCheck: formatTime(new Date()),
|
|
|
|
|
|
};
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER");
|
|
|
|
|
|
if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) {
|
2026-03-10 14:41:13 +08:00
|
|
|
|
const msgs: string[] = [];
|
|
|
|
|
|
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
|
|
|
|
|
|
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
|
|
|
|
|
|
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`);
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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<void> {
|
|
|
|
|
|
if (loginMode.value === "token") {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("ERROR", "Token 已失效,监测已停止,请更换新的 Token 后重新开始", "SESSION");
|
|
|
|
|
|
message.value = "Token 已失效,监测已停止";
|
2026-03-10 15:20:06 +08:00
|
|
|
|
stopMonitoring();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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 = "日志导出成功";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
// ===== 监测控制 =====
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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) {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
message.value = "登录失败,无法开始监测";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", "监测已启动", "MONITOR");
|
2026-03-10 14:41:13 +08:00
|
|
|
|
message.value = loginMode.value === "password"
|
2026-03-23 09:10:32 +08:00
|
|
|
|
? `监测已开始,欢迎 ${userName}!`
|
|
|
|
|
|
: "监测已开始(Token 登录)";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
isLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopMonitoring() {
|
|
|
|
|
|
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
|
|
|
|
|
if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; }
|
|
|
|
|
|
isPostponed.value = false;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
postponeResumeTime.value = null;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
isMonitoring.value = false;
|
|
|
|
|
|
isLoggedIn.value = false;
|
|
|
|
|
|
sessionId = "";
|
|
|
|
|
|
userGid = "";
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", "监测已停止", "MONITOR");
|
|
|
|
|
|
message.value = "监测已停止";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function postponeMonitoring() {
|
|
|
|
|
|
if (!isMonitoring.value) return;
|
|
|
|
|
|
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
|
|
|
|
|
if (postponeTimer) { clearTimeout(postponeTimer); }
|
|
|
|
|
|
isPostponed.value = true;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
postponeResumeTime.value = Date.now() + 60 * 60 * 1000;
|
|
|
|
|
|
addLog("INFO", "监测已推迟1小时", "MONITOR");
|
|
|
|
|
|
message.value = "监测已推迟1小时,将在1小时后自动恢复";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
postponeTimer = setTimeout(() => {
|
|
|
|
|
|
isPostponed.value = false;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
postponeResumeTime.value = null;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
postponeTimer = null;
|
|
|
|
|
|
apiCheckStatus();
|
|
|
|
|
|
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR");
|
|
|
|
|
|
message.value = "推迟结束,监测已自动恢复";
|
2026-03-10 14:41:13 +08:00
|
|
|
|
}, 60 * 60 * 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:20:06 +08:00
|
|
|
|
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;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
postponeResumeTime.value = Date.now() + minutes * 60 * 1000;
|
|
|
|
|
|
addLog("INFO", `点击"去处理",监测已暂停 ${minutes} 分钟`, "MONITOR");
|
|
|
|
|
|
message.value = `监测已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`;
|
2026-03-10 15:20:06 +08:00
|
|
|
|
postponeTimer = setTimeout(() => {
|
|
|
|
|
|
isPostponed.value = false;
|
2026-03-23 09:10:32 +08:00
|
|
|
|
postponeResumeTime.value = null;
|
2026-03-10 15:20:06 +08:00
|
|
|
|
postponeTimer = null;
|
|
|
|
|
|
apiCheckStatus();
|
|
|
|
|
|
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", "暂停结束,监测已自动恢复", "MONITOR");
|
|
|
|
|
|
message.value = "暂停结束,监测已自动恢复";
|
2026-03-10 15:20:06 +08:00
|
|
|
|
}, minutes * 60 * 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
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 = "监测已恢复";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
async function manualCheck() {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; }
|
2026-03-10 14:41:13 +08:00
|
|
|
|
isLoading.value = true;
|
|
|
|
|
|
await apiCheckStatus();
|
|
|
|
|
|
message.value = "手动检查完成";
|
|
|
|
|
|
isLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function testNotification() {
|
|
|
|
|
|
await notify("工单提醒(测试)", "待处理工单: 3,待关闭工单: 1,待确认工单: 2");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 配置更新 =====
|
2026-03-25 10:06:25 +08:00
|
|
|
|
|
|
|
|
|
|
async function checkForUpdates() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
addLog("INFO", "正在检查应用更新...", "UPDATER");
|
|
|
|
|
|
const update = await check();
|
|
|
|
|
|
if (update) {
|
|
|
|
|
|
pendingUpdate = update;
|
|
|
|
|
|
updateInfo.value = {
|
|
|
|
|
|
version: update.version,
|
|
|
|
|
|
notes: update.body || "",
|
|
|
|
|
|
date: update.date || "",
|
|
|
|
|
|
};
|
|
|
|
|
|
showUpdateDialog.value = true;
|
|
|
|
|
|
addLog("INFO", `发现新版本: ${update.version}`, "UPDATER");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
addLog("INFO", "当前已是最新版本", "UPDATER");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
addLog("WARN", `检查更新失败: ${e.message || e}`, "UPDATER");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmUpdate() {
|
|
|
|
|
|
if (!pendingUpdate) return;
|
|
|
|
|
|
isUpdating.value = true;
|
|
|
|
|
|
updateProgress.value = { total: 0, downloaded: 0 };
|
|
|
|
|
|
try {
|
|
|
|
|
|
addLog("INFO", "开始下载更新...", "UPDATER");
|
|
|
|
|
|
await pendingUpdate.downloadAndInstall((event) => {
|
|
|
|
|
|
if (event.event === "Started" && event.data.contentLength) {
|
|
|
|
|
|
updateProgress.value = { total: event.data.contentLength, downloaded: 0 };
|
|
|
|
|
|
} else if (event.event === "Progress") {
|
|
|
|
|
|
if (updateProgress.value) {
|
|
|
|
|
|
updateProgress.value.downloaded += event.data.chunkLength;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (event.event === "Finished") {
|
|
|
|
|
|
addLog("INFO", "更新下载完成,即将重启...", "UPDATER");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
await relaunch();
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
addLog("ERROR", `更新安装失败: ${e.message || e}`, "UPDATER");
|
|
|
|
|
|
message.value = "更新安装失败";
|
|
|
|
|
|
isUpdating.value = false;
|
|
|
|
|
|
updateProgress.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dismissUpdate() {
|
|
|
|
|
|
showUpdateDialog.value = false;
|
|
|
|
|
|
pendingUpdate = null;
|
|
|
|
|
|
updateInfo.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 系统托盘 =====
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
2026-03-25 10:06:25 +08:00
|
|
|
|
/** 加载 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 数据 */
|
|
|
|
|
|
async function makeTrayIconRGBA(r: number, g: number, b: number, size = 32): Promise<Uint8Array> {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
|
canvas.width = size;
|
|
|
|
|
|
canvas.height = size;
|
|
|
|
|
|
const ctx = canvas.getContext("2d")!;
|
|
|
|
|
|
|
2026-03-25 10:06:25 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
2026-03-25 10:06:25 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
2026-03-25 10:06:25 +08:00
|
|
|
|
const finalData = ctx.getImageData(0, 0, size, size);
|
|
|
|
|
|
return new Uint8Array(finalData.data.buffer);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 根据当前应用状态更新托盘图标和提示文字 */
|
|
|
|
|
|
async function updateTrayIcon() {
|
|
|
|
|
|
if (!trayIconInstance) return;
|
2026-03-10 14:41:13 +08:00
|
|
|
|
try {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
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} - 监测中`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 10:06:25 +08:00
|
|
|
|
const icon = await Image.new(await makeTrayIconRGBA(...color), 32, 32);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
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 {
|
2026-03-10 14:41:13 +08:00
|
|
|
|
await mainWindow.unminimize();
|
|
|
|
|
|
await mainWindow.show();
|
|
|
|
|
|
await mainWindow.setFocus();
|
2026-03-23 09:10:32 +08:00
|
|
|
|
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(); },
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function updateTrayMenu() {
|
|
|
|
|
|
if (!trayIconInstance) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const menu = await buildTrayMenu();
|
|
|
|
|
|
await trayIconInstance.setMenu(menu);
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
addLog("WARN", `更新托盘菜单失败: ${e.message || e}`, "SYSTEM");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
async function setupTray() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const menu = await buildTrayMenu();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
// 初始状态:灰色(未开始)
|
2026-03-25 10:06:25 +08:00
|
|
|
|
const icon = await Image.new(await makeTrayIconRGBA(158, 158, 158), 32, 32);
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
|
|
|
|
|
trayIconInstance = await TrayIcon.new({
|
2026-03-10 14:41:13 +08:00
|
|
|
|
icon,
|
2026-03-23 09:10:32 +08:00
|
|
|
|
tooltip: `${APP_NAME} - 未开始`,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
menu,
|
|
|
|
|
|
menuOnLeftClick: false,
|
|
|
|
|
|
action: async (event) => {
|
|
|
|
|
|
if (event.type === "Click" && event.button === "Left") {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
const win = getCurrentWindow();
|
|
|
|
|
|
await win.unminimize();
|
|
|
|
|
|
await win.show();
|
|
|
|
|
|
await win.setFocus();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
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<string>("notification-action", (event) => {
|
|
|
|
|
|
if (event.payload === "postpone_1h") postponeMonitoring();
|
2026-03-10 15:20:06 +08:00
|
|
|
|
else if (event.payload === "go_handle") pauseForHandle();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
unlistenClose = await listen<void>("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");
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
|
|
|
|
|
if (config.value.silent_start) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await getCurrentWindow().hide();
|
|
|
|
|
|
addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
|
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
checkNetworkStatus();
|
2026-03-25 10:06:25 +08:00
|
|
|
|
checkForUpdates();
|
2026-03-10 14:41:13 +08:00
|
|
|
|
|
|
|
|
|
|
if (config.value.auto_monitor && rememberPassword.value) {
|
|
|
|
|
|
const canAutoStart = loginMode.value === "token"
|
|
|
|
|
|
? !!tokenSessionId.value
|
|
|
|
|
|
: !!(username.value && password.value);
|
|
|
|
|
|
if (canAutoStart) {
|
2026-03-23 09:10:32 +08:00
|
|
|
|
addLog("INFO", "自动登录并开始监测...", "SYSTEM");
|
2026-03-10 14:41:13 +08:00
|
|
|
|
await startMonitoring();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cleanup() {
|
|
|
|
|
|
if (monitorTimer) clearInterval(monitorTimer);
|
|
|
|
|
|
if (postponeTimer) clearTimeout(postponeTimer);
|
|
|
|
|
|
unlistenNotification?.();
|
|
|
|
|
|
unlistenClose?.();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 09:10:32 +08:00
|
|
|
|
// 监听状态变化,自动更新托盘图标和菜单
|
|
|
|
|
|
watch([isMonitoring, isPostponed, ticketCounts], () => {
|
|
|
|
|
|
updateTrayIcon();
|
|
|
|
|
|
updateTrayMenu();
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
return {
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
username, password, rememberPassword, loginMode, tokenSessionId,
|
|
|
|
|
|
message, isLoading,
|
2026-03-23 09:10:32 +08:00
|
|
|
|
isLoggedIn, isMonitoring, isPostponed, postponeResumeTime, autoStartEnabled,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
logs, networkStatus, config, ticketCounts,
|
|
|
|
|
|
showCloseDialog,
|
2026-03-25 10:06:25 +08:00
|
|
|
|
showUpdateDialog, updateInfo, updateProgress, isUpdating,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
// 方法
|
|
|
|
|
|
initialize, cleanup,
|
|
|
|
|
|
addLog, clearLogs, clearMessage, saveSettings, exportLogs,
|
2026-03-23 09:10:32 +08:00
|
|
|
|
startMonitoring, stopMonitoring, resumeMonitoring, manualCheck,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
testNotification, checkNetworkStatus, updateConfig,
|
|
|
|
|
|
minimizeToTray, handleCloseAction,
|
2026-03-25 10:06:25 +08:00
|
|
|
|
checkForUpdates, confirmUpdate, dismissUpdate,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-03-23 09:10:32 +08:00
|
|
|
|
|
|
|
|
|
|
if (import.meta.hot) {
|
|
|
|
|
|
import.meta.hot.accept(acceptHMRUpdate(useMonitorStore, import.meta.hot));
|
|
|
|
|
|
}
|