feat(): 添加待审核

This commit is contained in:
tsl 2026-03-23 09:10:32 +08:00
parent 17618ec3a0
commit 92055dd6a0
71 changed files with 541 additions and 195 deletions

View File

@ -38,7 +38,8 @@
"Bash(start \"\" \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\\\\tauri-app_0.1.0_x64-setup.exe\")", "Bash(start \"\" \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\\\\tauri-app_0.1.0_x64-setup.exe\")",
"Bash(pnpm run build)", "Bash(pnpm run build)",
"Bash(pnpm add pinia)", "Bash(pnpm add pinia)",
"Bash(pnpm add dayjs)" "Bash(pnpm add dayjs)",
"Bash(pnpm tauri icon public/gong.svg)"
] ]
} }
} }

View File

@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 项目概述 ## 项目概述
这是一个基于 **Tauri 2.x + Vue 3** 的工单监控桌面应用,用于监控 CRM 系统crm.yunvip123.com的待处理工单并发送系统通知。 这是一个基于 **Tauri 2.x + Vue 3** 的工单监测桌面应用,用于监测 CRM 系统crm.yunvip123.com的待处理工单并发送系统通知。
## 常用命令 ## 常用命令
@ -33,7 +33,7 @@ cd src-tauri && cargo clippy
**前端驱动架构**所有业务逻辑API 调用、定时任务、通知)都在 Vue 前端实现Rust 后端仅负责系统级功能。 **前端驱动架构**所有业务逻辑API 调用、定时任务、通知)都在 Vue 前端实现Rust 后端仅负责系统级功能。
### 前端src/ ### 前端src/
- `src/App.vue` - 主界面,包含全部业务逻辑:用户认证、监调度、通知、网络检测、配置管理、系统托盘 - `src/App.vue` - 主界面,包含全部业务逻辑:用户认证、监调度、通知、网络检测、配置管理、系统托盘
- 使用 Vue 3 Composition API`<script setup>` - 使用 Vue 3 Composition API`<script setup>`
- Vite 开发服务器端口1420HMR 端口1421 - Vite 开发服务器端口1420HMR 端口1421
- 系统托盘使用 `@tauri-apps/api/tray``@tauri-apps/api/menu` 实现 - 系统托盘使用 `@tauri-apps/api/tray``@tauri-apps/api/menu` 实现

View File

@ -1,6 +1,6 @@
# 系统监应用程序 # 系统监应用程序
这是一个基于 Tauri + Vue 开发的系统监应用程序,具有以下功能: 这是一个基于 Tauri + Vue 开发的系统监应用程序,具有以下功能:
## 功能特性 ## 功能特性
@ -41,7 +41,7 @@ pnpm tauri build
- 右键点击托盘图标选择"显示设置"打开设置界面 - 右键点击托盘图标选择"显示设置"打开设置界面
- 输入用户名和密码,点击"保存设置" - 输入用户名和密码,点击"保存设置"
### 3. 登录和监 ### 3. 登录和监
- 保存设置后点击"登录"按钮 - 保存设置后点击"登录"按钮
- 登录成功后会自动启动定时任务每1分钟执行一次 - 登录成功后会自动启动定时任务每1分钟执行一次
- 系统会根据API返回值显示通知消息 - 系统会根据API返回值显示通知消息

View File

