498 lines
16 KiB
TypeScript
498 lines
16 KiB
TypeScript
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");
|
||
}
|
||
|
||
// ===== 监测调度 =====
|
||
/** 登录(密码模式)或设置 Token(Token 模式),然后启动定时轮询 */
|
||
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));
|
||
}
|