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

514 lines
16 KiB
TypeScript
Raw Normal View History

2026-03-31 17:08:30 +08:00
import { fetch } from "@tauri-apps/plugin-http";
import { defineStore, acceptHMRUpdate, storeToRefs } from "pinia";
import { ref, watch } from "vue";
import { useNotification } from "@/composables/useNotification";
import { useTray } from "@/composables/useTray";
import { LOGIN_URL, CHECK_URL, WORK_ORDER_URL } from "@/constants/api";
import { useAppStore } from "@/stores/app";
import { useLogStore } from "@/stores/log";
import { formatTime } from "@/utils/common";
export interface TicketCounts {
pending: number;
stayclose: number;
confirm: number;
workOrderCount: number;
lastCheck: string;
}
export const useMonitorStore = defineStore("monitor", () => {
// ===== 响应式数据 =====
const username = ref(""); // 登录用户名
const password = ref(""); // 登录密码
const rememberPassword = ref(false); // 是否记住密码
const loginMode = ref<"password" | "token">("password"); // 登录模式:密码 或 Token
const tokenSessionId = ref(""); // Token 模式下手动填入的 ASP.NET_SessionId
const isLoggedIn = ref(false); // 是否已完成登录
const isMonitoring = ref(false); // 是否正在轮询监测
const isPostponed = ref(false); // 监测是否处于推迟/暂停状态
const ticketCounts = ref<TicketCounts>({
pending: 0, // 待处理工单数
stayclose: 0, // 待关闭工单数
confirm: 0, // 待确认工单数
workOrderCount: 0, // 待审核工单数
lastCheck: "", // 最近一次检查时间
});
const postponeResumeTime = ref<number | null>(null); // 推迟/暂停结束的时间戳ms
// ===== 应用配置(来自 appStore=====
const appStore = useAppStore();
const { config } = storeToRefs(appStore);
// ===== 内部状态 =====
let sessionId = "";
let userGid = "";
let userName = "";
let monitorTimer: ReturnType<typeof setInterval> | null = null;
let postponeTimer: ReturnType<typeof setTimeout> | null = null;
// ===== 日志管理 =====
const { addLog } = useLogStore();
// ===== 通知 =====
const { notify } = useNotification();
// ===== API 请求 =====
/** 使用用户名/密码登录,成功后提取并存储 SessionId返回是否成功 */
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;
}
}
/** 查询待审核工单数量(分页接口,取 DataCount 字段) */
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;
}
}
/** 拉取工单状态计数触发通知isRetry=true 时不再递归重登 */
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;
const workOrderCount = await apiCheckWorkOrders();
ticketCounts.value = {
pending,
stayclose,
confirm,
workOrderCount,
lastCheck: formatTime(new Date()),
};
addLog(
"INFO",
`检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`,
"SCHEDULER",
);
if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) {
const msgs: string[] = [];
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`);
await notify("工单提醒", msgs.join(""));
}
} else {
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
if (!isRetry && isSessionExpiredMsg(result.msg)) {
await tryReLogin();
}
}
} catch (e: any) {
addLog("ERROR", `API 检查失败: ${e.message || e}`, "SCHEDULER");
}
}
/** 判断 API 返回消息是否属于会话过期类错误 */
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));
}
/** 会话过期后自动重新登录Token 模式下直接停止监测 */
async function tryReLogin(): Promise<void> {
if (loginMode.value === "token") {
addLog(
"ERROR",
"Token 已失效,监测已停止,请更换新的 Token 后重新开始",
"SESSION",
);
appStore.setMessage("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");
appStore.setMessage("会话已过期且自动重登录失败,请手动操作");
}
}
// ===== 认证与凭据 =====
/** 从 localStorage 加载已保存的登录凭据 */
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");
}
}
/** 根据 rememberPassword 决定写入或清除 localStorage 中的凭据 */
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 saveSettings() {
if (loginMode.value === "password") {
if (!username.value.trim() || !password.value.trim()) {
appStore.setMessage("请输入用户名和密码");
return;
}
} else {
if (!tokenSessionId.value.trim()) {
appStore.setMessage("请输入 Token (ASP.NET_SessionId)");
return;
}
}
saveCredentials();
appStore.setMessage("设置保存成功");
addLog("INFO", "用户凭据已保存", "CONFIG");
}
// ===== 监测调度 =====
/** 登录(密码模式)或设置 TokenToken 模式),然后启动定时轮询 */
async function startMonitoring() {
if (loginMode.value === "password") {
if (!username.value.trim() || !password.value.trim()) {
appStore.setMessage("请先设置用户名和密码");
return;
}
const ok = await apiLogin();
if (!ok) {
appStore.setMessage("登录失败,无法开始监测");
return;
}
} else {
if (!tokenSessionId.value.trim()) {
appStore.setMessage("请先输入 Token (ASP.NET_SessionId)");
return;
}
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");
appStore.setMessage(
loginMode.value === "password"
? `监测已开始,欢迎 ${userName}`
: "监测已开始Token 登录)",
);
}
/** 清除所有定时器,重置登录与监测状态 */
function stopMonitoring() {
if (monitorTimer) {
clearInterval(monitorTimer);
monitorTimer = null;
}
if (postponeTimer) {
clearTimeout(postponeTimer);
postponeTimer = null;
}
isPostponed.value = false;
postponeResumeTime.value = null;
isMonitoring.value = false;
isLoggedIn.value = false;
sessionId = "";
userGid = "";
addLog("INFO", "监测已停止", "MONITOR");
appStore.setMessage("监测已停止");
}
/** 将监测推迟 1 小时,到时自动恢复 */
function postponeMonitoring() {
if (!isMonitoring.value) return;
if (monitorTimer) {
clearInterval(monitorTimer);
monitorTimer = null;
}
if (postponeTimer) {
clearTimeout(postponeTimer);
}
isPostponed.value = true;
postponeResumeTime.value = Date.now() + 60 * 60 * 1000;
addLog("INFO", "监测已推迟1小时", "MONITOR");
appStore.setMessage("监测已推迟1小时将在1小时后自动恢复");
postponeTimer = setTimeout(
() => {
isPostponed.value = false;
postponeResumeTime.value = null;
postponeTimer = null;
apiCheckStatus();
monitorTimer = setInterval(
() => apiCheckStatus(),
config.value.check_interval * 1000,
);
addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR");
appStore.setMessage("推迟结束,监测已自动恢复");
},
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;
postponeResumeTime.value = Date.now() + minutes * 60 * 1000;
addLog("INFO", `点击"去处理",监测已暂停 ${minutes} 分钟`, "MONITOR");
appStore.setMessage(
`监测已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`,
);
postponeTimer = setTimeout(
() => {
isPostponed.value = false;
postponeResumeTime.value = null;
postponeTimer = null;
apiCheckStatus();
monitorTimer = setInterval(
() => apiCheckStatus(),
config.value.check_interval * 1000,
);
addLog("INFO", "暂停结束,监测已自动恢复", "MONITOR");
appStore.setMessage("暂停结束,监测已自动恢复");
},
minutes * 60 * 1000,
);
}
/** 手动提前结束暂停,立即恢复监测 */
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");
appStore.setMessage("监测已恢复");
}
/** 手动触发一次工单状态检查 */
async function manualCheck() {
if (!isLoggedIn.value) {
appStore.setMessage("请先开始监测(登录)");
return;
}
await apiCheckStatus();
appStore.setMessage("手动检查完成");
}
/** 发送一条测试通知,用于验证通知权限和声音配置 */
async function testNotification() {
await notify(
"工单提醒(测试)",
"待处理工单: 3待关闭工单: 1待确认工单: 2",
);
}
// ===== 系统托盘 =====
const { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray } = useTray(
isMonitoring,
isPostponed,
ticketCounts,
startMonitoring,
stopMonitoring,
);
// 检查间隔变化时重启定时器
watch(
() => config.value.check_interval,
(interval) => {
if (isMonitoring.value && monitorTimer) {
clearInterval(monitorTimer);
monitorTimer = setInterval(() => apiCheckStatus(), interval * 1000);
addLog("INFO", `检查间隔已更新为: ${interval}`, "CONFIG");
}
},
);
// 监听状态变化,自动更新托盘图标和菜单
watch(
[isMonitoring, isPostponed, ticketCounts],
() => {
updateTrayIcon();
updateTrayMenu();
},
{ deep: true },
);
return {
// 状态
username,
password,
rememberPassword,
loginMode,
tokenSessionId,
isLoggedIn,
isMonitoring,
isPostponed,
postponeResumeTime,
ticketCounts,
// 方法
saveSettings,
loadCredentials,
setupTray,
startMonitoring,
stopMonitoring,
postponeMonitoring,
pauseForHandle,
resumeMonitoring,
manualCheck,
testNotification,
minimizeToTray,
};
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useMonitorStore, import.meta.hot));
}