@ -0,0 +1,100 @@
fetch("https://crm.yunvip123.com/api/DemandManage/GetWorkOrderListPage", {
headers: {
accept: "application/json, text/javascript, */*; q=0.01",
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"sec-ch-ua": '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest",
},
referrer: "https://crm.yunvip123.com/WebUI/DemandManagement/DemandListNew.html?v=2.0.4.12",
body: "DL_ProductID=&DL_ID=&isSelect=8&Solution=&OrderType=&DL_TaskName=&CreatGID=&CreatTimeStart=&CreatTimeEnd=&SloveGID=&SloveTimeStart=&SloveTimeEnd=&ShowTable=DemandList&isShow=3&IsExport=0&PageIndex=1&PageSize=20",
method: "POST",
mode: "cors",
credentials: "include",
});
const requsetBody = {
DL_ProductID: null,
DL_ID: null,
isSelect: 8,
Solution: null,
OrderType: null,
DL_TaskName: null,
CreatGID: null,
CreatTimeStart: null,
CreatTimeEnd: null,
SloveGID: null,
SloveTimeStart: null,
SloveTimeEnd: null,
ShowTable: null,
DemandList: null,
isShow: 3,
IsExport: 0,
PageIndex: 1,
PageSize: 20,
};
const responseBody = {
success: true,
code: null,
msg: "执行成功",
data: {
PageTotal: 1,
PageSize: 20,
DataCount: 1,
PageIndex: 1,
DataList: [
{
ID: 58624,
DL_OrderType: 2,
DL_Status: 350,
DL_StatusID: null,
DL_ProductID: 64,
DL_ProductName: "六个羽友",
DL_ID: 1039,
DL_ItemID: "六个羽友2.21",
DL_ProblemDegree: "1",
DL_Priority: null,
DL_TaskName: "一次批量订场,立即支付页面转圈圈",
DL_ProblemSource: "",
DL_CreatorGID: "229707a9-8eac-4540-b1d0-f64ce87ce054",
DL_Creator: "龚诚",
DL_CreateTime: "2026-03-05 13:54:06",
DL_AffirmGID: "229707a9-8eac-4540-b1d0-f64ce87ce054",
DL_AffirmName: "龚诚",
DL_ConfirmTime: null,
DL_AffirmRemark: null,
DL_AssignGID: "512860ad-aa4a-4268-a5d9-6c7d2e28680b",
DL_AssignName: "A田森林",
DL_AssignRemark: null,
DL_SolveGID: null,
DL_SolveName: null,
DL_SolveTime: null,
DL_SolveRemark: null,
DL_CloseGID: null,
DL_CloseName: null,
DL_CloseTime: null,
DL_CloseRemark: null,
DL_Remark: null,
DL_AuditStatus: null,
DL_DemandContent:
' <p class="txtContent"><font color="#333333">一次批量订场(32个场次),立即支付页面转圈圈</font></p><p class="txtContent"><img src="https://img1.yunvip123.com/202603/134171636069619186.png" style="max-width: 100% !important; vertical-align: middle !important;"><img src="https://img1.yunvip123.com/202603/134171636323013515.png" style="max-width: 100% !important; vertical-align: middle !important;"></p><p class="txtContent"></p>\n ',
DL_TestContent: "",
DL_DemandName: "一次批量订场,立即支付页面转圈圈",
DL_OrderLevel: 50,
DL_RepeatID: null,
DL_RepeatStatus: null,
DL_Identitying: null,
DL_WorkHour: null,
DL_ConfirmState: null,
},
],
StatisticsInfo: null,
TrendData: null,
},
};

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "tauri-app", "name": "tauri-app",
"version": "0.1.0", "version": "0.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tauri-app", "name": "tauri-app",
"version": "0.1.0", "version": "0.1.3",
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@tauri-apps/api": "^2.10.1", "@tauri-apps/api": "^2.10.1",

View File

