feat(): 添加待审核
This commit is contained in:
+8
-2
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DashboardOutlined,
|
||||
@@ -29,7 +30,12 @@ const statusItems = computed<StatusBarItem[]>(() => {
|
||||
return items;
|
||||
});
|
||||
|
||||
onMounted(() => store.initialize());
|
||||
onMounted(() => {
|
||||
store.initialize();
|
||||
if (import.meta.env.DEV) {
|
||||
getCurrentWindow().setTitle("工单系统监测【开发版】");
|
||||
}
|
||||
});
|
||||
onUnmounted(() => store.cleanup());
|
||||
</script>
|
||||
|
||||
@@ -43,7 +49,7 @@ onUnmounted(() => store.cleanup());
|
||||
<a-tabs v-model:activeKey="activeTab" tab-position="left" class="main-tabs">
|
||||
<a-tab-pane key="monitor">
|
||||
<template #tab>
|
||||
<span><DashboardOutlined /> 监控</span>
|
||||
<span><DashboardOutlined /> 监测</span>
|
||||
</template>
|
||||
<MonitorControl />
|
||||
</a-tab-pane>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
const showSettings = ref(true)
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
|
||||
// 页面加载时尝试从存储中读取配置
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const config = await invoke('load_config')
|
||||
if (config && config.username) {
|
||||
username.value = config.username
|
||||
password.value = config.password || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('No saved config found.')
|
||||
}
|
||||
})
|
||||
|
||||
// 保存设置并登录
|
||||
async function saveAndLogin() {
|
||||
try {
|
||||
await invoke('save_config', {
|
||||
config: { username: username.value, password: password.value }
|
||||
})
|
||||
await invoke('start_monitoring') // 开始轮询
|
||||
showSettings.value = false // 隐藏设置界面
|
||||
await invoke('minimize_to_tray') // 最小化到托盘
|
||||
} catch (error) {
|
||||
alert('保存失败: ' + error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showSettings" class="settings">
|
||||
<h2>登录设置</h2>
|
||||
<div>
|
||||
<label>用户名:</label>
|
||||
<input v-model="username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label>密码:</label>
|
||||
<input v-model="password" type="password" />
|
||||
</div>
|
||||
<button @click="saveAndLogin">登录</button>
|
||||
</div>
|
||||
<div v-else class="main">
|
||||
<h2>正在监控...</h2>
|
||||
<p>程序已最小化到系统托盘。</p>
|
||||
<button @click="() => showSettings = true">重新打开设置</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.settings, .main {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
input {
|
||||
margin: 5px 0;
|
||||
padding: 8px;
|
||||
width: 200px;
|
||||
}
|
||||
button {
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -55,10 +55,10 @@ const store = useMonitorStore();
|
||||
保存设置
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="store.isLoading" :disabled="store.isMonitoring" style="background: #52c41a; border-color: #52c41a" @click="store.startMonitoring()">
|
||||
开始监控
|
||||
开始监测
|
||||
</a-button>
|
||||
<a-button danger :disabled="store.isLoading || !store.isMonitoring" @click="store.stopMonitoring()">
|
||||
停止监控
|
||||
停止监测
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
MinusCircleOutlined,
|
||||
@@ -8,7 +11,35 @@ import {
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const store = useMonitorStore();
|
||||
|
||||
const countdown = ref("");
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function updateCountdown() {
|
||||
if (!store.postponeResumeTime) {
|
||||
countdown.value = "";
|
||||
return;
|
||||
}
|
||||
const remaining = Math.max(0, store.postponeResumeTime - Date.now());
|
||||
const d = dayjs.duration(remaining);
|
||||
const h = Math.floor(d.asHours());
|
||||
if (h > 0) {
|
||||
countdown.value = `${h}小时${String(d.minutes()).padStart(2, "0")}分${String(d.seconds()).padStart(2, "0")}秒`;
|
||||
} else {
|
||||
countdown.value = `${d.minutes()}分${String(d.seconds()).padStart(2, "0")}秒`;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countdownTimer = setInterval(updateCountdown, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,12 +54,22 @@ const store = useMonitorStore();
|
||||
<div class="ticket-card ticket-card--confirm">
|
||||
<a-statistic title="待确认" :value="store.ticketCounts.confirm" :value-style="{ color: store.ticketCounts.confirm > 0 ? '#1677ff' : '#3f8600' }" />
|
||||
</div>
|
||||
<div class="ticket-card ticket-card--workorder">
|
||||
<a-statistic title="待审核" :value="store.ticketCounts.workOrderCount" :value-style="{ color: store.ticketCounts.workOrderCount > 0 ? '#722ed1' : '#3f8600' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="store.ticketCounts.lastCheck" class="last-check">
|
||||
<ClockCircleOutlined /> 上次检查:{{ store.ticketCounts.lastCheck }}
|
||||
</div>
|
||||
|
||||
<div v-if="store.isPostponed && countdown" class="postpone-countdown">
|
||||
<ClockCircleOutlined /> 监测已暂停,<span class="countdown-time">{{ countdown }}</span> 后自动恢复
|
||||
<a-button size="small" type="link" @click="store.resumeMonitoring()">
|
||||
立即恢复
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-space wrap :size="[12, 12]" style="margin-top: 20px">
|
||||
<a-button :loading="store.isLoading" :disabled="!store.isLoggedIn" @click="store.manualCheck()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
@@ -56,7 +97,7 @@ const store = useMonitorStore();
|
||||
}
|
||||
.ticket-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -77,9 +118,28 @@ const store = useMonitorStore();
|
||||
background: #e6f4ff;
|
||||
border: 1px solid #91caff;
|
||||
}
|
||||
.ticket-card--workorder {
|
||||
background: #f9f0ff;
|
||||
border: 1px solid #d3adf7;
|
||||
}
|
||||
.last-check {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.postpone-countdown {
|
||||
font-size: 12px;
|
||||
color: #874d00;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
margin-top: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.countdown-time {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, watch } from "vue";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
@@ -16,6 +16,17 @@ const intervalTime = computed<Dayjs>({
|
||||
store.config.check_interval = Math.max(10, val.hour() * 3600 + val.minute() * 60 + val.second());
|
||||
},
|
||||
});
|
||||
|
||||
// 自动保存:config 变化后防抖 500ms 触发保存
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
() => store.config,
|
||||
() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => store.updateConfig(), 500);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,34 +48,38 @@ const intervalTime = computed<Dayjs>({
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开机自启动">
|
||||
<a-switch v-model:checked="store.config.auto_start" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="启动后自动监控">
|
||||
<a-switch v-model:checked="store.config.auto_monitor" />
|
||||
<div class="form-hint">需同时开启「记住密码」</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="显示系统通知">
|
||||
<a-switch v-model:checked="store.config.show_notifications" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="通知提示音">
|
||||
<a-switch
|
||||
v-model:checked="store.config.notification_sound"
|
||||
:disabled="!store.config.show_notifications"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider />
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">开机自启动</div>
|
||||
<a-switch v-model:checked="store.config.auto_start" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">
|
||||
启动后自动监测
|
||||
<div class="form-hint">需同时开启「记住密码」</div>
|
||||
</div>
|
||||
<a-switch v-model:checked="store.config.auto_monitor" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">
|
||||
静默启动
|
||||
<div class="form-hint">启动时直接最小化到托盘</div>
|
||||
</div>
|
||||
<a-switch v-model:checked="store.config.silent_start" />
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">显示系统通知</div>
|
||||
<a-switch v-model:checked="store.config.show_notifications" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">通知提示音</div>
|
||||
<a-switch
|
||||
v-model:checked="store.config.notification_sound"
|
||||
:disabled="!store.config.show_notifications"
|
||||
/>
|
||||
</div>
|
||||
<a-divider />
|
||||
<a-form-item label="点击「去处理」后暂停时长(分钟)">
|
||||
<a-input-number
|
||||
v-model:value="store.config.handle_pause_minutes"
|
||||
@@ -73,6 +88,7 @@ const intervalTime = computed<Dayjs>({
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-divider />
|
||||
<a-form-item label="关闭窗口行为">
|
||||
<a-radio-group v-model:value="store.config.close_action">
|
||||
<a-radio value="ask">每次询问</a-radio>
|
||||
@@ -80,11 +96,6 @@ const intervalTime = computed<Dayjs>({
|
||||
<a-radio value="close">直接退出</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="store.isLoading" @click="store.updateConfig()">
|
||||
保存配置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
@@ -92,6 +103,26 @@ const intervalTime = computed<Dayjs>({
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
margin: 0 -10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.switch-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@ function getNetworkStatusText() {
|
||||
<PauseCircleFilled v-if="store.isPostponed" />
|
||||
<SyncOutlined v-else :spin="true" />
|
||||
</template>
|
||||
{{ store.isPostponed ? "已推迟1小时" : "监控中" }}
|
||||
{{ store.isPostponed ? "已推迟1小时" : "监测中" }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
:color="store.networkStatus.api_reachable ? 'success' : store.networkStatus.is_connected ? 'warning' : 'error'"
|
||||
|
||||
@@ -21,6 +21,7 @@ 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`;
|
||||
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
@@ -33,6 +34,7 @@ export interface TicketCounts {
|
||||
pending: number;
|
||||
stayclose: number;
|
||||
confirm: number;
|
||||
workOrderCount: number;
|
||||
lastCheck: string;
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ export function useMonitor() {
|
||||
const autoStartEnabled = ref(false);
|
||||
const logs = ref<LogEntry[]>([]);
|
||||
const ticketCounts = ref<TicketCounts>({
|
||||
pending: 0, stayclose: 0, confirm: 0, lastCheck: "",
|
||||
pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
|
||||
});
|
||||
const networkStatus = ref({
|
||||
is_connected: false,
|
||||
@@ -172,6 +174,37 @@ export function useMonitor() {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiCheckStatus(isRetry = false): Promise<void> {
|
||||
if (!userGid) {
|
||||
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
|
||||
@@ -193,16 +226,18 @@ export function useMonitor() {
|
||||
const pending = d.PendingCount || 0;
|
||||
const stayclose = d.StaycloseCount || 0;
|
||||
const confirm = d.ConfirmCount || 0;
|
||||
const workOrderCount = await apiCheckWorkOrders();
|
||||
ticketCounts.value = {
|
||||
pending, stayclose, confirm,
|
||||
pending, stayclose, confirm, workOrderCount,
|
||||
lastCheck: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER");
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0) {
|
||||
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 {
|
||||
@@ -362,7 +397,7 @@ export function useMonitor() {
|
||||
message.value = "日志导出成功";
|
||||
}
|
||||
|
||||
// ===== 监控控制 =====
|
||||
// ===== 监测控制 =====
|
||||
async function startMonitoring() {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
message.value = "请先设置用户名和密码";
|
||||
@@ -371,15 +406,15 @@ export function useMonitor() {
|
||||
isLoading.value = true;
|
||||
const ok = await apiLogin();
|
||||
if (!ok) {
|
||||
message.value = "登录失败,无法开始监控";
|
||||
message.value = "登录失败,无法开始监测";
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
isMonitoring.value = true;
|
||||
await apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "监控已启动", "MONITOR");
|
||||
message.value = `监控已开始,欢迎 ${userName}!`;
|
||||
addLog("INFO", "监测已启动", "MONITOR");
|
||||
message.value = `监测已开始,欢迎 ${userName}!`;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -391,8 +426,8 @@ export function useMonitor() {
|
||||
isLoggedIn.value = false;
|
||||
sessionId = "";
|
||||
userGid = "";
|
||||
addLog("INFO", "监控已停止", "MONITOR");
|
||||
message.value = "监控已停止";
|
||||
addLog("INFO", "监测已停止", "MONITOR");
|
||||
message.value = "监测已停止";
|
||||
}
|
||||
|
||||
function postponeMonitoring() {
|
||||
@@ -400,20 +435,20 @@ export function useMonitor() {
|
||||
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
||||
if (postponeTimer) { clearTimeout(postponeTimer); }
|
||||
isPostponed.value = true;
|
||||
addLog("INFO", "监控已推迟1小时", "MONITOR");
|
||||
message.value = "监控已推迟1小时,将在1小时后自动恢复";
|
||||
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 = "推迟结束,监控已自动恢复";
|
||||
addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR");
|
||||
message.value = "推迟结束,监测已自动恢复";
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function manualCheck() {
|
||||
if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; }
|
||||
if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; }
|
||||
isLoading.value = true;
|
||||
await apiCheckStatus();
|
||||
message.value = "手动检查完成";
|
||||
@@ -522,7 +557,7 @@ export function useMonitor() {
|
||||
checkNetworkStatus();
|
||||
|
||||
if (config.value.auto_monitor && rememberPassword.value && username.value && password.value) {
|
||||
addLog("INFO", "自动登录并开始监控...", "SYSTEM");
|
||||
addLog("INFO", "自动登录并开始监测...", "SYSTEM");
|
||||
await startMonitoring();
|
||||
}
|
||||
});
|
||||
|
||||
+228
-53
@@ -1,5 +1,6 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { TrayIcon } from "@tauri-apps/api/tray";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
@@ -16,12 +17,13 @@ import {
|
||||
} 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";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
|
||||
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`;
|
||||
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
@@ -34,6 +36,7 @@ export interface TicketCounts {
|
||||
pending: number;
|
||||
stayclose: number;
|
||||
confirm: number;
|
||||
workOrderCount: number;
|
||||
lastCheck: string;
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
const autoStartEnabled = ref(false);
|
||||
const logs = ref<LogEntry[]>([]);
|
||||
const ticketCounts = ref<TicketCounts>({
|
||||
pending: 0, stayclose: 0, confirm: 0, lastCheck: "",
|
||||
pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
|
||||
});
|
||||
const networkStatus = ref({
|
||||
is_connected: false,
|
||||
@@ -64,6 +67,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
check_interval: 60,
|
||||
auto_start: false,
|
||||
auto_monitor: false,
|
||||
silent_start: false,
|
||||
show_notifications: true,
|
||||
notification_sound: true,
|
||||
network_timeout: 30,
|
||||
@@ -71,6 +75,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
handle_pause_minutes: 20,
|
||||
});
|
||||
const showCloseDialog = ref(false);
|
||||
const postponeResumeTime = ref<number | null>(null);
|
||||
|
||||
// ===== 内部状态 =====
|
||||
let sessionId = "";
|
||||
@@ -80,10 +85,11 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
let postponeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let unlistenNotification: UnlistenFn | null = null;
|
||||
let unlistenClose: UnlistenFn | null = null;
|
||||
let trayIconInstance: TrayIcon | 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")}`;
|
||||
return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
function addLog(level: string, msg: string, category: string) {
|
||||
@@ -179,6 +185,37 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiCheckStatus(isRetry = false): Promise<void> {
|
||||
if (!userGid && loginMode.value === "password") {
|
||||
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
|
||||
@@ -202,16 +239,18 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
const pending = d.PendingCount || 0;
|
||||
const stayclose = d.StaycloseCount || 0;
|
||||
const confirm = d.ConfirmCount || 0;
|
||||
const workOrderCount = await apiCheckWorkOrders();
|
||||
ticketCounts.value = {
|
||||
pending, stayclose, confirm,
|
||||
pending, stayclose, confirm, workOrderCount,
|
||||
lastCheck: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER");
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0) {
|
||||
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 {
|
||||
@@ -234,8 +273,8 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
|
||||
async function tryReLogin(): Promise<void> {
|
||||
if (loginMode.value === "token") {
|
||||
addLog("ERROR", "Token 已失效,监控已停止,请更换新的 Token 后重新开始", "SESSION");
|
||||
message.value = "Token 已失效,监控已停止";
|
||||
addLog("ERROR", "Token 已失效,监测已停止,请更换新的 Token 后重新开始", "SESSION");
|
||||
message.value = "Token 已失效,监测已停止";
|
||||
stopMonitoring();
|
||||
return;
|
||||
}
|
||||
@@ -392,7 +431,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
message.value = "日志导出成功";
|
||||
}
|
||||
|
||||
// ===== 监控控制 =====
|
||||
// ===== 监测控制 =====
|
||||
async function startMonitoring() {
|
||||
if (loginMode.value === "password") {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
@@ -402,7 +441,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
isLoading.value = true;
|
||||
const ok = await apiLogin();
|
||||
if (!ok) {
|
||||
message.value = "登录失败,无法开始监控";
|
||||
message.value = "登录失败,无法开始监测";
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -419,10 +458,10 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
isMonitoring.value = true;
|
||||
await apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "监控已启动", "MONITOR");
|
||||
addLog("INFO", "监测已启动", "MONITOR");
|
||||
message.value = loginMode.value === "password"
|
||||
? `监控已开始,欢迎 ${userName}!`
|
||||
: "监控已开始(Token 登录)";
|
||||
? `监测已开始,欢迎 ${userName}!`
|
||||
: "监测已开始(Token 登录)";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -430,12 +469,13 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
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");
|
||||
message.value = "监控已停止";
|
||||
addLog("INFO", "监测已停止", "MONITOR");
|
||||
message.value = "监测已停止";
|
||||
}
|
||||
|
||||
function postponeMonitoring() {
|
||||
@@ -443,15 +483,17 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
||||
if (postponeTimer) { clearTimeout(postponeTimer); }
|
||||
isPostponed.value = true;
|
||||
addLog("INFO", "监控已推迟1小时", "MONITOR");
|
||||
message.value = "监控已推迟1小时,将在1小时后自动恢复";
|
||||
postponeResumeTime.value = Date.now() + 60 * 60 * 1000;
|
||||
addLog("INFO", "监测已推迟1小时", "MONITOR");
|
||||
message.value = "监测已推迟1小时,将在1小时后自动恢复";
|
||||
postponeTimer = setTimeout(() => {
|
||||
isPostponed.value = false;
|
||||
postponeResumeTime.value = null;
|
||||
postponeTimer = null;
|
||||
apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "推迟结束,监控已自动恢复", "MONITOR");
|
||||
message.value = "推迟结束,监控已自动恢复";
|
||||
addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR");
|
||||
message.value = "推迟结束,监测已自动恢复";
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
@@ -461,20 +503,33 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
if (postponeTimer) { clearTimeout(postponeTimer); }
|
||||
isPostponed.value = true;
|
||||
const minutes = config.value.handle_pause_minutes;
|
||||
addLog("INFO", `点击"去处理",监控已暂停 ${minutes} 分钟`, "MONITOR");
|
||||
message.value = `监控已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`;
|
||||
postponeResumeTime.value = Date.now() + minutes * 60 * 1000;
|
||||
addLog("INFO", `点击"去处理",监测已暂停 ${minutes} 分钟`, "MONITOR");
|
||||
message.value = `监测已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`;
|
||||
postponeTimer = setTimeout(() => {
|
||||
isPostponed.value = false;
|
||||
postponeResumeTime.value = null;
|
||||
postponeTimer = null;
|
||||
apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "暂停结束,监控已自动恢复", "MONITOR");
|
||||
message.value = "暂停结束,监控已自动恢复";
|
||||
addLog("INFO", "暂停结束,监测已自动恢复", "MONITOR");
|
||||
message.value = "暂停结束,监测已自动恢复";
|
||||
}, 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");
|
||||
message.value = "监测已恢复";
|
||||
}
|
||||
|
||||
async function manualCheck() {
|
||||
if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; }
|
||||
if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; }
|
||||
isLoading.value = true;
|
||||
await apiCheckStatus();
|
||||
message.value = "手动检查完成";
|
||||
@@ -507,41 +562,143 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
}
|
||||
|
||||
// ===== 系统托盘 =====
|
||||
async function setupTray() {
|
||||
|
||||
/** 用 Canvas 渲染彩色圆形背景 + 白色"工"字,返回 RGBA 像素数据 */
|
||||
function makeTrayIconRGBA(r: number, g: number, b: number, size = 32): Uint8Array {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
// 圆形背景
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.fill();
|
||||
|
||||
// 白色"工"字
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = `bold ${Math.round(size * 0.62)}px "Microsoft YaHei","SimHei",sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("工", size / 2, size / 2 + 1);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
return new Uint8Array(imageData.data.buffer);
|
||||
}
|
||||
|
||||
/** 根据当前应用状态更新托盘图标和提示文字 */
|
||||
async function updateTrayIcon() {
|
||||
if (!trayIconInstance) return;
|
||||
try {
|
||||
const mainWindow = getCurrentWindow();
|
||||
const menu = await Menu.new({
|
||||
items: [
|
||||
await MenuItem.new({
|
||||
id: "show",
|
||||
text: "显示面板",
|
||||
action: async () => {
|
||||
const hasTickets =
|
||||
ticketCounts.value.pending > 0 ||
|
||||
ticketCounts.value.stayclose > 0 ||
|
||||
ticketCounts.value.confirm > 0 ||
|
||||
ticketCounts.value.workOrderCount > 0;
|
||||
|
||||
let color: [number, number, number];
|
||||
let tooltip: string;
|
||||
|
||||
if (!isMonitoring.value) {
|
||||
// 未开始监测 — 灰色
|
||||
color = [158, 158, 158];
|
||||
tooltip = `${APP_NAME} - 未开始`;
|
||||
} else if (isPostponed.value) {
|
||||
// 已暂停/推迟 — 橙色
|
||||
color = [255, 152, 0];
|
||||
tooltip = `${APP_NAME} - 已暂停`;
|
||||
} else if (hasTickets) {
|
||||
// 有待处理工单 — 红色
|
||||
const total = ticketCounts.value.pending + ticketCounts.value.stayclose + ticketCounts.value.confirm + ticketCounts.value.workOrderCount;
|
||||
color = [244, 67, 54];
|
||||
tooltip = `${APP_NAME} - 有 ${total} 条工单待处理!`;
|
||||
} else {
|
||||
// 监测中,无异常 — 绿色
|
||||
color = [76, 175, 80];
|
||||
tooltip = `${APP_NAME} - 监测中`;
|
||||
}
|
||||
|
||||
const icon = await Image.new(makeTrayIconRGBA(...color), 32, 32);
|
||||
await trayIconInstance.setIcon(icon);
|
||||
await trayIconInstance.setTooltip(tooltip);
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `更新托盘图标失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
async function buildTrayMenu() {
|
||||
const mainWindow = getCurrentWindow();
|
||||
return 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: "toggle_monitor",
|
||||
text: isMonitoring.value ? "停止监测" : "开始监测",
|
||||
action: async () => {
|
||||
if (isMonitoring.value) {
|
||||
stopMonitoring();
|
||||
} else {
|
||||
await mainWindow.unminimize();
|
||||
await mainWindow.show();
|
||||
await mainWindow.setFocus();
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "quit",
|
||||
text: "退出",
|
||||
action: async () => { await mainWindow.destroy(); },
|
||||
}),
|
||||
],
|
||||
});
|
||||
await startMonitoring();
|
||||
}
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "open_website",
|
||||
text: "打开工单网站",
|
||||
action: async () => {
|
||||
await openUrl("https://crm.yunvip123.com");
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "quit",
|
||||
text: "退出",
|
||||
action: async () => { await mainWindow.destroy(); },
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const iconPath = await resolveResource("icons/32x32.png");
|
||||
const icon = await Image.fromPath(iconPath);
|
||||
async function updateTrayMenu() {
|
||||
if (!trayIconInstance) return;
|
||||
try {
|
||||
const menu = await buildTrayMenu();
|
||||
await trayIconInstance.setMenu(menu);
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `更新托盘菜单失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
await TrayIcon.new({
|
||||
async function setupTray() {
|
||||
try {
|
||||
const menu = await buildTrayMenu();
|
||||
|
||||
// 初始状态:灰色(未开始)
|
||||
const icon = await Image.new(makeTrayIconRGBA(158, 158, 158), 32, 32);
|
||||
|
||||
trayIconInstance = await TrayIcon.new({
|
||||
icon,
|
||||
tooltip: APP_NAME,
|
||||
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();
|
||||
const win = getCurrentWindow();
|
||||
await win.unminimize();
|
||||
await win.show();
|
||||
await win.setFocus();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -597,6 +754,14 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
}
|
||||
});
|
||||
addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||
|
||||
if (config.value.silent_start) {
|
||||
try {
|
||||
await getCurrentWindow().hide();
|
||||
addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
checkNetworkStatus();
|
||||
|
||||
if (config.value.auto_monitor && rememberPassword.value) {
|
||||
@@ -604,7 +769,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
? !!tokenSessionId.value
|
||||
: !!(username.value && password.value);
|
||||
if (canAutoStart) {
|
||||
addLog("INFO", "自动登录并开始监控...", "SYSTEM");
|
||||
addLog("INFO", "自动登录并开始监测...", "SYSTEM");
|
||||
await startMonitoring();
|
||||
}
|
||||
}
|
||||
@@ -617,18 +782,28 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||
unlistenClose?.();
|
||||
}
|
||||
|
||||
// 监听状态变化,自动更新托盘图标和菜单
|
||||
watch([isMonitoring, isPostponed, ticketCounts], () => {
|
||||
updateTrayIcon();
|
||||
updateTrayMenu();
|
||||
}, { deep: true });
|
||||
|
||||
return {
|
||||
// 状态
|
||||
username, password, rememberPassword, loginMode, tokenSessionId,
|
||||
message, isLoading,
|
||||
isLoggedIn, isMonitoring, isPostponed, autoStartEnabled,
|
||||
isLoggedIn, isMonitoring, isPostponed, postponeResumeTime, autoStartEnabled,
|
||||
logs, networkStatus, config, ticketCounts,
|
||||
showCloseDialog,
|
||||
// 方法
|
||||
initialize, cleanup,
|
||||
addLog, clearLogs, clearMessage, saveSettings, exportLogs,
|
||||
startMonitoring, stopMonitoring, manualCheck,
|
||||
startMonitoring, stopMonitoring, resumeMonitoring, manualCheck,
|
||||
testNotification, checkNetworkStatus, updateConfig,
|
||||
minimizeToTray, handleCloseAction,
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useMonitorStore, import.meta.hot));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user