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

498 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore, acceptHMRUpdate, storeToRefs } from "pinia";
import { ref, watch } from "vue";
import { login, checkWorkOrders, checkStatus } from "@/api/monitor";
import { useNotification } from "@/composables/useNotification";
import { useTray } from "@/composables/useTray";
import { useAppStore } from "@/stores/app";
import { useLogStore } from "@/stores/log";
import { formatTime, getErrorMessage } from "@/utils/common";
import type { TicketCounts, CheckStatusResponse } from "@/api/types";
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 { data, sessionId: newSessionId } = await login(
username.value,
password.value,
);
if (newSessionId) sessionId = newSessionId;
addLog("DEBUG", `SessionId: ${sessionId || "(未提取)"}`, "LOGIN");
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: unknown) {
addLog("ERROR", `登录请求失败: ${getErrorMessage(e)}`, "LOGIN");
return false;
}
}
/** 查询待审核工单数量(分页接口,取 DataCount 字段) */
async function apiCheckWorkOrders(): Promise<number> {
try {
const result = await checkWorkOrders(sessionId);
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: unknown) {
addLog(
"ERROR",
`待审核 API 检查失败: ${getErrorMessage(e)}`,
"SCHEDULER",
);
return 0;
}
}
/** 发送工单状态检查请求 */
async function performStatusCheckRequest() {
return await checkStatus(sessionId, userGid);
}
/**
* 处理检查到的工单数据并更新状态
* @param data 接口返回的原始计数数据
*/
async function updateTicketCountsAndLog(data: CheckStatusResponse) {
const pending = data.PendingCount || 0;
const stayclose = data.StaycloseCount || 0;
const confirm = data.ConfirmCount || 0;
const workOrderCount = await apiCheckWorkOrders();
const counts = {
pending,
stayclose,
confirm,
workOrderCount,
lastCheck: formatTime(new Date()),
};
ticketCounts.value = counts;
addLog(
"INFO",
`检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`,
"SCHEDULER",
);
return counts;
}
/** 发送工单提醒通知 */
async function sendTicketNotifications(counts: TicketCounts) {
const msgs: string[] = [];
if (counts.pending > 0) msgs.push(`待处理工单: ${counts.pending}`);
if (counts.stayclose > 0) msgs.push(`待关闭工单: ${counts.stayclose}`);
if (counts.confirm > 0) msgs.push(`待确认工单: ${counts.confirm}`);
if (counts.workOrderCount > 0)
msgs.push(`待审核: ${counts.workOrderCount}`);
if (msgs.length > 0) {
await notify("工单提醒", msgs.join(""));
}
}
/** 拉取工单状态计数触发通知isRetry=true 时不再递归重登 */
async function apiCheckStatus(isRetry = false): Promise<void> {
// 1. 预检查:如果会话失效且是密码模式,尝试重登
if (!userGid && loginMode.value === "password") {
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
if (!isRetry) await tryReLogin();
return;
}
try {
const result = await performStatusCheckRequest();
if (result.success && result.data) {
// 2. 处理成功检查
const counts = await updateTicketCountsAndLog(result.data);
await sendTicketNotifications(counts);
} else {
// 3. 处理业务失败
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
if (!isRetry && isSessionExpiredMsg(result.msg)) {
await tryReLogin();
}
}
} catch (e: unknown) {
// 4. 处理网络/系统异常
addLog("ERROR", `API 检查失败: ${getErrorMessage(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: unknown) {
addLog("ERROR", `加载凭据失败: ${getErrorMessage(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: unknown) {
addLog("ERROR", `保存凭据失败: ${getErrorMessage(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));
}