feat(): 添加待审核
|
|
@ -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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 开发服务器端口:1420,HMR 端口:1421
|
- Vite 开发服务器端口:1420,HMR 端口:1421
|
||||||
- 系统托盘使用 `@tauri-apps/api/tray` 和 `@tauri-apps/api/menu` 实现
|
- 系统托盘使用 `@tauri-apps/api/tray` 和 `@tauri-apps/api/menu` 实现
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 系统监控应用程序
|
# 系统监测应用程序
|
||||||
|
|
||||||
这是一个基于 Tauri + Vue 开发的系统监控应用程序,具有以下功能:
|
这是一个基于 Tauri + Vue 开发的系统监测应用程序,具有以下功能:
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ pnpm tauri build
|
||||||
- 右键点击托盘图标选择"显示设置"打开设置界面
|
- 右键点击托盘图标选择"显示设置"打开设置界面
|
||||||
- 输入用户名和密码,点击"保存设置"
|
- 输入用户名和密码,点击"保存设置"
|
||||||
|
|
||||||
### 3. 登录和监控
|
### 3. 登录和监测
|
||||||
- 保存设置后点击"登录"按钮
|
- 保存设置后点击"登录"按钮
|
||||||
- 登录成功后会自动启动定时任务(每1分钟执行一次)
|
- 登录成功后会自动启动定时任务(每1分钟执行一次)
|
||||||
- 系统会根据API返回值显示通知消息
|
- 系统会根据API返回值显示通知消息
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 407 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 407 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 404 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 637 B |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 641 B |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 641 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 452 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 824 B |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 440 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 488 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 522 B |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 929 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 340 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 429 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 429 B |
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 543 B |
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
10
src/App.vue
|
|
@ -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>
|
||||||
|
|
|
||||||
71
src/App2.vue
|
|
@ -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>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'"
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
|
||||||