@ -1,7 +1,7 @@
{ {
"name": "tauri-app", "name": "tauri-app",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

8
public/gong.svg Normal file
View File

@ -0,0 +1,8 @@
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<rect stroke="#000" id="svg_2" height="256" width="259" y="1" x="-3" fill="#fff"/>
<text stroke-width="4" font-weight="normal" xml:space="preserve" text-anchor="start" font-family="'Alumni Sans'" font-size="128" id="svg_1" y="166.5" x="64" stroke="#000" fill="#000000"></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 393 B

2
src-tauri/Cargo.lock generated
View File

@ -4197,7 +4197,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-app" name = "tauri-app"
version = "0.1.0" version = "0.1.2"
dependencies = [ dependencies = [
"env_logger", "env_logger",
"log", "log",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-app" name = "tauri-app"
version = "0.1.0" version = "0.1.2"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"

View File

@ -38,6 +38,7 @@
"core:window:allow-show", "core:window:allow-show",
"core:window:allow-hide", "core:window:allow-hide",
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-set-title",
"core:window:allow-unminimize", "core:window:allow-unminimize",
"core:image:default", "core:image:default",
"core:image:allow-from-path" "core:image:allow-from-path"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 353 B

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

View File

@ -52,7 +52,7 @@ fn handle_callback_request(app_handle: &tauri::AppHandle, mut stream: std::net::
let (action, body) = if request.contains("GET /postpone") { let (action, body) = if request.contains("GET /postpone") {
let _ = app_handle.emit("notification-action", "postpone_1h"); let _ = app_handle.emit("notification-action", "postpone_1h");
("postpone", simple_page("已推迟1小时将在1小时后恢复")) ("postpone", simple_page("已推迟1小时将在1小时后恢复"))
} else if request.contains("GET /handle") { } else if request.contains("GET /handle") {
let _ = app_handle.emit("notification-action", "go_handle"); let _ = app_handle.emit("notification-action", "go_handle");
("handle", redirect_page("https://crm.yunvip123.com/index.html")) ("handle", redirect_page("https://crm.yunvip123.com/index.html"))

View File

@ -18,7 +18,7 @@ fn xml_escape(s: &str) -> String {
/// 发送带交互按钮的 Toast 通知 /// 发送带交互按钮的 Toast 通知
/// ///
/// - 点击消息体 → 关闭通知(非打包应用不支持 foreground 激活) /// - 点击消息体 → 关闭通知(非打包应用不支持 foreground 激活)
/// - 点击"推迟1小时" → 通过 localhost 回调暂停监并弹出应用窗口 /// - 点击"推迟1小时" → 通过 localhost 回调暂停监并弹出应用窗口
/// - 点击"去处理" → 直接打开 CRM 网站 /// - 点击"去处理" → 直接打开 CRM 网站
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn send_interactive_toast( pub fn send_interactive_toast(

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "工单系统监测", "productName": "工单系统监测",
"version": "0.1.2", "version": "0.1.3",
"identifier": "com.administrator.tauri-app", "identifier": "com.administrator.tauri-app",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue"; import { ref, computed, onMounted, onUnmounted } from "vue";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { import {
SettingOutlined, SettingOutlined,
DashboardOutlined, DashboardOutlined,
@ -29,7 +30,12 @@ const statusItems = computed<StatusBarItem[]>(() => {
return items; return items;
}); });
onMounted(() => store.initialize()); onMounted(() => {
store.initialize();
if (import.meta.env.DEV) {
getCurrentWindow().setTitle("工单系统监测【开发版】");
}
});
onUnmounted(() => store.cleanup()); onUnmounted(() => store.cleanup());
</script> </script>
@ -43,7 +49,7 @@ onUnmounted(() => store.cleanup());
<a-tabs v-model:activeKey="activeTab" tab-position="left" class="main-tabs"> <a-tabs v-model:activeKey="activeTab" tab-position="left" class="main-tabs">
<a-tab-pane key="monitor"> <a-tab-pane key="monitor">
<template #tab> <template #tab>
<span><DashboardOutlined /> </span> <span><DashboardOutlined /> </span>
</template> </template>
<MonitorControl /> <MonitorControl />
</a-tab-pane> </a-tab-pane>

View File

@ -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>

View File

@ -55,10 +55,10 @@ const store = useMonitorStore();
保存设置 保存设置
</a-button> </a-button>
<a-button type="primary" :loading="store.isLoading" :disabled="store.isMonitoring" style="background: #52c41a; border-color: #52c41a" @click="store.startMonitoring()"> <a-button type="primary" :loading="store.isLoading" :disabled="store.isMonitoring" style="background: #52c41a; border-color: #52c41a" @click="store.startMonitoring()">
开始监 开始监
</a-button> </a-button>
<a-button danger :disabled="store.isLoading || !store.isMonitoring" @click="store.stopMonitoring()"> <a-button danger :disabled="store.isLoading || !store.isMonitoring" @click="store.stopMonitoring()">
停止监 停止监
</a-button> </a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>

View File

@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { import {
ReloadOutlined, ReloadOutlined,
MinusCircleOutlined, MinusCircleOutlined,
@ -8,7 +11,35 @@ import {
} from "@ant-design/icons-vue"; } from "@ant-design/icons-vue";
import { useMonitorStore } from "../stores/monitor"; import { useMonitorStore } from "../stores/monitor";
dayjs.extend(duration);
const store = useMonitorStore(); 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> </script>
<template> <template>
@ -23,12 +54,22 @@ const store = useMonitorStore();
<div class="ticket-card ticket-card--confirm"> <div class="ticket-card ticket-card--confirm">
<a-statistic title="待确认" :value="store.ticketCounts.confirm" :value-style="{ color: store.ticketCounts.confirm > 0 ? '#1677ff' : '#3f8600' }" /> <a-statistic title="待确认" :value="store.ticketCounts.confirm" :value-style="{ color: store.ticketCounts.confirm > 0 ? '#1677ff' : '#3f8600' }" />
</div> </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>
<div v-if="store.ticketCounts.lastCheck" class="last-check"> <div v-if="store.ticketCounts.lastCheck" class="last-check">
<ClockCircleOutlined /> 上次检查{{ store.ticketCounts.lastCheck }} <ClockCircleOutlined /> 上次检查{{ store.ticketCounts.lastCheck }}
</div> </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-space wrap :size="[12, 12]" style="margin-top: 20px">
<a-button :loading="store.isLoading" :disabled="!store.isLoggedIn" @click="store.manualCheck()"> <a-button :loading="store.isLoading" :disabled="!store.isLoggedIn" @click="store.manualCheck()">
<template #icon><ReloadOutlined /></template> <template #icon><ReloadOutlined /></template>
@ -56,7 +97,7 @@ const store = useMonitorStore();
} }
.ticket-dashboard { .ticket-dashboard {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -77,9 +118,28 @@ const store = useMonitorStore();
background: #e6f4ff; background: #e6f4ff;
border: 1px solid #91caff; border: 1px solid #91caff;
} }
.ticket-card--workorder {
background: #f9f0ff;
border: 1px solid #d3adf7;
}
.last-check { .last-check {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-bottom: 4px; 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> </style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed, watch } from "vue";
import dayjs, { type Dayjs } from "dayjs"; import dayjs, { type Dayjs } from "dayjs";
import { useMonitorStore } from "../stores/monitor"; 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()); 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> </script>
<template> <template>
@ -37,34 +48,38 @@ const intervalTime = computed<Dayjs>({
style="width: 100%" style="width: 100%"
/> />
</a-form-item> </a-form-item>
<a-row :gutter="24"> <a-divider />
<a-col :span="12"> <div class="switch-row">
<a-form-item label="开机自启动"> <div class="switch-label">开机自启动</div>
<a-switch v-model:checked="store.config.auto_start" /> <a-switch v-model:checked="store.config.auto_start" />
</a-form-item> </div>
</a-col> <div class="switch-row">
<a-col :span="12"> <div class="switch-label">
<a-form-item label="启动后自动监控"> 启动后自动监测
<a-switch v-model:checked="store.config.auto_monitor" /> <div class="form-hint">需同时开启记住密码</div>
<div class="form-hint">需同时开启记住密码</div> </div>
</a-form-item> <a-switch v-model:checked="store.config.auto_monitor" />
</a-col> </div>
</a-row> <div class="switch-row">
<a-row :gutter="24"> <div class="switch-label">
<a-col :span="12"> 静默启动
<a-form-item label="显示系统通知"> <div class="form-hint">启动时直接最小化到托盘</div>
<a-switch v-model:checked="store.config.show_notifications" /> </div>
</a-form-item> <a-switch v-model:checked="store.config.silent_start" />
</a-col> </div>
<a-col :span="12"> <a-divider />
<a-form-item label="通知提示音"> <div class="switch-row">
<a-switch <div class="switch-label">显示系统通知</div>
v-model:checked="store.config.notification_sound" <a-switch v-model:checked="store.config.show_notifications" />
:disabled="!store.config.show_notifications" </div>
/> <div class="switch-row">
</a-form-item> <div class="switch-label">通知提示音</div>
</a-col> <a-switch
</a-row> v-model:checked="store.config.notification_sound"
:disabled="!store.config.show_notifications"
/>
</div>
<a-divider />
<a-form-item label="点击「去处理」后暂停时长(分钟)"> <a-form-item label="点击「去处理」后暂停时长(分钟)">
<a-input-number <a-input-number
v-model:value="store.config.handle_pause_minutes" v-model:value="store.config.handle_pause_minutes"
@ -73,6 +88,7 @@ const intervalTime = computed<Dayjs>({
style="width: 100%" style="width: 100%"
/> />
</a-form-item> </a-form-item>
<a-divider />
<a-form-item label="关闭窗口行为"> <a-form-item label="关闭窗口行为">
<a-radio-group v-model:value="store.config.close_action"> <a-radio-group v-model:value="store.config.close_action">
<a-radio value="ask">每次询问</a-radio> <a-radio value="ask">每次询问</a-radio>
@ -80,11 +96,6 @@ const intervalTime = computed<Dayjs>({
<a-radio value="close">直接退出</a-radio> <a-radio value="close">直接退出</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item>
<a-button type="primary" :loading="store.isLoading" @click="store.updateConfig()">
保存配置
</a-button>
</a-form-item>
</a-form> </a-form>
</template> </template>
@ -92,6 +103,26 @@ const intervalTime = computed<Dayjs>({
.form-hint { .form-hint {
font-size: 12px; font-size: 12px;
color: #999; 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> </style>

View File

@ -32,7 +32,7 @@ function getNetworkStatusText() {
<PauseCircleFilled v-if="store.isPostponed" /> <PauseCircleFilled v-if="store.isPostponed" />
<SyncOutlined v-else :spin="true" /> <SyncOutlined v-else :spin="true" />
</template> </template>
{{ store.isPostponed ? "已推迟1小时" : "监中" }} {{ store.isPostponed ? "已推迟1小时" : "监中" }}
</a-tag> </a-tag>
<a-tag <a-tag
:color="store.networkStatus.api_reachable ? 'success' : store.networkStatus.is_connected ? 'warning' : 'error'" :color="store.networkStatus.api_reachable ? 'success' : store.networkStatus.is_connected ? 'warning' : 'error'"

View File

@ -21,6 +21,7 @@ export const APP_NAME = "工单系统监测";
const API_BASE = "https://crm.yunvip123.com/api"; const API_BASE = "https://crm.yunvip123.com/api";
const LOGIN_URL = `${API_BASE}/SystemUser/Login`; const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`; const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
export interface LogEntry { export interface LogEntry {
timestamp: string; timestamp: string;
@ -33,6 +34,7 @@ export interface TicketCounts {
pending: number; pending: number;
stayclose: number; stayclose: number;
confirm: number; confirm: number;
workOrderCount: number;
lastCheck: string; lastCheck: string;
} }
@ -49,7 +51,7 @@ export function useMonitor() {
const autoStartEnabled = ref(false); const autoStartEnabled = ref(false);
const logs = ref<LogEntry[]>([]); const logs = ref<LogEntry[]>([]);
const ticketCounts = ref<TicketCounts>({ const ticketCounts = ref<TicketCounts>({
pending: 0, stayclose: 0, confirm: 0, lastCheck: "", pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
}); });
const networkStatus = ref({ const networkStatus = ref({
is_connected: false, 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> { async function apiCheckStatus(isRetry = false): Promise<void> {
if (!userGid) { if (!userGid) {
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER"); addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
@ -193,16 +226,18 @@ export function useMonitor() {
const pending = d.PendingCount || 0; const pending = d.PendingCount || 0;
const stayclose = d.StaycloseCount || 0; const stayclose = d.StaycloseCount || 0;
const confirm = d.ConfirmCount || 0; const confirm = d.ConfirmCount || 0;
const workOrderCount = await apiCheckWorkOrders();
ticketCounts.value = { ticketCounts.value = {
pending, stayclose, confirm, pending, stayclose, confirm, workOrderCount,
lastCheck: formatTime(new Date()), lastCheck: formatTime(new Date()),
}; };
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER"); addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER");
if (pending > 0 || stayclose > 0 || confirm > 0) { if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) {
const msgs: string[] = []; const msgs: string[] = [];
if (pending > 0) msgs.push(`待处理工单: ${pending}`); if (pending > 0) msgs.push(`待处理工单: ${pending}`);
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`); if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`); if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`);
await notify("工单提醒", msgs.join("")); await notify("工单提醒", msgs.join(""));
} }
} else { } else {
@ -362,7 +397,7 @@ export function useMonitor() {
message.value = "日志导出成功"; message.value = "日志导出成功";
} }
// ===== 监控制 ===== // ===== 监控制 =====
async function startMonitoring() { async function startMonitoring() {
if (!username.value.trim() || !password.value.trim()) { if (!username.value.trim() || !password.value.trim()) {
message.value = "请先设置用户名和密码"; message.value = "请先设置用户名和密码";
@ -371,15 +406,15 @@ export function useMonitor() {
isLoading.value = true; isLoading.value = true;
const ok = await apiLogin(); const ok = await apiLogin();
if (!ok) { if (!ok) {
message.value = "登录失败,无法开始监"; message.value = "登录失败,无法开始监";
isLoading.value = false; isLoading.value = false;
return; return;
} }
isMonitoring.value = true; isMonitoring.value = true;
await apiCheckStatus(); await apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "监已启动", "MONITOR"); addLog("INFO", "监已启动", "MONITOR");
message.value = `已开始,欢迎 ${userName}`; message.value = `已开始,欢迎 ${userName}`;
isLoading.value = false; isLoading.value = false;
} }
@ -391,8 +426,8 @@ export function useMonitor() {
isLoggedIn.value = false; isLoggedIn.value = false;
sessionId = ""; sessionId = "";
userGid = ""; userGid = "";
addLog("INFO", "监已停止", "MONITOR"); addLog("INFO", "监已停止", "MONITOR");
message.value = "监已停止"; message.value = "监已停止";
} }
function postponeMonitoring() { function postponeMonitoring() {
@ -400,20 +435,20 @@ export function useMonitor() {
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
if (postponeTimer) { clearTimeout(postponeTimer); } if (postponeTimer) { clearTimeout(postponeTimer); }
isPostponed.value = true; isPostponed.value = true;
addLog("INFO", "监已推迟1小时", "MONITOR"); addLog("INFO", "监已推迟1小时", "MONITOR");
message.value = "监已推迟1小时将在1小时后自动恢复"; message.value = "监已推迟1小时将在1小时后自动恢复";
postponeTimer = setTimeout(() => { postponeTimer = setTimeout(() => {
isPostponed.value = false; isPostponed.value = false;
postponeTimer = null; postponeTimer = null;
apiCheckStatus(); apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "推迟结束,监已自动恢复", "MONITOR"); addLog("INFO", "推迟结束,监已自动恢复", "MONITOR");
message.value = "推迟结束,监已自动恢复"; message.value = "推迟结束,监已自动恢复";
}, 60 * 60 * 1000); }, 60 * 60 * 1000);
} }
async function manualCheck() { async function manualCheck() {
if (!isLoggedIn.value) { message.value = "请先开始监(登录)"; return; } if (!isLoggedIn.value) { message.value = "请先开始监(登录)"; return; }
isLoading.value = true; isLoading.value = true;
await apiCheckStatus(); await apiCheckStatus();
message.value = "手动检查完成"; message.value = "手动检查完成";
@ -522,7 +557,7 @@ export function useMonitor() {
checkNetworkStatus(); checkNetworkStatus();
if (config.value.auto_monitor && rememberPassword.value && username.value && password.value) { if (config.value.auto_monitor && rememberPassword.value && username.value && password.value) {
addLog("INFO", "自动登录并开始监...", "SYSTEM"); addLog("INFO", "自动登录并开始监...", "SYSTEM");
await startMonitoring(); await startMonitoring();
} }
}); });

View File

@ -1,5 +1,6 @@
import { ref } from "vue"; import { ref, watch } from "vue";
import { defineStore } from "pinia"; import dayjs from "dayjs";
import { defineStore, acceptHMRUpdate } from "pinia";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { TrayIcon } from "@tauri-apps/api/tray"; import { TrayIcon } from "@tauri-apps/api/tray";
import { Image } from "@tauri-apps/api/image"; import { Image } from "@tauri-apps/api/image";
@ -16,12 +17,13 @@ import {
} from "@tauri-apps/plugin-autostart"; } from "@tauri-apps/plugin-autostart";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event"; 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 = "工单系统监测"; export const APP_NAME = "工单系统监测";
const API_BASE = "https://crm.yunvip123.com/api"; const API_BASE = "https://crm.yunvip123.com/api";
const LOGIN_URL = `${API_BASE}/SystemUser/Login`; const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`; const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
export interface LogEntry { export interface LogEntry {
timestamp: string; timestamp: string;
@ -34,6 +36,7 @@ export interface TicketCounts {
pending: number; pending: number;
stayclose: number; stayclose: number;
confirm: number; confirm: number;
workOrderCount: number;
lastCheck: string; lastCheck: string;
} }
@ -52,7 +55,7 @@ export const useMonitorStore = defineStore("monitor", () => {
const autoStartEnabled = ref(false); const autoStartEnabled = ref(false);
const logs = ref<LogEntry[]>([]); const logs = ref<LogEntry[]>([]);
const ticketCounts = ref<TicketCounts>({ const ticketCounts = ref<TicketCounts>({
pending: 0, stayclose: 0, confirm: 0, lastCheck: "", pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
}); });
const networkStatus = ref({ const networkStatus = ref({
is_connected: false, is_connected: false,
@ -64,6 +67,7 @@ export const useMonitorStore = defineStore("monitor", () => {
check_interval: 60, check_interval: 60,
auto_start: false, auto_start: false,
auto_monitor: false, auto_monitor: false,
silent_start: false,
show_notifications: true, show_notifications: true,
notification_sound: true, notification_sound: true,
network_timeout: 30, network_timeout: 30,
@ -71,6 +75,7 @@ export const useMonitorStore = defineStore("monitor", () => {
handle_pause_minutes: 20, handle_pause_minutes: 20,
}); });
const showCloseDialog = ref(false); const showCloseDialog = ref(false);
const postponeResumeTime = ref<number | null>(null);
// ===== 内部状态 ===== // ===== 内部状态 =====
let sessionId = ""; let sessionId = "";
@ -80,10 +85,11 @@ export const useMonitorStore = defineStore("monitor", () => {
let postponeTimer: ReturnType<typeof setTimeout> | null = null; let postponeTimer: ReturnType<typeof setTimeout> | null = null;
let unlistenNotification: UnlistenFn | null = null; let unlistenNotification: UnlistenFn | null = null;
let unlistenClose: UnlistenFn | null = null; let unlistenClose: UnlistenFn | null = null;
let trayIconInstance: TrayIcon | null = null;
// ===== 工具函数 ===== // ===== 工具函数 =====
function formatTime(date: Date): string { 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) { 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> { async function apiCheckStatus(isRetry = false): Promise<void> {
if (!userGid && loginMode.value === "password") { if (!userGid && loginMode.value === "password") {
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER"); addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
@ -202,16 +239,18 @@ export const useMonitorStore = defineStore("monitor", () => {
const pending = d.PendingCount || 0; const pending = d.PendingCount || 0;
const stayclose = d.StaycloseCount || 0; const stayclose = d.StaycloseCount || 0;
const confirm = d.ConfirmCount || 0; const confirm = d.ConfirmCount || 0;
const workOrderCount = await apiCheckWorkOrders();
ticketCounts.value = { ticketCounts.value = {
pending, stayclose, confirm, pending, stayclose, confirm, workOrderCount,
lastCheck: formatTime(new Date()), lastCheck: formatTime(new Date()),
}; };
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER"); addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER");
if (pending > 0 || stayclose > 0 || confirm > 0) { if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) {
const msgs: string[] = []; const msgs: string[] = [];
if (pending > 0) msgs.push(`待处理工单: ${pending}`); if (pending > 0) msgs.push(`待处理工单: ${pending}`);
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`); if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`); if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`);
await notify("工单提醒", msgs.join("")); await notify("工单提醒", msgs.join(""));
} }
} else { } else {
@ -234,8 +273,8 @@ export const useMonitorStore = defineStore("monitor", () => {
async function tryReLogin(): Promise<void> { async function tryReLogin(): Promise<void> {
if (loginMode.value === "token") { if (loginMode.value === "token") {
addLog("ERROR", "Token 已失效,监已停止,请更换新的 Token 后重新开始", "SESSION"); addLog("ERROR", "Token 已失效,监已停止,请更换新的 Token 后重新开始", "SESSION");
message.value = "Token 已失效,监已停止"; message.value = "Token 已失效,监已停止";
stopMonitoring(); stopMonitoring();
return; return;
} }
@ -392,7 +431,7 @@ export const useMonitorStore = defineStore("monitor", () => {
message.value = "日志导出成功"; message.value = "日志导出成功";
} }
// ===== 监控制 ===== // ===== 监控制 =====
async function startMonitoring() { async function startMonitoring() {
if (loginMode.value === "password") { if (loginMode.value === "password") {
if (!username.value.trim() || !password.value.trim()) { if (!username.value.trim() || !password.value.trim()) {
@ -402,7 +441,7 @@ export const useMonitorStore = defineStore("monitor", () => {
isLoading.value = true; isLoading.value = true;
const ok = await apiLogin(); const ok = await apiLogin();
if (!ok) { if (!ok) {
message.value = "登录失败,无法开始监"; message.value = "登录失败,无法开始监";
isLoading.value = false; isLoading.value = false;
return; return;
} }
@ -419,10 +458,10 @@ export const useMonitorStore = defineStore("monitor", () => {
isMonitoring.value = true; isMonitoring.value = true;
await apiCheckStatus(); await apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "监已启动", "MONITOR"); addLog("INFO", "监已启动", "MONITOR");
message.value = loginMode.value === "password" message.value = loginMode.value === "password"
? `已开始,欢迎 ${userName}` ? `已开始,欢迎 ${userName}`
: "监已开始Token 登录)"; : "监已开始Token 登录)";
isLoading.value = false; isLoading.value = false;
} }
@ -430,12 +469,13 @@ export const useMonitorStore = defineStore("monitor", () => {
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; } if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; }
isPostponed.value = false; isPostponed.value = false;
postponeResumeTime.value = null;
isMonitoring.value = false; isMonitoring.value = false;
isLoggedIn.value = false; isLoggedIn.value = false;
sessionId = ""; sessionId = "";
userGid = ""; userGid = "";
addLog("INFO", "监已停止", "MONITOR"); addLog("INFO", "监已停止", "MONITOR");
message.value = "监已停止"; message.value = "监已停止";
} }
function postponeMonitoring() { function postponeMonitoring() {
@ -443,15 +483,17 @@ export const useMonitorStore = defineStore("monitor", () => {
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
if (postponeTimer) { clearTimeout(postponeTimer); } if (postponeTimer) { clearTimeout(postponeTimer); }
isPostponed.value = true; isPostponed.value = true;
addLog("INFO", "监控已推迟1小时", "MONITOR"); postponeResumeTime.value = Date.now() + 60 * 60 * 1000;
message.value = "监控已推迟1小时将在1小时后自动恢复"; addLog("INFO", "监测已推迟1小时", "MONITOR");
message.value = "监测已推迟1小时将在1小时后自动恢复";
postponeTimer = setTimeout(() => { postponeTimer = setTimeout(() => {
isPostponed.value = false; isPostponed.value = false;
postponeResumeTime.value = null;
postponeTimer = null; postponeTimer = null;
apiCheckStatus(); apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "推迟结束,监已自动恢复", "MONITOR"); addLog("INFO", "推迟结束,监已自动恢复", "MONITOR");
message.value = "推迟结束,监已自动恢复"; message.value = "推迟结束,监已自动恢复";
}, 60 * 60 * 1000); }, 60 * 60 * 1000);
} }
@ -461,20 +503,33 @@ export const useMonitorStore = defineStore("monitor", () => {
if (postponeTimer) { clearTimeout(postponeTimer); } if (postponeTimer) { clearTimeout(postponeTimer); }
isPostponed.value = true; isPostponed.value = true;
const minutes = config.value.handle_pause_minutes; const minutes = config.value.handle_pause_minutes;
addLog("INFO", `点击"去处理",监控已暂停 ${minutes} 分钟`, "MONITOR"); postponeResumeTime.value = Date.now() + minutes * 60 * 1000;
message.value = `监控已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`; addLog("INFO", `点击"去处理",监测已暂停 ${minutes} 分钟`, "MONITOR");
message.value = `监测已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`;
postponeTimer = setTimeout(() => { postponeTimer = setTimeout(() => {
isPostponed.value = false; isPostponed.value = false;
postponeResumeTime.value = null;
postponeTimer = null; postponeTimer = null;
apiCheckStatus(); apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000); monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "暂停结束,监已自动恢复", "MONITOR"); addLog("INFO", "暂停结束,监已自动恢复", "MONITOR");
message.value = "暂停结束,监已自动恢复"; message.value = "暂停结束,监已自动恢复";
}, minutes * 60 * 1000); }, 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() { async function manualCheck() {
if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; } if (!isLoggedIn.value) { message.value = "请先开始监(登录)"; return; }
isLoading.value = true; isLoading.value = true;
await apiCheckStatus(); await apiCheckStatus();
message.value = "手动检查完成"; 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 { try {
const mainWindow = getCurrentWindow(); const hasTickets =
const menu = await Menu.new({ ticketCounts.value.pending > 0 ||
items: [ ticketCounts.value.stayclose > 0 ||
await MenuItem.new({ ticketCounts.value.confirm > 0 ||
id: "show", ticketCounts.value.workOrderCount > 0;
text: "显示面板",
action: async () => { 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.unminimize();
await mainWindow.show(); await mainWindow.show();
await mainWindow.setFocus(); await mainWindow.setFocus();
}, await startMonitoring();
}), }
await MenuItem.new({ },
id: "quit", }),
text: "退出", await MenuItem.new({
action: async () => { await mainWindow.destroy(); }, 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"); async function updateTrayMenu() {
const icon = await Image.fromPath(iconPath); 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, icon,
tooltip: APP_NAME, tooltip: `${APP_NAME} - 未开始`,
menu, menu,
menuOnLeftClick: false, menuOnLeftClick: false,
action: async (event) => { action: async (event) => {
if (event.type === "Click" && event.button === "Left") { if (event.type === "Click" && event.button === "Left") {
await mainWindow.unminimize(); const win = getCurrentWindow();
await mainWindow.show(); await win.unminimize();
await mainWindow.setFocus(); await win.show();
await win.setFocus();
} }
}, },
}); });
@ -597,6 +754,14 @@ export const useMonitorStore = defineStore("monitor", () => {
} }
}); });
addLog("INFO", "应用程序启动完成", "SYSTEM"); addLog("INFO", "应用程序启动完成", "SYSTEM");
if (config.value.silent_start) {
try {
await getCurrentWindow().hide();
addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
} catch { /* ignore */ }
}
checkNetworkStatus(); checkNetworkStatus();
if (config.value.auto_monitor && rememberPassword.value) { if (config.value.auto_monitor && rememberPassword.value) {
@ -604,7 +769,7 @@ export const useMonitorStore = defineStore("monitor", () => {
? !!tokenSessionId.value ? !!tokenSessionId.value
: !!(username.value && password.value); : !!(username.value && password.value);
if (canAutoStart) { if (canAutoStart) {
addLog("INFO", "自动登录并开始监...", "SYSTEM"); addLog("INFO", "自动登录并开始监...", "SYSTEM");
await startMonitoring(); await startMonitoring();
} }
} }
@ -617,18 +782,28 @@ export const useMonitorStore = defineStore("monitor", () => {
unlistenClose?.(); unlistenClose?.();
} }
// 监听状态变化,自动更新托盘图标和菜单
watch([isMonitoring, isPostponed, ticketCounts], () => {
updateTrayIcon();
updateTrayMenu();
}, { deep: true });
return { return {
// 状态 // 状态
username, password, rememberPassword, loginMode, tokenSessionId, username, password, rememberPassword, loginMode, tokenSessionId,
message, isLoading, message, isLoading,
isLoggedIn, isMonitoring, isPostponed, autoStartEnabled, isLoggedIn, isMonitoring, isPostponed, postponeResumeTime, autoStartEnabled,
logs, networkStatus, config, ticketCounts, logs, networkStatus, config, ticketCounts,
showCloseDialog, showCloseDialog,
// 方法 // 方法
initialize, cleanup, initialize, cleanup,
addLog, clearLogs, clearMessage, saveSettings, exportLogs, addLog, clearLogs, clearMessage, saveSettings, exportLogs,
startMonitoring, stopMonitoring, manualCheck, startMonitoring, stopMonitoring, resumeMonitoring, manualCheck,
testNotification, checkNetworkStatus, updateConfig, testNotification, checkNetworkStatus, updateConfig,
minimizeToTray, handleCloseAction, minimizeToTray, handleCloseAction,
}; };
}); });
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useMonitorStore, import.meta.hot));
}