2026-03-10 14:41:13 +08:00
|
|
|
|
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<LogEntry[]>([]);
|
|
|
|
|
|
const ticketCounts = ref<TicketCounts>({
|
|
|
|
|
|
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",
|
2026-03-10 15:20:06 +08:00
|
|
|
|
handle_pause_minutes: 20,
|
2026-03-10 14:41:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
const showCloseDialog = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 内部状态 =====
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 工具函数 =====
|
|
|
|
|
|
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<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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
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<void> {
|
|
|
|
|
|
if (loginMode.value === "token") {
|
2026-03-10 15:20:06 +08:00
|
|
|
|
addLog("ERROR", "Token 已失效,监控已停止,请更换新的 Token 后重新开始", "SESSION");
|
|
|
|
|
|
message.value = "Token 已失效,监控已停止";
|
|
|
|
|
|
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 = "日志导出成功";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 监控控制 =====
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 14:41:13 +08:00
|
|
|
|
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<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");
|
|
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|