work-order-monitor/src/stores/monitor.ts

614 lines
21 KiB
TypeScript
Raw Normal View History

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",
});
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") {
addLog("WARN", "Token 模式下无法自动重登录,请更换新的 Token", "SESSION");
message.value = "Token 可能已过期,请更换新的 Token";
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);
}
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();
});
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,
};
});