refactor: ♻️ 重构文件结构
This commit is contained in:
parent
1b6e12e402
commit
0b2c1ca83f
|
|
@ -0,0 +1,124 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 **Tauri 2.x + Vue 3** 的工单监测桌面应用,用于监测 CRM 系统(crm.yunvip123.com)的待处理工单并发送系统通知。
|
||||
|
||||
## 常用命令
|
||||
|
||||
项目使用 **pnpm** 作为包管理器。
|
||||
|
||||
```bash
|
||||
# 启动开发模式(前端 + Tauri)
|
||||
pnpm run tauri-dev
|
||||
|
||||
# 仅启动前端开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 前端类型检查 + 构建
|
||||
pnpm run build
|
||||
|
||||
# 构建 Tauri 生产版本(含签名/安装包)
|
||||
pnpm run tauri-build
|
||||
|
||||
# 构建并发布
|
||||
pnpm run deploy
|
||||
|
||||
# 版本号管理(默认 patch)
|
||||
pnpm run version # patch
|
||||
pnpm run version:minor
|
||||
pnpm run version:major
|
||||
|
||||
# 代码检查
|
||||
pnpm run lint
|
||||
pnpm run lint:fix
|
||||
|
||||
# 规范提交(commitizen + cz-git)
|
||||
pnpm run commit
|
||||
|
||||
# Rust 后端检查
|
||||
cd src-tauri && cargo check
|
||||
cd src-tauri && cargo clippy
|
||||
```
|
||||
|
||||
## 架构概述
|
||||
|
||||
**前端驱动架构**:所有业务逻辑(API 调用、定时任务、通知)都在 Vue 前端实现,Rust 后端仅负责系统级功能(窗口管理、插件注册)。
|
||||
|
||||
### 前端分层(src/)
|
||||
|
||||
**Stores(Pinia)**:
|
||||
|
||||
- `stores/monitor.ts` — 核心业务:登录、轮询检查工单、推迟/暂停逻辑、托盘联动
|
||||
- `stores/app.ts` — 全局配置(轮询间隔、自启动、静默启动等),配置持久化到 `localStorage`(key: `crm_config`)
|
||||
- `stores/log.ts` — 运行日志管理
|
||||
- `stores/network.ts` — 网络状态检测
|
||||
|
||||
**Composables**:
|
||||
|
||||
- `composables/useNotification.ts` — 系统通知封装(`tauri-plugin-notification`)
|
||||
- `composables/useTray.ts` — 系统托盘创建、图标/菜单动态更新
|
||||
- `composables/useUpdater.ts` — 应用自动更新(`tauri-plugin-updater`)
|
||||
- `composables/useMonitor.ts` — 监测相关辅助逻辑
|
||||
|
||||
**Constants**:
|
||||
|
||||
- `constants/api.ts` — 所有 API URL 集中定义(`LOGIN_URL`、`CHECK_URL`、`WORK_ORDER_URL`)
|
||||
- `constants/app.ts` — 应用级常量
|
||||
|
||||
**Utils**:
|
||||
|
||||
- `utils/storage.ts` — localStorage 读写封装
|
||||
- `utils/common.ts` — 通用工具函数(`formatTime` 等)
|
||||
|
||||
### Rust 后端(src-tauri/src/)
|
||||
|
||||
- `lib.rs` — 应用初始化、插件注册、窗口关闭事件处理(关闭→隐藏到托盘,发出 `close-requested` 事件)
|
||||
|
||||
### 关键数据流
|
||||
|
||||
1. `App.vue` 初始化:加载配置 → 加载凭据 → 初始化托盘 → 监听通知动作事件 + 关闭事件
|
||||
2. `monitorStore.startMonitoring()` 执行登录(密码或 Token 模式),成功后启动 `setInterval` 轮询
|
||||
3. 工单有待处理项时调用 `useNotification.notify()`,通知动作(推迟/去处理)通过 Tauri 事件回传
|
||||
4. 托盘图标/菜单通过 `watch([isMonitoring, isPostponed, ticketCounts])` 自动同步
|
||||
|
||||
### API 集成
|
||||
|
||||
- 登录:`POST https://crm.yunvip123.com/api/SystemUser/Login`
|
||||
- 工单查询:`GET https://crm.yunvip123.com/api/DemandManage/QueryIndexCount`
|
||||
- 认证方式:登录响应 `set-cookie` 中提取 `ASP.NET_SessionId`,后续请求携带
|
||||
- HTTP 白名单配置在 `src-tauri/capabilities/default.json`
|
||||
|
||||
### 通知触发条件
|
||||
|
||||
当 `PendingCount`(待处理)、`StaycloseCount`(待关闭)、`ConfirmCount`(待确认)任意 > 0 时触发。
|
||||
|
||||
## 代码规范
|
||||
|
||||
- Vue 组件使用 `<script setup lang="ts">` + `<style lang="scss" scoped>`
|
||||
- 方法定义优先使用 `function` 关键字(而非箭头函数赋值)
|
||||
- `props` 尽量提供 `default` 默认值;在 `script` 中使用 `props` 时通过 `toRefs(props)` 解构
|
||||
- 新增成员变量和方法须加注释
|
||||
|
||||
## 关键配置文件
|
||||
|
||||
- `src-tauri/capabilities/default.json` — Tauri 权限配置(HTTP 白名单、插件权限)
|
||||
- `src-tauri/tauri.conf.json` — 应用配置(窗口、图标、bundle、updater 端点)
|
||||
- `.env` / `.env.development` / `.env.production` — 环境变量
|
||||
- `vite.config.ts` — Vite 配置,开发服务器端口 1420,HMR 端口 1421
|
||||
|
||||
## 依赖的 Tauri 插件
|
||||
|
||||
- `tauri-plugin-http` — HTTP 请求
|
||||
- `tauri-plugin-notification` — 系统通知
|
||||
- `tauri-plugin-autostart` — 开机自启动
|
||||
- `tauri-plugin-opener` — 打开外部链接
|
||||
- `tauri-plugin-updater` — 应用自动更新
|
||||
- `tauri-plugin-store` — 持久化存储
|
||||
- `tauri-plugin-process` — 进程控制
|
||||
|
||||
## Windows 编译要求
|
||||
|
||||
需要安装 Visual Studio Build Tools(包含 C++ 工具链)才能编译 Rust 后端。
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
paths:
|
||||
- "**/*.{js,mjs,cjs,jsx,ts,tsx}"
|
||||
---
|
||||
|
||||
# JavaScript/TypeScript
|
||||
|
||||
- 尽量使用`function`定义方法
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
paths:
|
||||
- "**/*.vue"
|
||||
---
|
||||
|
||||
# Uniapp
|
||||
|
||||
- 组件文件命名使用`easycom`规范,示例:`components/组件名称/组件名称.vue`。
|
||||
- 如果在`src/components`中符合`easycom`命名规范的,且在`pages.json`中配置`easycom.autoscan`不为`false`的组件,无需在使用是引入。
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
paths:
|
||||
- "**/*.vue"
|
||||
---
|
||||
|
||||
# Vue 3
|
||||
|
||||
- 新建文件默认使用<script setup>。
|
||||
- 样式默认使用<style lang="scss" scoped>。
|
||||
- 尽量使用`function`定义方法
|
||||
- `props`尽量使用`default`默认值初始化,包括`Object`类型中的属性值
|
||||
- 如果在`script`中用到了`props`,则使用`toRefs(props)`解构
|
||||
|
|
@ -1,24 +1,39 @@
|
|||
{
|
||||
"permissions": {
|
||||
"defaultMode": "acceptEdits",
|
||||
"allow": [
|
||||
"Read(./)",
|
||||
"Edit(./)",
|
||||
"Write(./)",
|
||||
|
||||
"Bash(pnpm build *)",
|
||||
"Bash(pnpm install:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(pnpm run:*)",
|
||||
"Bash(pnpm test:*)",
|
||||
"Bash(pnpm view:*)",
|
||||
"Bash(pnpm list:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(where:*)",
|
||||
"Bash(echo:*)",
|
||||
|
||||
"WebFetch(domain:tauri.app)",
|
||||
"Bash(npm install @tauri-apps/plugin-http @tauri-apps/plugin-autostart)",
|
||||
"Bash(npx tauri build)",
|
||||
"Bash(cargo update)",
|
||||
"Bash(cargo build)",
|
||||
"Bash(where link.exe)",
|
||||
"Bash(PATH=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\" cargo build)",
|
||||
"Bash(export PATH=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\")",
|
||||
"Bash(CC=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe\" CXX=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64/cl.exe\" INCLUDE=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/include;C:/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/ucrt;C:/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/um;C:/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/shared\" LIB=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/lib/x64;C:/Program Files \\(x86\\)/Windows Kits/10/Lib/10.0.26100.0/um/x64;C:/Program Files \\(x86\\)/Windows Kits/10/Lib/10.0.26100.0/ucrt/x64\" PATH=\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\" cargo build)",
|
||||
"Bash(echo $PATH)",
|
||||
"Bash(echo \"PATH=$PATH\")",
|
||||
"Bash(cmd.exe /C \"\"\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/2022/BuildTools/VC/Auxiliary/Build/vcvars64.bat\"\" && cargo build\")",
|
||||
"Bash(cmd.exe //C \"\"\"C:\\\\Program Files \\(x86\\)\\\\Microsoft Visual Studio\\\\2022\\\\BuildTools\\\\VC\\\\Auxiliary\\\\Build\\\\vcvars64.bat\"\" >nul 2>&1 & cargo build\")",
|
||||
"Bash(cmd.exe //C \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\build.bat\")",
|
||||
"Bash(npx vue-tsc --noEmit)",
|
||||
"Bash(tasklist)",
|
||||
"Bash(taskkill //F //IM tauri-app.exe)",
|
||||
"Bash(taskkill //F //IM vite.exe)",
|
||||
"Bash(cmd.exe //C \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\run.bat\")",
|
||||
"Bash(cmd /c \"\"\"C:\\\\Program Files \\(x86\\)\\\\Microsoft Visual Studio\\\\2022\\\\BuildTools\\\\VC\\\\Auxiliary\\\\Build\\\\vcvars64.bat\"\" >nul 2>&1 && set\")",
|
||||
"Bash(cmd /c \"\"\"C:\\\\Program Files \\(x86\\)\\\\Microsoft Visual Studio\\\\2022\\\\BuildTools\\\\VC\\\\Auxiliary\\\\Build\\\\vcvars64.bat\"\" && set\")",
|
||||
|
|
@ -31,15 +46,8 @@
|
|||
"Bash(powershell.exe -NoProfile -Command \"where.exe cl.exe\")",
|
||||
"Bash(powershell.exe -NoProfile -Command \"Get-ItemProperty ''HKCU:\\\\Environment'' | Select-Object -ExpandProperty PATH | Select-String ''HostX64''\")",
|
||||
"Bash(cargo check)",
|
||||
"Bash(pnpm run tauri dev)",
|
||||
"Bash(netstat -ano)",
|
||||
"Bash(findstr :1420)",
|
||||
"Bash(pnpm run tauri build)",
|
||||
"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 add pinia)",
|
||||
"Bash(pnpm add dayjs)",
|
||||
"Bash(pnpm tauri icon public/gong.svg)"
|
||||
]
|
||||
"Bash(start \"\" \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\\\\tauri-app_0.1.0_x64-setup.exe\")"
|
||||
],
|
||||
"ask": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)", "Bash(git push *)", "Bash(rm -rf *)"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ps1]
|
||||
charset = utf-8-bom
|
||||
end_of_line = crlf
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 开发环境
|
||||
VITE_APP_NAME=工单系统监测【开发版】
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 生产环境
|
||||
VITE_APP_NAME=工单系统监测
|
||||
|
|
@ -0,0 +1 @@
|
|||
pnpm exec commitlint --edit $1
|
||||
|
|
@ -0,0 +1 @@
|
|||
pnpm lint-staged
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/cache
|
||||
/project.local.yml
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# the name by which the project can be referenced within Serena
|
||||
project_name: "tauri-app"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- vue
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
#
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
74
CLAUDE.md
74
CLAUDE.md
|
|
@ -1,74 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 **Tauri 2.x + Vue 3** 的工单监测桌面应用,用于监测 CRM 系统(crm.yunvip123.com)的待处理工单并发送系统通知。
|
||||
|
||||
## 常用命令
|
||||
|
||||
项目使用 **pnpm** 作为包管理器(存在 package-lock.json)。
|
||||
|
||||
```bash
|
||||
# 启动开发模式(前端 + Tauri)
|
||||
pnpm run tauri dev
|
||||
|
||||
# 仅启动前端开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 构建生产版本
|
||||
pnpm run tauri build
|
||||
|
||||
# 前端类型检查 + 构建
|
||||
pnpm run build
|
||||
|
||||
# Rust 后端检查
|
||||
cd src-tauri && cargo check
|
||||
cd src-tauri && cargo clippy
|
||||
```
|
||||
|
||||
## 架构概述
|
||||
|
||||
**前端驱动架构**:所有业务逻辑(API 调用、定时任务、通知)都在 Vue 前端实现,Rust 后端仅负责系统级功能。
|
||||
|
||||
### 前端(src/)
|
||||
- `src/App.vue` - 主界面,包含全部业务逻辑:用户认证、监测调度、通知、网络检测、配置管理、系统托盘
|
||||
- 使用 Vue 3 Composition API(`<script setup>`)
|
||||
- Vite 开发服务器端口:1420,HMR 端口:1421
|
||||
- 系统托盘使用 `@tauri-apps/api/tray` 和 `@tauri-apps/api/menu` 实现
|
||||
|
||||
### Rust 后端(src-tauri/src/)
|
||||
- `lib.rs` - 应用初始化、插件注册、窗口关闭事件处理(关闭→隐藏到托盘)
|
||||
|
||||
### Tauri 命令
|
||||
无自定义命令(所有功能通过前端 API 实现)
|
||||
|
||||
### API 集成
|
||||
- 登录:`POST https://crm.yunvip123.com/api/SystemUser/Login`
|
||||
- 工单查询:`GET https://crm.yunvip123.com/api/DemandManage/QueryIndexCount`
|
||||
- 认证方式:ASP.NET_SessionId Cookie
|
||||
- HTTP 白名单配置在 `src-tauri/capabilities/default.json`
|
||||
|
||||
### 通知触发条件
|
||||
当以下任意计数 > 0 时触发系统通知:
|
||||
- `PendingCount`(待处理)
|
||||
- `StaycloseCount`(待关闭)
|
||||
- `ConfirmCount`(待确认)
|
||||
|
||||
## 关键配置
|
||||
|
||||
- `src-tauri/capabilities/default.json` - Tauri 权限配置(HTTP 白名单、插件权限)
|
||||
- `src-tauri/tauri.conf.json` - 应用配置(窗口、图标、bundle)
|
||||
- 用户配置文件路径:`%APPDATA%/work-order-monitor/config.json`(Windows)
|
||||
|
||||
## 依赖的 Tauri 插件
|
||||
|
||||
- `tauri-plugin-http` - HTTP 请求
|
||||
- `tauri-plugin-notification` - 系统通知
|
||||
- `tauri-plugin-autostart` - 开机自启动
|
||||
- `tauri-plugin-opener` - 打开外部链接
|
||||
|
||||
## Windows 编译要求
|
||||
|
||||
需要安装 Visual Studio Build Tools(包含 C++ 工具链)才能编译 Rust 后端。
|
||||
144
README.md
144
README.md
|
|
@ -1,7 +1,137 @@
|
|||
# Tauri + Vue + TypeScript
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
# 工单系统监测
|
||||
|
||||
基于 **Tauri 2.x + Vue 3 + TypeScript** 的工单监测桌面应用,用于监测 CRM 系统的待处理工单并发送系统通知。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 18
|
||||
- Rust(stable)
|
||||
- Visual Studio Build Tools(Windows,含 C++ 工具链)
|
||||
- pnpm
|
||||
|
||||
**推荐 IDE:** [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发模式(前端 + Tauri)
|
||||
pnpm run tauri-dev
|
||||
|
||||
# 仅启动前端开发服务器
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
签名密钥已包含在项目中(`.tauri/` 目录),构建脚本会自动加载,直接运行:
|
||||
|
||||
```bash
|
||||
pnpm run tauri-build
|
||||
```
|
||||
|
||||
构建产物位于 `src-tauri/target/release/bundle/nsis/`。
|
||||
|
||||
## 版本号管理
|
||||
|
||||
使用自定义脚本统一更新 `package.json`、`tauri.conf.json`、`Cargo.toml` 三个文件的版本号:
|
||||
|
||||
```bash
|
||||
pnpm run version:patch # 补丁版本 x.y.Z → x.y.Z+1
|
||||
pnpm run version:minor # 次版本 x.Y.z → x.Y+1.0
|
||||
pnpm run version:major # 主版本 X.y.z → X+1.0.0
|
||||
pnpm run version -- 1.2.3 # 指定版本号
|
||||
```
|
||||
|
||||
## 发布
|
||||
|
||||
构建完成后,运行发布脚本将安装包和更新元数据上传到远程服务器:
|
||||
|
||||
```bash
|
||||
pnpm run publish -- --notes "更新说明"
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
|
||||
1. 从构建产物中读取 `.nsis.zip` 和 `.nsis.zip.sig`
|
||||
2. 生成 `update.json`(供应用内自动更新使用)
|
||||
3. 通过 SCP 上传到服务器 `192.168.2.127:/var/www/html/work-order-monitor/`
|
||||
|
||||
更新地址:`http://web.nps.yunvip123.cn/work-order-monitor/update.json`
|
||||
|
||||
## 完整发版流程
|
||||
|
||||
```bash
|
||||
# 1. 更新版本号
|
||||
pnpm run version:minor
|
||||
|
||||
# 2. 构建
|
||||
pnpm run tauri-build
|
||||
|
||||
# 3. 发布到服务器
|
||||
pnpm run publish -- --notes "v0.3.0 新功能说明"
|
||||
```
|
||||
|
||||
或一键构建 + 发布:
|
||||
|
||||
```bash
|
||||
pnpm run release
|
||||
```
|
||||
|
||||
## 签名密钥
|
||||
|
||||
项目已内置 updater 签名密钥(`.tauri/signing-key` 和 `.tauri/signing-key.pub`),无密码保护,任何人克隆项目后即可直接构建。
|
||||
|
||||
如需重新生成密钥:
|
||||
|
||||
```bash
|
||||
pnpm tauri signer generate -w .tauri/signing-key -p "" --ci -f
|
||||
```
|
||||
|
||||
生成后需将 `.tauri/signing-key.pub` 的内容更新到 `src-tauri/tauri.conf.json` 的 `plugins.updater.pubkey` 字段。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/ # 前端(Vue 3)
|
||||
├── App.vue # 主界面
|
||||
├── stores/monitor.ts # Pinia 状态管理(核心业务逻辑)
|
||||
├── components/
|
||||
│ ├── MonitorControl.vue # 监测面板(工单数据卡片)
|
||||
│ ├── LoginForm.vue # 登录表单
|
||||
│ ├── SettingsPanel.vue # 配置面板
|
||||
│ ├── StatusHeader.vue # 顶部状态栏
|
||||
│ ├── LogViewer.vue # 日志查看器
|
||||
│ ├── CloseDialog.vue # 关闭确认弹窗
|
||||
│ └── UpdateDialog.vue # 更新确认弹窗
|
||||
|
||||
src-tauri/ # Rust 后端
|
||||
├── src/lib.rs # 应用初始化、插件注册
|
||||
├── src/notification.rs # Windows Toast 通知
|
||||
├── tauri.conf.json # Tauri 配置
|
||||
├── capabilities/default.json # 权限配置
|
||||
|
||||
scripts/
|
||||
├── build.mjs # 构建(自动加载签名密钥)
|
||||
├── bump-version.mjs # 版本号管理
|
||||
└── publish.mjs # 构建发布
|
||||
```
|
||||
|
||||
## 监测的 API
|
||||
|
||||
| 接口 | 说明 | 触发通知条件 |
|
||||
| ----------------------------------- | ---------------- | ------------------------------------------------ |
|
||||
| `SystemUser/Login` | 登录获取 Session | - |
|
||||
| `DemandManage/QueryIndexCount` | 工单统计 | PendingCount / StaycloseCount / ConfirmCount > 0 |
|
||||
| `DemandManage/GetWorkOrderListPage` | 待审核工单列表 | DataCount > 0 |
|
||||
|
||||
## Tauri 插件
|
||||
|
||||
- `tauri-plugin-http` — HTTP 请求
|
||||
- `tauri-plugin-notification` — 系统通知
|
||||
- `tauri-plugin-autostart` — 开机自启动
|
||||
- `tauri-plugin-opener` — 打开外部链接
|
||||
- `tauri-plugin-updater` — 应用内自动更新
|
||||
- `tauri-plugin-process` — 进程管理(重启)
|
||||
|
|
|
|||
127
README_APP.md
127
README_APP.md
|
|
@ -1,127 +0,0 @@
|
|||
# 工单系统监测
|
||||
|
||||
基于 **Tauri 2.x + Vue 3** 的工单监测桌面应用,用于监测 CRM 系统的待处理工单并发送系统通知。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 18
|
||||
- Rust (stable)
|
||||
- Visual Studio Build Tools(Windows,含 C++ 工具链)
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发模式(前端 + Tauri)
|
||||
pnpm tauri dev
|
||||
|
||||
# 仅启动前端
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
签名密钥已包含在项目中(`.tauri/` 目录),构建脚本会自动加载,直接运行:
|
||||
|
||||
```bash
|
||||
pnpm run tauri-build
|
||||
```
|
||||
|
||||
构建产物位于 `src-tauri/target/release/bundle/nsis/`。
|
||||
|
||||
## 版本号管理
|
||||
|
||||
使用自定义脚本统一更新 `package.json`、`tauri.conf.json`、`Cargo.toml` 三个文件的版本号:
|
||||
|
||||
```bash
|
||||
node scripts/bump-version.mjs # 默认 patch: 0.2.0 → 0.2.1
|
||||
node scripts/bump-version.mjs minor # minor: 0.2.0 → 0.3.0
|
||||
node scripts/bump-version.mjs major # major: 0.2.0 → 1.0.0
|
||||
node scripts/bump-version.mjs 1.2.3 # 指定版本
|
||||
```
|
||||
|
||||
## 发布
|
||||
|
||||
构建完成后,运行发布脚本将安装包和更新元数据上传到远程服务器:
|
||||
|
||||
```bash
|
||||
node scripts/publish.mjs --notes "更新说明"
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
1. 从构建产物中读取 `.nsis.zip` 和 `.nsis.zip.sig`
|
||||
2. 生成 `update.json`(供应用内自动更新使用)
|
||||
3. 通过 SCP 上传到服务器 `192.168.2.127:/var/www/html/work-order-monitor/`
|
||||
|
||||
更新地址:`http://web.nps.yunvip123.cn/work-order-monitor/update.json`
|
||||
|
||||
## 完整发版流程
|
||||
|
||||
```bash
|
||||
# 1. 更新版本号
|
||||
node scripts/bump-version.mjs minor
|
||||
|
||||
# 2. 构建
|
||||
pnpm run tauri-build
|
||||
|
||||
# 3. 发布到服务器
|
||||
node scripts/publish.mjs --notes "v0.3.0 新功能说明"
|
||||
```
|
||||
|
||||
## 签名密钥
|
||||
|
||||
项目已内置 updater 签名密钥(`.tauri/signing-key` 和 `.tauri/signing-key.pub`),无密码保护,任何人克隆项目后即可直接构建。
|
||||
|
||||
如需重新生成密钥:
|
||||
|
||||
```bash
|
||||
pnpm tauri signer generate -w .tauri/signing-key -p "" --ci -f
|
||||
```
|
||||
|
||||
生成后需将 `.tauri/signing-key.pub` 的内容更新到 `src-tauri/tauri.conf.json` 的 `plugins.updater.pubkey` 字段。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/ # 前端(Vue 3)
|
||||
├── App.vue # 主界面
|
||||
├── stores/monitor.ts # Pinia 状态管理(核心业务逻辑)
|
||||
├── components/
|
||||
│ ├── MonitorControl.vue # 监测面板(工单数据卡片)
|
||||
│ ├── LoginForm.vue # 登录表单
|
||||
│ ├── SettingsPanel.vue # 配置面板
|
||||
│ ├── StatusHeader.vue # 顶部状态栏
|
||||
│ ├── LogViewer.vue # 日志查看器
|
||||
│ ├── CloseDialog.vue # 关闭确认弹窗
|
||||
│ └── UpdateDialog.vue # 更新确认弹窗
|
||||
|
||||
src-tauri/ # Rust 后端
|
||||
├── src/lib.rs # 应用初始化、插件注册
|
||||
├── src/notification.rs # Windows Toast 通知
|
||||
├── tauri.conf.json # Tauri 配置
|
||||
├── capabilities/default.json # 权限配置
|
||||
|
||||
scripts/
|
||||
├── build.mjs # 构建(自动加载签名密钥)
|
||||
├── bump-version.mjs # 版本号管理
|
||||
└── publish.mjs # 构建发布
|
||||
```
|
||||
|
||||
## 监测的 API
|
||||
|
||||
| 接口 | 说明 | 触发通知条件 |
|
||||
| --- | --- | --- |
|
||||
| `SystemUser/Login` | 登录获取 Session | - |
|
||||
| `DemandManage/QueryIndexCount` | 工单统计 | PendingCount / StaycloseCount / ConfirmCount > 0 |
|
||||
| `DemandManage/GetWorkOrderListPage` | 待审核工单列表 | DataCount > 0 |
|
||||
|
||||
## Tauri 插件
|
||||
|
||||
- `tauri-plugin-http` — HTTP 请求
|
||||
- `tauri-plugin-notification` — 系统通知
|
||||
- `tauri-plugin-autostart` — 开机自启动
|
||||
- `tauri-plugin-opener` — 打开外部链接
|
||||
- `tauri-plugin-updater` — 应用内自动更新
|
||||
- `tauri-plugin-process` — 进程管理(重启)
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* feat:新增功能
|
||||
* fix:bug 修复
|
||||
* docs:文档更新
|
||||
* style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
|
||||
* refactor:重构代码(既没有新增功能,也没有修复 bug)
|
||||
* perf:性能, 体验优化
|
||||
* test:新增测试用例或是更新现有测试
|
||||
* build:主要目的是修改项目构建系统(例如 vite,rollup 的配置等)的提交
|
||||
* ci:主要目的是修改项目持续集成流程的提交
|
||||
* chore:不属于以上类型的其他类型,比如构建流程, 依赖管理
|
||||
* revert:回滚某个更早之前的提交
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
// 限制提交类型
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert",
|
||||
],
|
||||
],
|
||||
// scope 可选,如果提供必须是以下之一
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"monitor", // 监测核心逻辑
|
||||
"auth", // 认证/登录
|
||||
"notify", // 通知
|
||||
"tray", // 系统托盘
|
||||
"config", // 配置管理
|
||||
"store", // 状态管理
|
||||
"ui", // 界面/组件
|
||||
"tauri", // Rust/Tauri 后端
|
||||
"network", // 网络检测
|
||||
"update", // 自动更新
|
||||
"deps", // 依赖更新
|
||||
"release", // 版本发布
|
||||
],
|
||||
],
|
||||
// subject 不能为空
|
||||
"subject-empty": [2, "never"],
|
||||
// subject 不能以句号结尾
|
||||
"subject-full-stop": [2, "never", "."],
|
||||
// subject 不限制大小写
|
||||
"subject-case": [0],
|
||||
// type 必须小写
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
// scope 必须小写
|
||||
"scope-case": [2, "always", "lower-case"],
|
||||
// subject 最小长度
|
||||
"subject-min-length": [2, "always", 4],
|
||||
// subject 最大长度
|
||||
"subject-max-length": [2, "always", 100],
|
||||
// header 最大长度(type + scope + subject)
|
||||
"header-max-length": [2, "always", 120],
|
||||
// body 每行最大长度
|
||||
"body-max-line-length": [2, "always", 200],
|
||||
// footer 每行最大长度
|
||||
"footer-max-line-length": [2, "always", 200],
|
||||
},
|
||||
prompt: {
|
||||
useEmoji: true,
|
||||
emojiAlign: "center",
|
||||
allowCustomIssuePrefix: false,
|
||||
allowEmptyIssuePrefix: false,
|
||||
// 定义 scope 选项
|
||||
scopes: [
|
||||
{ value: "monitor", name: "monitor: 监测核心逻辑" },
|
||||
{ value: "auth", name: "auth: 认证/登录" },
|
||||
{ value: "notify", name: "notify: 通知" },
|
||||
{ value: "tray", name: "tray: 系统托盘" },
|
||||
{ value: "config", name: "config: 配置管理" },
|
||||
{ value: "store", name: "store: 状态管理" },
|
||||
{ value: "ui", name: "ui: 界面/组件" },
|
||||
{ value: "tauri", name: "tauri: Rust/Tauri 后端" },
|
||||
{ value: "network", name: "network: 网络检测" },
|
||||
{ value: "update", name: "update: 自动更新" },
|
||||
{ value: "deps", name: "deps: 依赖更新" },
|
||||
{ value: "release", name: "release: 版本发布" },
|
||||
],
|
||||
allowCustomScopes: false,
|
||||
allowEmptyScopes: true,
|
||||
messages: {
|
||||
type: '选择你要提交的类型:',
|
||||
scope: '选择一个提交范围(可选):',
|
||||
customScope: '请输入自定义范围:',
|
||||
subject: '请简短描述这次改动:\n',
|
||||
body: '请详细描述这次改动(可选,用 "|" 换行):\n',
|
||||
breaking: '是否有破坏性变更(可选,用 "|" 换行):\n',
|
||||
footerPrefixesSelect: '选择关联的 ISSUE 类型(可选):',
|
||||
customFooterPrefix: '输入 ISSUE 前缀:',
|
||||
footer: '列出关联的 ISSUE,如: #31, #34:\n',
|
||||
generatingByAI: '正在通过 AI 生成提交信息...',
|
||||
generatedSelectByAI: '选择一个 AI 生成的 subject:',
|
||||
confirmCommit: '是否确认提交以上信息?',
|
||||
},
|
||||
minSubjectLength: 4,
|
||||
defaultSubject: '',
|
||||
types: [
|
||||
{ value: 'feat', name: 'feat: 新增功能', emoji: ':sparkles:' },
|
||||
{ value: 'fix', name: 'fix: 修复缺陷', emoji: ':bug:' },
|
||||
{ value: 'docs', name: 'docs: 文档变更', emoji: ':memo:' },
|
||||
{ value: 'style', name: 'style: 代码格式(不影响逻辑)', emoji: ':lipstick:' },
|
||||
{ value: 'refactor', name: 'refactor: 重构(既非新功能,也不是修 bug)', emoji: ':recycle:' },
|
||||
{ value: 'perf', name: 'perf: 性能优化', emoji: ':zap:' },
|
||||
{ value: 'test', name: 'test: 添加或修改测试', emoji: ':white_check_mark:' },
|
||||
{ value: 'build', name: 'build: 构建系统或外部依赖变更', emoji: ':package:' },
|
||||
{ value: 'ci', name: 'ci: CI 配置文件和脚本变更', emoji: ':ferris_wheel:' },
|
||||
{ value: 'chore', name: 'chore: 其它不修改 src 或测试的改动', emoji: ':hammer:' },
|
||||
{ value: 'revert', name: 'revert: 回退之前的 commit', emoji: ':rewind:' },
|
||||
],
|
||||
issuePrefixs: [
|
||||
{ value: 'link', name: 'link: 关联 ISSUE' },
|
||||
{ value: 'close', name: 'close: 关闭 ISSUE' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import js from "@eslint/js";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import importPlugin, {
|
||||
createNodeResolver,
|
||||
flatConfigs,
|
||||
} from "eslint-plugin-import-x";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
import vuePlugin from "eslint-plugin-vue";
|
||||
import globals from "globals";
|
||||
import ts from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
// 全局忽略
|
||||
{
|
||||
ignores: [
|
||||
"**/node_modules/*",
|
||||
"**/dist/*",
|
||||
"build/",
|
||||
"public/",
|
||||
"docs/",
|
||||
"bin/",
|
||||
"pnpm-lock.yaml",
|
||||
".vscode/",
|
||||
".idea/",
|
||||
".husky/",
|
||||
"Dockerfile",
|
||||
".hbuilderx/",
|
||||
".vite/",
|
||||
"uni_modules/",
|
||||
"libs/",
|
||||
"static/",
|
||||
"patches/",
|
||||
"wxcomponents/",
|
||||
"lib/",
|
||||
"scripts/",
|
||||
"src-tauri/",
|
||||
"**/*.bak.*",
|
||||
],
|
||||
},
|
||||
|
||||
// 应用推荐规则
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
|
||||
// Vue 推荐规则
|
||||
...vuePlugin.configs["flat/recommended"],
|
||||
|
||||
// prettier 必须在所有规则之后,关闭与 prettier 冲突的格式规则
|
||||
prettier,
|
||||
|
||||
// 基础配置
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,vue}"],
|
||||
|
||||
plugins: { prettier: prettierPlugin, "import-x": importPlugin },
|
||||
settings: {
|
||||
"import-x/resolver-next": [
|
||||
createNodeResolver(),
|
||||
(
|
||||
await import("eslint-import-resolver-typescript")
|
||||
).createTypeScriptImportResolver({
|
||||
project: ["tsconfig.json"],
|
||||
}),
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.browser,
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...flatConfigs.recommended.rules,
|
||||
|
||||
// Prettier 规则
|
||||
"prettier/prettier": "warn",
|
||||
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
|
||||
// Import 规则
|
||||
"import-x/no-self-import": "error",
|
||||
"import-x/no-cycle": "error",
|
||||
"import-x/no-useless-path-segments": "error",
|
||||
"import-x/no-extraneous-dependencies": "error",
|
||||
"import-x/no-mutable-exports": "error",
|
||||
"import-x/no-named-as-default": "error",
|
||||
"import-x/no-named-as-default-member": "error",
|
||||
"import-x/no-duplicates": "warn",
|
||||
"import-x/no-deprecated": "warn",
|
||||
"import-x/newline-after-import": "warn",
|
||||
"import-x/first": "warn",
|
||||
"import-x/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// TypeScript 宽松规则(对已有代码降级)
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-empty-object-type": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ varsIgnorePattern: "^_", argsIgnorePattern: "^_" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Vue 文件专属配置:在 script 块中使用 TypeScript 解析器
|
||||
{
|
||||
files: ["**/*.vue"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
extraFileExtensions: [".vue"],
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 测试文件覆盖配置
|
||||
{
|
||||
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mocha,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "work-order-monitor",
|
||||
"private": true,
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
@ -9,7 +9,17 @@
|
|||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri-dev": "tauri dev",
|
||||
"tauri-build": "node scripts/build.mjs"
|
||||
"tauri-build": "node scripts/build.mjs",
|
||||
"version": "node scripts/bump-version.mjs",
|
||||
"version:patch": "node scripts/bump-version.mjs patch",
|
||||
"version:minor": "node scripts/bump-version.mjs minor",
|
||||
"version:major": "node scripts/bump-version.mjs major",
|
||||
"publish": "node scripts/publish.mjs",
|
||||
"deploy": "npm run tauri-build && npm run publish",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"commit": "git-cz",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
|
|
@ -19,17 +29,43 @@
|
|||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-store": "^2.4.2",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs": "^1.11.20",
|
||||
"lodash-es": "^4.17.23",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.29"
|
||||
"vue": "^3.5.31"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,vue}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{js,ts,vue,css,less,json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-git": "^1.12.0",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"vite": "^7.3.1",
|
||||
"vue-tsc": "^3.2.5"
|
||||
"vue-tsc": "^3.2.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3106
pnpm-lock.yaml
3106
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,36 @@
|
|||
// https://prettier.io/docs/en/configuration.html
|
||||
export default {
|
||||
tabWidth: 4,
|
||||
endOfLine: "lf",
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"*.js",
|
||||
"*.mjs",
|
||||
"*.jsx",
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.vue",
|
||||
"*.json",
|
||||
"*.yml",
|
||||
"*.yaml",
|
||||
],
|
||||
options: {
|
||||
tabWidth: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.html", "*.vue", "*.json"],
|
||||
options: {
|
||||
printWidth: 120,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.ps1"],
|
||||
options: {
|
||||
endOfLine: "crlf",
|
||||
tabWidth: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -6167,7 +6167,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "work-order-monitor"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "work-order-monitor"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -1,48 +1,43 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"notification:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://crm.yunvip123.com/**" },
|
||||
{ "url": "https://www.baidu.com/**" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:allow-fetch",
|
||||
"allow": [
|
||||
{ "url": "https://crm.yunvip123.com/**" },
|
||||
{ "url": "https://www.baidu.com/**" }
|
||||
]
|
||||
},
|
||||
"http:allow-fetch-cancel",
|
||||
"http:allow-fetch-read-body",
|
||||
"http:allow-fetch-send",
|
||||
"autostart:default",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:tray:default",
|
||||
"core:tray:allow-new",
|
||||
"core:tray:allow-set-icon",
|
||||
"core:tray:allow-set-menu",
|
||||
"core:tray:allow-set-tooltip",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-unminimize",
|
||||
"core:image:default",
|
||||
"core:image:allow-from-path",
|
||||
"updater:default",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"notification:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "https://crm.yunvip123.com/**" }, { "url": "https://www.baidu.com/**" }]
|
||||
},
|
||||
{
|
||||
"identifier": "http:allow-fetch",
|
||||
"allow": [{ "url": "https://crm.yunvip123.com/**" }, { "url": "https://www.baidu.com/**" }]
|
||||
},
|
||||
"http:allow-fetch-cancel",
|
||||
"http:allow-fetch-read-body",
|
||||
"http:allow-fetch-send",
|
||||
"autostart:default",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:tray:default",
|
||||
"core:tray:allow-new",
|
||||
"core:tray:allow-set-icon",
|
||||
"core:tray:allow-set-menu",
|
||||
"core:tray:allow-set-tooltip",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-unminimize",
|
||||
"core:image:default",
|
||||
"core:image:allow-from-path",
|
||||
"updater:default",
|
||||
"process:allow-restart",
|
||||
"store:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ pub fn run() {
|
|||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![]),
|
||||
))
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.setup(|app| {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "工单系统监测",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"identifier": "com.yunpu.workordermonitor",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
|
@ -23,28 +23,15 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": [
|
||||
"nsis"
|
||||
],
|
||||
"targets": ["nsis"],
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"icons/32x32.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"resources": ["icons/32x32.png", "icons/icon.ico"]
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDY0Q0YxMzNBM0Q5OEU4NEMKUldSTTZKZzlPaFBQWkVXRVdBVHdTdlNvVnZSL2FFQkZtbFpRTzdjNGxHbjJsZ1R5MFRLdkpmYzMK",
|
||||
"endpoints": [
|
||||
"http://web.nps.yunvip123.cn/work-order-monitor/update.json"
|
||||
],
|
||||
"endpoints": ["http://web.nps.yunvip123.cn/work-order-monitor/update.json"],
|
||||
"dangerousInsecureTransportProtocol": true,
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
|
|
|
|||
119
src/App.vue
119
src/App.vue
|
|
@ -1,43 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { SettingOutlined, DashboardOutlined, ToolOutlined, FileTextOutlined } from "@ant-design/icons-vue";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DashboardOutlined,
|
||||
ToolOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { isEnabled as isAutostartEnabled } from "@tauri-apps/plugin-autostart";
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
import StatusHeader from "./components/StatusHeader.vue";
|
||||
import LoginForm from "./components/LoginForm.vue";
|
||||
import MonitorControl from "./components/MonitorControl.vue";
|
||||
import SettingsPanel from "./components/SettingsPanel.vue";
|
||||
import LogViewer from "./components/LogViewer.vue";
|
||||
import AppStatusBar, { type StatusBarItem } from "./components/AppStatusBar.vue";
|
||||
import CloseDialog from "./components/CloseDialog.vue";
|
||||
import UpdateDialog from "./components/UpdateDialog.vue";
|
||||
import { useMonitorStore } from "./stores/monitor";
|
||||
import AppStatusBar, { type StatusBarItem } from "@/components/AppStatusBar.vue";
|
||||
import CloseDialog from "@/components/CloseDialog.vue";
|
||||
import LogViewer from "@/components/LogViewer.vue";
|
||||
import LoginForm from "@/components/LoginForm.vue";
|
||||
import MonitorControl from "@/components/MonitorControl.vue";
|
||||
import SettingsPanel from "@/components/SettingsPanel.vue";
|
||||
import StatusHeader from "@/components/StatusHeader.vue";
|
||||
import UpdateDialog from "@/components/UpdateDialog.vue";
|
||||
import { useUpdater } from "@/composables/useUpdater";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
import { useMonitorStore } from "@/stores/monitor";
|
||||
import { useNetworkStore } from "@/stores/network";
|
||||
|
||||
const activeTab = ref("monitor");
|
||||
const store = useMonitorStore();
|
||||
const appStore = useAppStore();
|
||||
const logStore = useLogStore();
|
||||
const updater = useUpdater();
|
||||
const networkStore = useNetworkStore();
|
||||
|
||||
/** 通知动作事件的取消监听函数 */
|
||||
let unlistenNotification: UnlistenFn | null = null;
|
||||
/** 窗口关闭请求事件的取消监听函数 */
|
||||
let unlistenClose: UnlistenFn | null = null;
|
||||
|
||||
const statusItems = computed<StatusBarItem[]>(() => {
|
||||
const items: StatusBarItem[] = [];
|
||||
if (store.isPostponed) {
|
||||
items.push({ key: "postponed", type: "warning", content: store.message });
|
||||
} else if (store.message) {
|
||||
items.push({ key: "message", type: "info", content: store.message });
|
||||
items.push({ key: "postponed", type: "warning", content: appStore.message });
|
||||
} else if (appStore.message) {
|
||||
items.push({ key: "message", type: "info", content: appStore.message });
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.initialize();
|
||||
onMounted(async () => {
|
||||
appStore.loadConfig();
|
||||
store.loadCredentials();
|
||||
await store.setupTray();
|
||||
|
||||
try {
|
||||
const enabled = await isAutostartEnabled();
|
||||
appStore.autoStartEnabled = enabled;
|
||||
appStore.config.auto_start = enabled;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
unlistenNotification = await listen<string>("notification-action", (event) => {
|
||||
if (event.payload === "postpone_1h") store.postponeMonitoring();
|
||||
else if (event.payload === "go_handle") store.pauseForHandle();
|
||||
});
|
||||
|
||||
unlistenClose = await listen<void>("close-requested", async () => {
|
||||
const action = appStore.config.close_action;
|
||||
if (action === "minimize") {
|
||||
await getCurrentWindow().hide();
|
||||
} else if (action === "close") {
|
||||
await getCurrentWindow().destroy();
|
||||
} else {
|
||||
appStore.showCloseDialog = true;
|
||||
}
|
||||
});
|
||||
|
||||
logStore.addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||
|
||||
if (appStore.config.silent_start) {
|
||||
try {
|
||||
await getCurrentWindow().hide();
|
||||
logStore.addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
getCurrentWindow().setTitle("工单系统监测【开发版】");
|
||||
getCurrentWindow().setTitle(import.meta.env.VITE_APP_NAME);
|
||||
}
|
||||
|
||||
getVersion()
|
||||
.then((v) => {
|
||||
updater.currentVersion.value = v;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
networkStore.checkNetworkStatus();
|
||||
updater.checkForUpdates();
|
||||
|
||||
if (appStore.config.auto_monitor && store.rememberPassword) {
|
||||
const canAutoStart = store.loginMode === "token" ? !!store.tokenSessionId : !!(store.username && store.password);
|
||||
if (canAutoStart) {
|
||||
logStore.addLog("INFO", "自动登录并开始监测...", "SYSTEM");
|
||||
await store.startMonitoring();
|
||||
}
|
||||
}
|
||||
});
|
||||
onUnmounted(() => store.cleanup());
|
||||
|
||||
onUnmounted(() => {
|
||||
store.stopMonitoring();
|
||||
unlistenNotification?.();
|
||||
unlistenClose?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -47,7 +118,7 @@ onUnmounted(() => store.cleanup());
|
|||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="app-content">
|
||||
<a-tabs v-model:activeKey="activeTab" tab-position="left" class="main-tabs">
|
||||
<a-tabs v-model:active-key="activeTab" tab-position="left" class="main-tabs">
|
||||
<a-tab-pane key="monitor">
|
||||
<template #tab>
|
||||
<span><DashboardOutlined /> 监测</span>
|
||||
|
|
|
|||
|
|
@ -1,67 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
export interface StatusBarItem {
|
||||
key: string;
|
||||
content: string;
|
||||
type?: "info" | "warning" | "success" | "error";
|
||||
}
|
||||
|
||||
defineProps<{ items?: StatusBarItem[] }>();
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
info: "rgba(255,255,255,0.85)",
|
||||
warning: "#ffe58f",
|
||||
success: "#95de64",
|
||||
error: "#ff7875",
|
||||
};
|
||||
|
||||
const iconMap: Record<string, unknown> = {
|
||||
info: InfoCircleOutlined,
|
||||
warning: ExclamationCircleOutlined,
|
||||
success: CheckCircleOutlined,
|
||||
error: CloseCircleOutlined,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-statusbar">
|
||||
<template v-for="(item, index) in items" :key="item.key">
|
||||
<span v-if="index > 0" class="statusbar-sep" />
|
||||
<span class="statusbar-item" :style="{ color: colorMap[item.type ?? 'info'] }">
|
||||
<component :is="iconMap[item.type ?? 'info']" />
|
||||
{{ item.content }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 28px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(8px);
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.statusbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
}
|
||||
.statusbar-sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
export interface StatusBarItem {
|
||||
key: string;
|
||||
content: string;
|
||||
type?: "info" | "warning" | "success" | "error";
|
||||
}
|
||||
|
||||
defineProps<{ items?: StatusBarItem[] }>();
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
info: "rgba(255,255,255,0.85)",
|
||||
warning: "#ffe58f",
|
||||
success: "#95de64",
|
||||
error: "#ff7875",
|
||||
};
|
||||
|
||||
const iconMap: Record<string, unknown> = {
|
||||
info: InfoCircleOutlined,
|
||||
warning: ExclamationCircleOutlined,
|
||||
success: CheckCircleOutlined,
|
||||
error: CloseCircleOutlined,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-statusbar">
|
||||
<template v-for="(item, index) in items" :key="item.key">
|
||||
<span v-if="index > 0" class="statusbar-sep" />
|
||||
<span class="statusbar-item" :style="{ color: colorMap[item.type ?? 'info'] }">
|
||||
<component :is="iconMap[item.type ?? 'info']" />
|
||||
{{ item.content }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 28px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(8px);
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.statusbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
}
|
||||
.statusbar-sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
const rememberClose = ref(false);
|
||||
|
||||
function handleClose(action: "minimize" | "close") {
|
||||
store.handleCloseAction(action, rememberClose.value);
|
||||
rememberClose.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="store.showCloseDialog"
|
||||
title="关闭窗口"
|
||||
:width="340"
|
||||
centered
|
||||
>
|
||||
<div class="dialog-body">
|
||||
<p class="dialog-tip">请选择关闭窗口时的操作:</p>
|
||||
<a-checkbox v-model:checked="rememberClose">记住我的选择</a-checkbox>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleClose('minimize')">最小化到托盘</a-button>
|
||||
<a-button danger type="primary" @click="handleClose('close')">直接退出程序</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.dialog-tip {
|
||||
margin: 0 0 12px;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const rememberClose = ref(false);
|
||||
|
||||
function handleClose(action: "minimize" | "close") {
|
||||
appStore.handleCloseAction(action, rememberClose.value);
|
||||
rememberClose.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal v-model:open="appStore.showCloseDialog" title="关闭窗口" :width="340" centered>
|
||||
<div class="dialog-body">
|
||||
<p class="dialog-tip">请选择关闭窗口时的操作:</p>
|
||||
<a-checkbox v-model:checked="rememberClose"> 记住我的选择 </a-checkbox>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleClose('minimize')"> 最小化到托盘 </a-button>
|
||||
<a-button danger type="primary" @click="handleClose('close')"> 直接退出程序 </a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.dialog-tip {
|
||||
margin: 0 0 12px;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { DeleteOutlined, DownloadOutlined } from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
import { computed } from "vue";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
|
||||
const store = useMonitorStore();
|
||||
const reversedLogs = computed(() => [...store.logs].reverse());
|
||||
const logStore = useLogStore();
|
||||
const reversedLogs = computed(() => [...logStore.logs].reverse());
|
||||
|
||||
const levelColor: Record<string, string> = {
|
||||
INFO: "blue",
|
||||
|
|
@ -17,32 +17,34 @@ const levelColor: Record<string, string> = {
|
|||
<template>
|
||||
<div class="log-viewer">
|
||||
<div class="log-viewer__toolbar">
|
||||
<span class="log-viewer__count">共 {{ store.logs.length }} 条日志</span>
|
||||
<span class="log-viewer__count">共 {{ logStore.logs.length }} 条日志</span>
|
||||
<a-space>
|
||||
<a-button size="small" :disabled="store.logs.length === 0" @click="store.exportLogs()">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
<a-button size="small" :disabled="logStore.logs.length === 0" @click="logStore.exportLogs()">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
导出
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="store.clearLogs()">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button size="small" danger @click="logStore.clearLogs()">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
清除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-empty v-if="store.logs.length === 0" description="暂无日志记录" />
|
||||
<a-empty v-if="logStore.logs.length === 0" description="暂无日志记录" />
|
||||
|
||||
<div v-else class="log-viewer__list">
|
||||
<div
|
||||
v-for="log in reversedLogs"
|
||||
:key="`${log.timestamp}-${log.message}`"
|
||||
class="log-item"
|
||||
>
|
||||
<div v-for="log in reversedLogs" :key="`${log.timestamp}-${log.message}`" class="log-item">
|
||||
<span class="log-item__time">{{ log.timestamp }}</span>
|
||||
<a-tag :color="levelColor[log.level] || 'default'" size="small" class="log-item__tag">
|
||||
{{ log.level }}
|
||||
</a-tag>
|
||||
<a-tag color="geekblue" size="small" class="log-item__tag">{{ log.category }}</a-tag>
|
||||
<a-tag color="geekblue" size="small" class="log-item__tag">
|
||||
{{ log.category }}
|
||||
</a-tag>
|
||||
<span class="log-item__msg">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
import { ref } from "vue";
|
||||
import { useMonitorStore } from "@/stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
/** 表单操作加载状态 */
|
||||
const isLoading = ref(false);
|
||||
|
||||
async function handleSaveSettings() {
|
||||
isLoading.value = true;
|
||||
await store.saveSettings();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
async function handleStartMonitoring() {
|
||||
isLoading.value = true;
|
||||
await store.startMonitoring();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
async function handleStopMonitoring() {
|
||||
isLoading.value = true;
|
||||
await store.stopMonitoring();
|
||||
isLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-radio-group v-model:value="store.loginMode" :disabled="store.isMonitoring">
|
||||
<a-radio-button value="password">账号密码登录</a-radio-button>
|
||||
<a-radio-button value="token">Token 登录</a-radio-button>
|
||||
<a-radio-button value="password"> 账号密码登录 </a-radio-button>
|
||||
<a-radio-button value="token"> Token 登录 </a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="store.loginMode === 'password'">
|
||||
<a-form-item label="用户名">
|
||||
<a-input
|
||||
v-model:value="store.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="store.isLoading"
|
||||
/>
|
||||
<a-input v-model:value="store.username" placeholder="请输入用户名" :disabled="isLoading" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input-password
|
||||
v-model:value="store.password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="store.isLoading"
|
||||
/>
|
||||
<a-input-password v-model:value="store.password" placeholder="请输入密码" :disabled="isLoading" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -35,29 +48,33 @@ const store = useMonitorStore();
|
|||
<a-input
|
||||
v-model:value="store.tokenSessionId"
|
||||
placeholder="请输入 ASP.NET_SessionId"
|
||||
:disabled="store.isLoading"
|
||||
:disabled="isLoading"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-typography-text type="secondary" style="font-size: 12px; display: block; margin-bottom: 12px;">
|
||||
<a-typography-text type="secondary" style="font-size: 12px; display: block; margin-bottom: 12px">
|
||||
从浏览器开发者工具 (F12) → Application → Cookies 中复制 ASP.NET_SessionId 的值
|
||||
</a-typography-text>
|
||||
</template>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="store.rememberPassword">
|
||||
记住{{ store.loginMode === 'password' ? '密码' : 'Token' }}
|
||||
记住{{ store.loginMode === "password" ? "密码" : "Token" }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="store.isLoading" @click="store.saveSettings()">
|
||||
保存设置
|
||||
</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="isLoading" @click="handleSaveSettings()"> 保存设置 </a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
:disabled="store.isMonitoring"
|
||||
style="background: #52c41a; border-color: #52c41a"
|
||||
@click="handleStartMonitoring()"
|
||||
>
|
||||
开始监测
|
||||
</a-button>
|
||||
<a-button danger :disabled="store.isLoading || !store.isMonitoring" @click="store.stopMonitoring()">
|
||||
<a-button danger :disabled="isLoading || !store.isMonitoring" @click="handleStopMonitoring()">
|
||||
停止监测
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
MinusCircleOutlined,
|
||||
|
|
@ -9,11 +6,24 @@ import {
|
|||
WifiOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { useMonitorStore } from "@/stores/monitor";
|
||||
import { useNetworkStore } from "@/stores/network";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const store = useMonitorStore();
|
||||
const networkStore = useNetworkStore();
|
||||
/** 手动检查加载状态 */
|
||||
const isLoading = ref(false);
|
||||
|
||||
async function handleManualCheck() {
|
||||
isLoading.value = true;
|
||||
await store.manualCheck();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
const countdown = ref("");
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
|
@ -46,16 +56,32 @@ onUnmounted(() => {
|
|||
<div class="monitor-control">
|
||||
<div class="ticket-dashboard">
|
||||
<div class="ticket-card ticket-card--pending">
|
||||
<a-statistic title="待处理" :value="store.ticketCounts.pending" :value-style="{ color: store.ticketCounts.pending > 0 ? '#cf1322' : '#3f8600' }" />
|
||||
<a-statistic
|
||||
title="待处理"
|
||||
:value="store.ticketCounts.pending"
|
||||
:value-style="{ color: store.ticketCounts.pending > 0 ? '#cf1322' : '#3f8600' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="ticket-card ticket-card--stayclose">
|
||||
<a-statistic title="待关闭" :value="store.ticketCounts.stayclose" :value-style="{ color: store.ticketCounts.stayclose > 0 ? '#d46b08' : '#3f8600' }" />
|
||||
<a-statistic
|
||||
title="待关闭"
|
||||
:value="store.ticketCounts.stayclose"
|
||||
:value-style="{ color: store.ticketCounts.stayclose > 0 ? '#d46b08' : '#3f8600' }"
|
||||
/>
|
||||
</div>
|
||||
<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 class="ticket-card ticket-card--workorder">
|
||||
<a-statistic title="待审核" :value="store.ticketCounts.workOrderCount" :value-style="{ color: store.ticketCounts.workOrderCount > 0 ? '#722ed1' : '#3f8600' }" />
|
||||
<a-statistic
|
||||
title="待审核"
|
||||
:value="store.ticketCounts.workOrderCount"
|
||||
:value-style="{ color: store.ticketCounts.workOrderCount > 0 ? '#722ed1' : '#3f8600' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -65,26 +91,32 @@ onUnmounted(() => {
|
|||
|
||||
<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>
|
||||
<a-button size="small" type="link" @click="store.resumeMonitoring()"> 立即恢复 </a-button>
|
||||
</div>
|
||||
|
||||
<a-space wrap :size="[12, 12]" style="margin-top: 20px">
|
||||
<a-button :loading="store.isLoading" :disabled="!store.isLoggedIn" @click="store.manualCheck()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
<a-button :loading="isLoading" :disabled="!store.isLoggedIn" @click="handleManualCheck()">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
手动检查
|
||||
</a-button>
|
||||
<a-button @click="store.checkNetworkStatus()">
|
||||
<template #icon><WifiOutlined /></template>
|
||||
<a-button @click="networkStore.checkNetworkStatus()">
|
||||
<template #icon>
|
||||
<WifiOutlined />
|
||||
</template>
|
||||
网络检测
|
||||
</a-button>
|
||||
<a-button @click="store.testNotification()">
|
||||
<template #icon><BellOutlined /></template>
|
||||
<template #icon>
|
||||
<BellOutlined />
|
||||
</template>
|
||||
测试通知
|
||||
</a-button>
|
||||
<a-button @click="store.minimizeToTray()">
|
||||
<template #icon><MinusCircleOutlined /></template>
|
||||
<template #icon>
|
||||
<MinusCircleOutlined />
|
||||
</template>
|
||||
最小化到托盘
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
|
|
|||
|
|
@ -1,101 +1,100 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, watch } from "vue";
|
||||
import { CloudSyncOutlined } from "@ant-design/icons-vue";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
import { debounce } from "lodash-es";
|
||||
import { computed, watch } from "vue";
|
||||
import { useUpdater } from "@/composables/useUpdater";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const store = useMonitorStore();
|
||||
const appStore = useAppStore();
|
||||
const { currentVersion, isCheckingUpdates, isUpdating, checkForUpdates } = useUpdater();
|
||||
|
||||
// ===== 检查间隔:内部用秒,TimePicker 用 Dayjs =====
|
||||
const intervalTime = computed<Dayjs>({
|
||||
get() {
|
||||
const t = store.config.check_interval;
|
||||
return dayjs().hour(Math.floor(t / 3600)).minute(Math.floor((t % 3600) / 60)).second(t % 60).millisecond(0);
|
||||
const t = appStore.config.check_interval;
|
||||
const hours = Math.floor(t / 3600);
|
||||
const minutes = Math.floor((t % 3600) / 60);
|
||||
const seconds = t % 60;
|
||||
return dayjs().hour(hours).minute(minutes).second(seconds).millisecond(0);
|
||||
},
|
||||
set(val: Dayjs | null) {
|
||||
if (!val) return;
|
||||
store.config.check_interval = Math.max(10, val.hour() * 3600 + val.minute() * 60 + val.second());
|
||||
const totalSeconds = val.hour() * 3600 + val.minute() * 60 + val.second();
|
||||
appStore.config.check_interval = Math.max(10, totalSeconds);
|
||||
},
|
||||
});
|
||||
|
||||
// 自动保存:config 变化后防抖 500ms 触发保存
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
() => store.config,
|
||||
() => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => store.updateConfig(), 500);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
const saveConfig = debounce(() => appStore.updateConfig(), 500);
|
||||
watch(() => appStore.config, saveConfig, { deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="检查间隔">
|
||||
<a-time-picker
|
||||
v-model:value="intervalTime"
|
||||
format="HH:mm:ss"
|
||||
:allow-clear="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-hint">最短 10 秒,当前 {{ store.config.check_interval }} 秒</div>
|
||||
<a-time-picker v-model:value="intervalTime" format="HH:mm:ss" :allow-clear="false" style="width: 100%" />
|
||||
<div class="form-hint">最短 10 秒,当前 {{ appStore.config.check_interval }} 秒</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="网络超时(秒)">
|
||||
<a-input-number
|
||||
v-model:value="store.config.network_timeout"
|
||||
:min="5"
|
||||
:max="120"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="appStore.config.network_timeout" :min="5" :max="120" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-divider />
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">开机自启动</div>
|
||||
<a-switch v-model:checked="store.config.auto_start" />
|
||||
<a-switch v-model:checked="appStore.config.auto_start" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">
|
||||
启动后自动监测
|
||||
<div class="form-hint">需同时开启「记住密码」</div>
|
||||
</div>
|
||||
<a-switch v-model:checked="store.config.auto_monitor" />
|
||||
<a-switch v-model:checked="appStore.config.auto_monitor" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">
|
||||
静默启动
|
||||
<div class="form-hint">启动时直接最小化到托盘</div>
|
||||
</div>
|
||||
<a-switch v-model:checked="store.config.silent_start" />
|
||||
<a-switch v-model:checked="appStore.config.silent_start" />
|
||||
</div>
|
||||
<a-divider />
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">显示系统通知</div>
|
||||
<a-switch v-model:checked="store.config.show_notifications" />
|
||||
<a-switch v-model:checked="appStore.config.show_notifications" />
|
||||
</div>
|
||||
<div class="switch-row">
|
||||
<div class="switch-label">通知提示音</div>
|
||||
<a-switch
|
||||
v-model:checked="store.config.notification_sound"
|
||||
:disabled="!store.config.show_notifications"
|
||||
/>
|
||||
<a-switch v-model:checked="appStore.config.notification_sound" :disabled="!appStore.config.show_notifications" />
|
||||
</div>
|
||||
<a-divider />
|
||||
<a-form-item label="点击「去处理」后暂停时长(分钟)">
|
||||
<a-input-number
|
||||
v-model:value="store.config.handle_pause_minutes"
|
||||
:min="1"
|
||||
:max="120"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="appStore.config.handle_pause_minutes" :min="1" :max="120" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-divider />
|
||||
<a-form-item label="关闭窗口行为">
|
||||
<a-radio-group v-model:value="store.config.close_action">
|
||||
<a-radio value="ask">每次询问</a-radio>
|
||||
<a-radio value="minimize">最小化到托盘</a-radio>
|
||||
<a-radio value="close">直接退出</a-radio>
|
||||
<a-radio-group v-model:value="appStore.config.close_action">
|
||||
<a-radio value="ask"> 每次询问 </a-radio>
|
||||
<a-radio value="minimize"> 最小化到托盘 </a-radio>
|
||||
<a-radio value="close"> 直接退出 </a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-divider />
|
||||
<div class="about-card">
|
||||
<div class="about-card__row">
|
||||
<div>
|
||||
<div class="about-card__title">当前版本</div>
|
||||
<div class="about-card__value">v{{ currentVersion || "--" }}</div>
|
||||
</div>
|
||||
<a-button type="primary" :loading="isCheckingUpdates" :disabled="isUpdating" @click="checkForUpdates()">
|
||||
<template #icon>
|
||||
<CloudSyncOutlined />
|
||||
</template>
|
||||
检查更新
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="form-hint">手动检查是否有可安装的新版本。</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
|
|
@ -125,4 +124,30 @@ watch(
|
|||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.about-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.about-card__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.about-card__title {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.about-card__value {
|
||||
margin-top: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
CloseCircleFilled,
|
||||
SyncOutlined,
|
||||
PauseCircleFilled,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { APP_NAME, useMonitorStore } from "../stores/monitor";
|
||||
import { CheckCircleFilled, CloseCircleFilled, SyncOutlined, PauseCircleFilled } from "@ant-design/icons-vue";
|
||||
import { APP_NAME } from "@/constants/app";
|
||||
import { useMonitorStore } from "@/stores/monitor";
|
||||
import { useNetworkStore } from "@/stores/network";
|
||||
|
||||
const store = useMonitorStore();
|
||||
const networkStore = useNetworkStore();
|
||||
|
||||
function getNetworkStatusText() {
|
||||
if (!store.networkStatus.is_connected) return "网络断开";
|
||||
if (!store.networkStatus.api_reachable) return "API不可达";
|
||||
if (!networkStore.isConnected) return "网络断开";
|
||||
if (!networkStore.apiReachable) return "API不可达";
|
||||
return "网络正常";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="status-header">
|
||||
<h1 class="status-header__title">{{ APP_NAME }}</h1>
|
||||
<a-space :size="12">
|
||||
<a-tag :color="store.isLoggedIn ? 'success' : 'error'">
|
||||
<template #icon>
|
||||
<CheckCircleFilled v-if="store.isLoggedIn" />
|
||||
<CloseCircleFilled v-else />
|
||||
</template>
|
||||
{{ store.isLoggedIn ? "已登录" : "未登录" }}
|
||||
</a-tag>
|
||||
<a-tag v-if="store.isMonitoring" :color="store.isPostponed ? 'warning' : 'processing'">
|
||||
<template #icon>
|
||||
<PauseCircleFilled v-if="store.isPostponed" />
|
||||
<SyncOutlined v-else :spin="true" />
|
||||
</template>
|
||||
{{ store.isPostponed ? "已推迟1小时" : "监测中" }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
:color="store.networkStatus.api_reachable ? 'success' : store.networkStatus.is_connected ? 'warning' : 'error'"
|
||||
>
|
||||
{{ getNetworkStatusText() }}
|
||||
</a-tag>
|
||||
<span v-if="store.networkStatus.response_time" class="response-time">
|
||||
{{ store.networkStatus.response_time }}ms
|
||||
</span>
|
||||
</a-space>
|
||||
<h1 class="status-header__title">
|
||||
{{ APP_NAME }}
|
||||
</h1>
|
||||
<div class="status-header__actions">
|
||||
<a-space :size="12" wrap>
|
||||
<a-tag :color="store.isLoggedIn ? 'success' : 'error'">
|
||||
<template #icon>
|
||||
<CheckCircleFilled v-if="store.isLoggedIn" />
|
||||
<CloseCircleFilled v-else />
|
||||
</template>
|
||||
{{ store.isLoggedIn ? "已登录" : "未登录" }}
|
||||
</a-tag>
|
||||
<a-tag v-if="store.isMonitoring" :color="store.isPostponed ? 'warning' : 'processing'">
|
||||
<template #icon>
|
||||
<PauseCircleFilled v-if="store.isPostponed" />
|
||||
<SyncOutlined v-else :spin="true" />
|
||||
</template>
|
||||
{{ store.isPostponed ? "已推迟1小时" : "监测中" }}
|
||||
</a-tag>
|
||||
<a-tag :color="networkStore.apiReachable ? 'success' : networkStore.isConnected ? 'warning' : 'error'">
|
||||
{{ getNetworkStatusText() }}
|
||||
</a-tag>
|
||||
<span v-if="networkStore.responseTime" class="response-time"> {{ networkStore.responseTime }}ms </span>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -60,6 +58,11 @@ function getNetworkStatusText() {
|
|||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.status-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.response-time {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
|
|
|||
|
|
@ -1,99 +1,101 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!store.updateProgress || !store.updateProgress.total) return 0;
|
||||
return Math.round((store.updateProgress.downloaded / store.updateProgress.total) * 100);
|
||||
});
|
||||
|
||||
const downloadedMB = computed(() => {
|
||||
if (!store.updateProgress) return "0";
|
||||
return (store.updateProgress.downloaded / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
|
||||
const totalMB = computed(() => {
|
||||
if (!store.updateProgress || !store.updateProgress.total) return "0";
|
||||
return (store.updateProgress.total / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="store.showUpdateDialog"
|
||||
title="发现新版本"
|
||||
:width="400"
|
||||
centered
|
||||
:closable="!store.isUpdating"
|
||||
:maskClosable="!store.isUpdating"
|
||||
>
|
||||
<div class="update-body">
|
||||
<div v-if="!store.isUpdating">
|
||||
<p class="version-info">
|
||||
新版本 <span class="version-tag">v{{ store.updateInfo?.version }}</span> 已发布
|
||||
</p>
|
||||
<div v-if="store.updateInfo?.notes" class="update-notes">
|
||||
<p class="notes-title">更新说明:</p>
|
||||
<p class="notes-content">{{ store.updateInfo.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="update-progress">
|
||||
<p>正在下载更新...</p>
|
||||
<a-progress :percent="progressPercent" :status="progressPercent >= 100 ? 'success' : 'active'" />
|
||||
<p class="progress-detail">{{ downloadedMB }} / {{ totalMB }} MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!store.isUpdating">
|
||||
<a-button @click="store.dismissUpdate()">稍后提醒</a-button>
|
||||
<a-button type="primary" @click="store.confirmUpdate()">立即更新</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button disabled>更新中,请稍候...</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.update-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.version-info {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
.version-tag {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
}
|
||||
.notes-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
.notes-content {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.update-progress {
|
||||
text-align: center;
|
||||
}
|
||||
.update-progress p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.progress-detail {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useUpdater } from "@/composables/useUpdater";
|
||||
|
||||
const { showUpdateDialog, updateInfo, updateProgress, isUpdating, confirmUpdate, dismissUpdate } = useUpdater();
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!updateProgress.value || !updateProgress.value.total) return 0;
|
||||
return Math.round((updateProgress.value.downloaded / updateProgress.value.total) * 100);
|
||||
});
|
||||
|
||||
const downloadedMB = computed(() => {
|
||||
if (!updateProgress.value) return "0";
|
||||
return (updateProgress.value.downloaded / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
|
||||
const totalMB = computed(() => {
|
||||
if (!updateProgress.value || !updateProgress.value.total) return "0";
|
||||
return (updateProgress.value.total / 1024 / 1024).toFixed(1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="showUpdateDialog"
|
||||
title="发现新版本"
|
||||
:width="400"
|
||||
centered
|
||||
:closable="!isUpdating"
|
||||
:mask-closable="!isUpdating"
|
||||
>
|
||||
<div class="update-body">
|
||||
<div v-if="!isUpdating">
|
||||
<p class="version-info">
|
||||
新版本 <span class="version-tag">v{{ updateInfo?.version }}</span> 已发布
|
||||
</p>
|
||||
<div v-if="updateInfo?.notes" class="update-notes">
|
||||
<p class="notes-title">更新说明:</p>
|
||||
<p class="notes-content">
|
||||
{{ updateInfo.notes }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="update-progress">
|
||||
<p>正在下载更新...</p>
|
||||
<a-progress :percent="progressPercent" :status="progressPercent >= 100 ? 'success' : 'active'" />
|
||||
<p class="progress-detail">{{ downloadedMB }} / {{ totalMB }} MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!isUpdating">
|
||||
<a-button @click="dismissUpdate()"> 稍后提醒 </a-button>
|
||||
<a-button type="primary" @click="confirmUpdate()"> 立即更新 </a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button disabled>更新中,请稍候...</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.update-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.version-info {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
.version-tag {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
}
|
||||
.notes-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
.notes-content {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.update-progress {
|
||||
text-align: center;
|
||||
}
|
||||
.update-progress p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.progress-detail {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,582 +0,0 @@
|
|||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { TrayIcon } from "@tauri-apps/api/tray";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import {
|
||||
enable as enableAutostart,
|
||||
disable as disableAutostart,
|
||||
isEnabled as isAutostartEnabled,
|
||||
} from "@tauri-apps/plugin-autostart";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
|
||||
export const APP_NAME = "工单系统监测";
|
||||
const API_BASE = "https://crm.yunvip123.com/api";
|
||||
const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
|
||||
const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
|
||||
const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface TicketCounts {
|
||||
pending: number;
|
||||
stayclose: number;
|
||||
confirm: number;
|
||||
workOrderCount: number;
|
||||
lastCheck: string;
|
||||
}
|
||||
|
||||
export function useMonitor() {
|
||||
// ===== 响应式数据 =====
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const rememberPassword = ref(false);
|
||||
const message = ref("");
|
||||
const isLoading = ref(false);
|
||||
const isLoggedIn = ref(false);
|
||||
const isMonitoring = ref(false);
|
||||
const isPostponed = ref(false);
|
||||
const autoStartEnabled = ref(false);
|
||||
const logs = ref<LogEntry[]>([]);
|
||||
const ticketCounts = ref<TicketCounts>({
|
||||
pending: 0, stayclose: 0, confirm: 0, workOrderCount: 0, lastCheck: "",
|
||||
});
|
||||
const networkStatus = ref({
|
||||
is_connected: false,
|
||||
api_reachable: false,
|
||||
response_time: null as number | null,
|
||||
last_check: "",
|
||||
});
|
||||
const config = ref({
|
||||
check_interval: 60,
|
||||
auto_start: false,
|
||||
auto_monitor: false,
|
||||
show_notifications: true,
|
||||
notification_sound: true,
|
||||
network_timeout: 30,
|
||||
});
|
||||
|
||||
// ===== 内部状态 =====
|
||||
let sessionId = "";
|
||||
let userGid = "";
|
||||
let userName = "";
|
||||
let monitorTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let postponeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let unlistenNotification: UnlistenFn | null = null;
|
||||
|
||||
// ===== 工具函数 =====
|
||||
function formatTime(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function addLog(level: string, msg: string, category: string) {
|
||||
logs.value.push({ timestamp: formatTime(new Date()), level, message: msg, category });
|
||||
if (logs.value.length > 1000) logs.value.shift();
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs.value = [];
|
||||
message.value = "日志已清除";
|
||||
}
|
||||
|
||||
function clearMessage() {
|
||||
message.value = "";
|
||||
}
|
||||
|
||||
// ===== 通知声音 =====
|
||||
function playNotificationSound() {
|
||||
if (!config.value.notification_sound) return;
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
|
||||
oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15);
|
||||
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4);
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + 0.4);
|
||||
} catch { /* 静默失败 */ }
|
||||
}
|
||||
|
||||
// ===== 通知 =====
|
||||
async function notify(title: string, body: string) {
|
||||
if (!config.value.show_notifications) return;
|
||||
let granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
const permission = await requestPermission();
|
||||
granted = permission === "granted";
|
||||
}
|
||||
if (granted) {
|
||||
try {
|
||||
await invoke("send_clickable_notification", { title, body });
|
||||
playNotificationSound();
|
||||
addLog("INFO", `通知: ${title} - ${body}`, "NOTIFICATION");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `发送通知失败: ${e}`, "NOTIFICATION");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== API =====
|
||||
async function apiLogin(): Promise<boolean> {
|
||||
try {
|
||||
const body = JSON.stringify({ Account: username.value, PassWord: password.value });
|
||||
const resp = await fetch(LOGIN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": String(new TextEncoder().encode(body).length),
|
||||
Host: "crm.yunvip123.com",
|
||||
"Cache-Control": "no-cache",
|
||||
Cookie: "",
|
||||
},
|
||||
body,
|
||||
credentials: "omit",
|
||||
});
|
||||
|
||||
const setCookie = resp.headers.get("set-cookie") || "";
|
||||
const match = setCookie.match(/ASP\.NET_SessionId=([^;]+)/);
|
||||
if (match) sessionId = match[1];
|
||||
addLog("DEBUG", `SessionId: ${sessionId || "(未提取)"}`, "LOGIN");
|
||||
|
||||
const data = await resp.json();
|
||||
if (data.success && data.data) {
|
||||
userGid = data.data.GID;
|
||||
userName = data.data.SU_UserName || "用户";
|
||||
if (!sessionId) {
|
||||
addLog("WARN", "未能从响应头提取SessionId", "LOGIN");
|
||||
}
|
||||
isLoggedIn.value = true;
|
||||
addLog("INFO", `登录成功,用户: ${userName}, GID: ${userGid}`, "LOGIN");
|
||||
return true;
|
||||
} else {
|
||||
addLog("ERROR", `登录失败: ${data.msg}`, "LOGIN");
|
||||
return false;
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `登录请求失败: ${e.message || e}`, "LOGIN");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiCheckWorkOrders(): Promise<number> {
|
||||
try {
|
||||
const resp = await fetch(WORK_ORDER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `ASP.NET_SessionId=${sessionId}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isSelect: 8,
|
||||
isShow: 3,
|
||||
IsExport: 0,
|
||||
PageIndex: 1,
|
||||
PageSize: 20,
|
||||
}),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success && result.data) {
|
||||
const count = result.data.DataCount || 0;
|
||||
addLog("INFO", `待审核检查完成 - 数据条数: ${count}`, "SCHEDULER");
|
||||
return count;
|
||||
} else {
|
||||
addLog("WARN", `待审核 API 返回异常: ${result.msg}`, "SCHEDULER");
|
||||
return 0;
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `待审核 API 检查失败: ${e.message || e}`, "SCHEDULER");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiCheckStatus(isRetry = false): Promise<void> {
|
||||
if (!userGid) {
|
||||
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
|
||||
if (!isRetry) await tryReLogin();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(CHECK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `ASP.NET_SessionId=${sessionId}`,
|
||||
},
|
||||
body: JSON.stringify({ UserGID: userGid }),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const pending = d.PendingCount || 0;
|
||||
const stayclose = d.StaycloseCount || 0;
|
||||
const confirm = d.ConfirmCount || 0;
|
||||
const workOrderCount = await apiCheckWorkOrders();
|
||||
ticketCounts.value = {
|
||||
pending, stayclose, confirm, workOrderCount,
|
||||
lastCheck: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}, 待审核: ${workOrderCount}`, "SCHEDULER");
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0 || workOrderCount > 0) {
|
||||
const msgs: string[] = [];
|
||||
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
|
||||
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
|
||||
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
|
||||
if (workOrderCount > 0) msgs.push(`待审核: ${workOrderCount}`);
|
||||
await notify("工单提醒", msgs.join(","));
|
||||
}
|
||||
} else {
|
||||
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
|
||||
if (!isRetry && isSessionExpiredMsg(result.msg)) {
|
||||
await tryReLogin();
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `API 检查失败: ${e.message || e}`, "SCHEDULER");
|
||||
}
|
||||
}
|
||||
|
||||
function isSessionExpiredMsg(msg: string | undefined): boolean {
|
||||
if (!msg) return false;
|
||||
const keywords = ["session", "expired", "unauthorized", "登录", "过期", "失效", "超时"];
|
||||
const lower = msg.toLowerCase();
|
||||
return keywords.some((k) => lower.includes(k));
|
||||
}
|
||||
|
||||
async function tryReLogin(): Promise<void> {
|
||||
if (!username.value || !password.value) {
|
||||
addLog("WARN", "无法自动重登录:缺少凭据", "SESSION");
|
||||
return;
|
||||
}
|
||||
addLog("INFO", "会话可能已过期,正在自动重新登录...", "SESSION");
|
||||
const ok = await apiLogin();
|
||||
if (ok) {
|
||||
addLog("INFO", "自动重新登录成功,继续检查", "SESSION");
|
||||
await apiCheckStatus(true);
|
||||
} else {
|
||||
addLog("ERROR", "自动重新登录失败,请手动重新登录", "SESSION");
|
||||
message.value = "会话已过期且自动重登录失败,请手动操作";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 网络检测 =====
|
||||
async function checkNetworkStatus() {
|
||||
isLoading.value = true;
|
||||
let isConnected = false;
|
||||
let apiReachable = false;
|
||||
let responseTime: number | null = null;
|
||||
|
||||
try {
|
||||
const resp = await fetch("https://www.baidu.com", {
|
||||
method: "GET",
|
||||
connectTimeout: config.value.network_timeout * 1000,
|
||||
});
|
||||
isConnected = resp.ok;
|
||||
} catch {
|
||||
/* 网络不通 */
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
const apiStart = Date.now();
|
||||
const resp = await fetch(LOGIN_URL, {
|
||||
method: "HEAD",
|
||||
connectTimeout: config.value.network_timeout * 1000,
|
||||
});
|
||||
responseTime = Date.now() - apiStart;
|
||||
apiReachable = resp.status < 500;
|
||||
} catch {
|
||||
/* API 不可达 */
|
||||
}
|
||||
}
|
||||
|
||||
networkStatus.value = {
|
||||
is_connected: isConnected,
|
||||
api_reachable: apiReachable,
|
||||
response_time: responseTime,
|
||||
last_check: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `网络检查完成 - 连接: ${isConnected}, API可达: ${apiReachable}`, "NETWORK");
|
||||
message.value = "网络状态检查完成";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ===== 凭据存储 =====
|
||||
function loadCredentials() {
|
||||
try {
|
||||
const saved = localStorage.getItem("crm_credentials");
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
username.value = data.username || "";
|
||||
password.value = data.password || "";
|
||||
rememberPassword.value = data.remember || false;
|
||||
addLog("INFO", "已加载保存的凭据", "CONFIG");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `加载凭据失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
}
|
||||
|
||||
function saveCredentials() {
|
||||
try {
|
||||
if (rememberPassword.value) {
|
||||
localStorage.setItem(
|
||||
"crm_credentials",
|
||||
JSON.stringify({ username: username.value, password: password.value, remember: true })
|
||||
);
|
||||
addLog("INFO", "凭据已保存到本地", "CONFIG");
|
||||
} else {
|
||||
localStorage.removeItem("crm_credentials");
|
||||
addLog("INFO", "已清除本地凭据", "CONFIG");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `保存凭据失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const saved = localStorage.getItem("crm_config");
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
config.value = { ...config.value, ...data };
|
||||
addLog("INFO", "已加载保存的配置", "CONFIG");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `加载配置失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfigToStorage() {
|
||||
try {
|
||||
localStorage.setItem("crm_config", JSON.stringify(config.value));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
message.value = "请输入用户名和密码";
|
||||
return;
|
||||
}
|
||||
saveCredentials();
|
||||
message.value = "设置保存成功";
|
||||
addLog("INFO", "用户凭据已保存", "CONFIG");
|
||||
}
|
||||
|
||||
function exportLogs() {
|
||||
if (logs.value.length === 0) {
|
||||
message.value = "暂无日志可导出";
|
||||
return;
|
||||
}
|
||||
const lines = logs.value.map(
|
||||
(l) => `[${l.timestamp}] [${l.level}] [${l.category}] ${l.message}`
|
||||
);
|
||||
const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `monitor-logs-${formatTime(new Date()).replaceAll(/[: ]/g, "-")}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
addLog("INFO", `已导出 ${logs.value.length} 条日志`, "SYSTEM");
|
||||
message.value = "日志导出成功";
|
||||
}
|
||||
|
||||
// ===== 监测控制 =====
|
||||
async function startMonitoring() {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
message.value = "请先设置用户名和密码";
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
const ok = await apiLogin();
|
||||
if (!ok) {
|
||||
message.value = "登录失败,无法开始监测";
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
isMonitoring.value = true;
|
||||
await apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "监测已启动", "MONITOR");
|
||||
message.value = `监测已开始,欢迎 ${userName}!`;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
function stopMonitoring() {
|
||||
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
||||
if (postponeTimer) { clearTimeout(postponeTimer); postponeTimer = null; }
|
||||
isPostponed.value = false;
|
||||
isMonitoring.value = false;
|
||||
isLoggedIn.value = false;
|
||||
sessionId = "";
|
||||
userGid = "";
|
||||
addLog("INFO", "监测已停止", "MONITOR");
|
||||
message.value = "监测已停止";
|
||||
}
|
||||
|
||||
function postponeMonitoring() {
|
||||
if (!isMonitoring.value) return;
|
||||
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
|
||||
if (postponeTimer) { clearTimeout(postponeTimer); }
|
||||
isPostponed.value = true;
|
||||
addLog("INFO", "监测已推迟1小时", "MONITOR");
|
||||
message.value = "监测已推迟1小时,将在1小时后自动恢复";
|
||||
postponeTimer = setTimeout(() => {
|
||||
isPostponed.value = false;
|
||||
postponeTimer = null;
|
||||
apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "推迟结束,监测已自动恢复", "MONITOR");
|
||||
message.value = "推迟结束,监测已自动恢复";
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function manualCheck() {
|
||||
if (!isLoggedIn.value) { message.value = "请先开始监测(登录)"; return; }
|
||||
isLoading.value = true;
|
||||
await apiCheckStatus();
|
||||
message.value = "手动检查完成";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
async function testNotification() {
|
||||
await notify("工单提醒(测试)", "待处理工单: 3,待关闭工单: 1,待确认工单: 2");
|
||||
}
|
||||
|
||||
// ===== 配置更新 =====
|
||||
async function updateConfig() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
if (config.value.auto_start) await enableAutostart();
|
||||
else await disableAutostart();
|
||||
autoStartEnabled.value = config.value.auto_start;
|
||||
addLog("INFO", `自启动设置: ${config.value.auto_start ? "启用" : "禁用"}`, "CONFIG");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `自启动设置失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
if (isMonitoring.value && monitorTimer) {
|
||||
clearInterval(monitorTimer);
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", `检查间隔已更新为: ${config.value.check_interval}秒`, "CONFIG");
|
||||
}
|
||||
saveConfigToStorage();
|
||||
message.value = "配置更新成功";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ===== 系统托盘 =====
|
||||
async function setupTray() {
|
||||
try {
|
||||
const mainWindow = getCurrentWindow();
|
||||
const menu = 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: "quit",
|
||||
text: "退出",
|
||||
action: async () => { await mainWindow.destroy(); },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 使用 Tauri 资源路径解析图标
|
||||
const iconPath = await resolveResource("icons/32x32.png");
|
||||
const icon = await Image.fromPath(iconPath);
|
||||
|
||||
await TrayIcon.new({
|
||||
icon,
|
||||
tooltip: APP_NAME,
|
||||
menu,
|
||||
menuOnLeftClick: false,
|
||||
action: async (event) => {
|
||||
if (event.type === "Click" && event.button === "Left") {
|
||||
await mainWindow.unminimize();
|
||||
await mainWindow.show();
|
||||
await mainWindow.setFocus();
|
||||
}
|
||||
},
|
||||
});
|
||||
addLog("INFO", "系统托盘创建成功", "SYSTEM");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
async function minimizeToTray() {
|
||||
try {
|
||||
await getCurrentWindow().hide();
|
||||
} catch (e: any) {
|
||||
message.value = `最小化失败: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 网络状态文本 =====
|
||||
function getNetworkStatusText() {
|
||||
if (!networkStatus.value.is_connected) return "网络断开";
|
||||
if (!networkStatus.value.api_reachable) return "API不可达";
|
||||
return "网络正常";
|
||||
}
|
||||
|
||||
// ===== 生命周期 =====
|
||||
onMounted(async () => {
|
||||
loadConfig();
|
||||
loadCredentials();
|
||||
await setupTray();
|
||||
try {
|
||||
autoStartEnabled.value = await isAutostartEnabled();
|
||||
config.value.auto_start = autoStartEnabled.value;
|
||||
} catch { /* ignore */ }
|
||||
unlistenNotification = await listen<string>("notification-action", (event) => {
|
||||
if (event.payload === "postpone_1h") postponeMonitoring();
|
||||
});
|
||||
addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||
checkNetworkStatus();
|
||||
|
||||
if (config.value.auto_monitor && rememberPassword.value && username.value && password.value) {
|
||||
addLog("INFO", "自动登录并开始监测...", "SYSTEM");
|
||||
await startMonitoring();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (monitorTimer) clearInterval(monitorTimer);
|
||||
if (postponeTimer) clearTimeout(postponeTimer);
|
||||
unlistenNotification?.();
|
||||
});
|
||||
|
||||
return {
|
||||
// 状态
|
||||
username, password, rememberPassword, message, isLoading,
|
||||
isLoggedIn, isMonitoring, isPostponed, autoStartEnabled,
|
||||
logs, networkStatus, config, ticketCounts,
|
||||
// 方法
|
||||
addLog, clearLogs, clearMessage, saveSettings, exportLogs,
|
||||
startMonitoring, stopMonitoring, manualCheck,
|
||||
testNotification, checkNetworkStatus, updateConfig,
|
||||
minimizeToTray, getNetworkStatusText,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
|
||||
/** 通知 Composable */
|
||||
export function useNotification() {
|
||||
const { addLog } = useLogStore();
|
||||
const { config } = storeToRefs(useAppStore());
|
||||
/** 使用 Web Audio API 播放双音节提示音 */
|
||||
function playNotificationSound() {
|
||||
if (!config.value.notification_sound) return;
|
||||
const ctx = new AudioContext();
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
|
||||
oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15);
|
||||
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4);
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + 0.4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知并播放提示音
|
||||
* @param title 通知标题
|
||||
* @param body 通知正文
|
||||
*/
|
||||
async function notify(title: string, body: string) {
|
||||
if (!config.value.show_notifications) return;
|
||||
let granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
const permission = await requestPermission();
|
||||
granted = permission === "granted";
|
||||
}
|
||||
if (granted) {
|
||||
try {
|
||||
await invoke("send_clickable_notification", { title, body });
|
||||
playNotificationSound();
|
||||
addLog("INFO", `通知: ${title} - ${body}`, "NOTIFICATION");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `发送通知失败: ${e}`, "NOTIFICATION");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { playNotificationSound, notify };
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* useTray.ts — 系统托盘 Composable
|
||||
*
|
||||
* 负责创建、更新系统托盘图标与右键菜单。
|
||||
* 图标颜色根据监测状态动态变化:
|
||||
* 灰色 — 未开始监测
|
||||
* 橙色 — 监测已暂停/推迟
|
||||
* 红色 — 有待处理工单
|
||||
* 绿色 — 监测中,无异常
|
||||
*/
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { TrayIcon } from "@tauri-apps/api/tray";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { type Ref } from "vue";
|
||||
import { CRM_URL } from "@/constants/app";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
import { type TicketCounts } from "@/stores/monitor";
|
||||
|
||||
/**
|
||||
* @param isMonitoring 是否正在监测
|
||||
* @param isPostponed 是否已推迟/暂停
|
||||
* @param ticketCounts 当前工单计数
|
||||
* @param startMonitoring 开始监测(托盘菜单调用)
|
||||
* @param stopMonitoring 停止监测(托盘菜单调用)
|
||||
*/
|
||||
export function useTray(
|
||||
isMonitoring: Ref<boolean>,
|
||||
isPostponed: Ref<boolean>,
|
||||
ticketCounts: Ref<TicketCounts>,
|
||||
startMonitoring: () => Promise<void>,
|
||||
stopMonitoring: () => void,
|
||||
) {
|
||||
const { addLog } = useLogStore();
|
||||
const appName = import.meta.env.VITE_APP_NAME;
|
||||
// 当前托盘图标实例,null 表示尚未初始化
|
||||
let trayIconInstance: TrayIcon | null = null;
|
||||
// SVG 图片缓存,避免重复加载
|
||||
let cachedSvgImg: HTMLImageElement | null = null;
|
||||
|
||||
/**
|
||||
* 加载 /gong.svg 并缓存,后续调用直接返回缓存结果。
|
||||
*/
|
||||
async function loadSvgImage(): Promise<HTMLImageElement> {
|
||||
if (cachedSvgImg) return cachedSvgImg;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new window.Image();
|
||||
img.onload = () => {
|
||||
cachedSvgImg = img;
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = "/gong.svg";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SVG 渲染到 Canvas,对每个像素按亮度重新着色后返回 RGBA 原始数据。
|
||||
*
|
||||
* 着色规则:
|
||||
* - 暗像素(图标笔画,亮度 < 128)→ 白色,确保图标在彩色背景上清晰可见
|
||||
* - 亮像素(背景) → 传入的 (r, g, b) 状态色
|
||||
*
|
||||
* SVG 加载失败时回退到纯色背景 + "工" 字文本方案。
|
||||
*
|
||||
* @param r 状态色红分量 (0-255)
|
||||
* @param g 状态色绿分量 (0-255)
|
||||
* @param b 状态色蓝分量 (0-255)
|
||||
* @param size 图标边长(像素),默认 32
|
||||
*/
|
||||
async function makeTrayIconRGBA(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
size = 32,
|
||||
): Promise<Uint8Array> {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
try {
|
||||
const svgImg = await loadSvgImage();
|
||||
ctx.drawImage(svgImg, 0, 0, size, size);
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// 根据亮度判断:暗像素(文字) → 白色,亮像素(背景) → 状态色
|
||||
const brightness =
|
||||
data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
|
||||
if (brightness < 128) {
|
||||
data[i] = data[i + 1] = data[i + 2] = 255;
|
||||
} else {
|
||||
data[i] = r;
|
||||
data[i + 1] = g;
|
||||
data[i + 2] = b;
|
||||
}
|
||||
data[i + 3] = 255;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
} catch {
|
||||
// SVG 加载失败,回退到纯 Canvas 绘制
|
||||
addLog("WARN", "SVG 图标加载失败,使用回退方案", "SYSTEM");
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
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 finalData = ctx.getImageData(0, 0, size, size);
|
||||
return new Uint8Array(finalData.data.buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前应用状态更新托盘图标颜色与 tooltip 文字。
|
||||
*
|
||||
* 状态优先级(高 → 低):
|
||||
* 未监测 > 已暂停 > 有工单 > 正常监测中
|
||||
*/
|
||||
async function updateTrayIcon() {
|
||||
if (!trayIconInstance) return;
|
||||
try {
|
||||
const hasTickets =
|
||||
ticketCounts.value.pending > 0 ||
|
||||
ticketCounts.value.stayclose > 0 ||
|
||||
ticketCounts.value.confirm > 0 ||
|
||||
ticketCounts.value.workOrderCount > 0;
|
||||
|
||||
let color: [number, number, number];
|
||||
let tooltip: string;
|
||||
|
||||
if (!isMonitoring.value) {
|
||||
// 未开始监测 — 灰色
|
||||
color = [158, 158, 158];
|
||||
tooltip = `${appName} - 未开始`;
|
||||
} else if (isPostponed.value) {
|
||||
// 已暂停/推迟 — 橙色
|
||||
color = [255, 152, 0];
|
||||
tooltip = `${appName} - 已暂停`;
|
||||
} else if (hasTickets) {
|
||||
// 有待处理工单 — 红色
|
||||
const total =
|
||||
ticketCounts.value.pending +
|
||||
ticketCounts.value.stayclose +
|
||||
ticketCounts.value.confirm +
|
||||
ticketCounts.value.workOrderCount;
|
||||
color = [244, 67, 54];
|
||||
tooltip = `${appName} - 有 ${total} 条工单待处理!`;
|
||||
} else {
|
||||
// 监测中,无异常 — 绿色
|
||||
color = [76, 175, 80];
|
||||
tooltip = `${appName} - 监测中`;
|
||||
}
|
||||
|
||||
const icon = await Image.new(await makeTrayIconRGBA(...color), 32, 32);
|
||||
await trayIconInstance.setIcon(icon);
|
||||
await trayIconInstance.setTooltip(tooltip);
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `更新托盘图标失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建托盘右键菜单,包含以下菜单项:
|
||||
* - 显示面板:还原并聚焦主窗口
|
||||
* - 开始/停止监测:根据当前状态切换
|
||||
* - 打开工单网站:在默认浏览器中打开
|
||||
* - 退出:销毁主窗口
|
||||
*/
|
||||
async function buildTrayMenu() {
|
||||
const mainWindow = getCurrentWindow();
|
||||
return await Menu.new({
|
||||
items: [
|
||||
await MenuItem.new({
|
||||
id: "show",
|
||||
text: "显示面板",
|
||||
action: async () => {
|
||||
await mainWindow.unminimize();
|
||||
await mainWindow.show();
|
||||
await mainWindow.setFocus();
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "toggle_monitor",
|
||||
text: isMonitoring.value ? "停止监测" : "开始监测",
|
||||
action: async () => {
|
||||
if (isMonitoring.value) {
|
||||
stopMonitoring();
|
||||
} else {
|
||||
// 开始监测前先还原窗口,确保登录流程可见
|
||||
await mainWindow.unminimize();
|
||||
await mainWindow.show();
|
||||
await mainWindow.setFocus();
|
||||
await startMonitoring();
|
||||
}
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "open_website",
|
||||
text: "打开工单网站",
|
||||
action: async () => {
|
||||
await openUrl(CRM_URL);
|
||||
},
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "quit",
|
||||
text: "退出",
|
||||
action: async () => {
|
||||
await mainWindow.destroy();
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新构建并替换托盘右键菜单。
|
||||
* 在监测状态变化后调用,以同步"开始/停止监测"等动态菜单文字。
|
||||
*/
|
||||
async function updateTrayMenu() {
|
||||
if (!trayIconInstance) return;
|
||||
try {
|
||||
const menu = await buildTrayMenu();
|
||||
await trayIconInstance.setMenu(menu);
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `更新托盘菜单失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统托盘:创建图标(初始灰色)、注册右键菜单、绑定左键单击事件。
|
||||
* 应在应用启动时调用一次。
|
||||
*/
|
||||
async function setupTray() {
|
||||
try {
|
||||
const menu = await buildTrayMenu();
|
||||
|
||||
// 初始状态:灰色(未开始)
|
||||
const icon = await Image.new(
|
||||
await makeTrayIconRGBA(158, 158, 158),
|
||||
32,
|
||||
32,
|
||||
);
|
||||
|
||||
trayIconInstance = await TrayIcon.new({
|
||||
icon,
|
||||
tooltip: `${appName} - 未开始`,
|
||||
menu,
|
||||
menuOnLeftClick: false,
|
||||
action: async (event) => {
|
||||
// 左键单击:还原并聚焦主窗口
|
||||
if (event.type === "Click" && event.button === "Left") {
|
||||
const win = getCurrentWindow();
|
||||
await win.unminimize();
|
||||
await win.show();
|
||||
await win.setFocus();
|
||||
}
|
||||
},
|
||||
});
|
||||
addLog("INFO", "系统托盘创建成功", "SYSTEM");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
/** 隐藏主窗口(最小化到系统托盘)。 */
|
||||
async function minimizeToTray() {
|
||||
await getCurrentWindow().hide();
|
||||
}
|
||||
|
||||
return { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray };
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { ref } from "vue";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
|
||||
// ===== 模块级单例状态 =====
|
||||
/** 是否显示更新对话框 */
|
||||
const showUpdateDialog = ref(false);
|
||||
/** 新版本信息 */
|
||||
const updateInfo = ref<{
|
||||
version: string;
|
||||
notes: string;
|
||||
date: string;
|
||||
} | null>(null);
|
||||
/** 下载进度 */
|
||||
const updateProgress = ref<{ total: number; downloaded: number } | null>(null);
|
||||
/** 是否正在更新中 */
|
||||
const isUpdating = ref(false);
|
||||
/** 当前应用版本号 */
|
||||
const currentVersion = ref("");
|
||||
/** 是否正在检查更新 */
|
||||
const isCheckingUpdates = ref(false);
|
||||
|
||||
/** 待安装的更新对象 */
|
||||
let pendingUpdate: Update | null = null;
|
||||
|
||||
/**
|
||||
* 版本更新 Composable(单例模式,模块级状态共享)
|
||||
*/
|
||||
export function useUpdater() {
|
||||
const { addLog } = useLogStore();
|
||||
|
||||
/** 检查是否有可用的应用更新,发现新版本时展示更新对话框 */
|
||||
async function checkForUpdates() {
|
||||
if (isCheckingUpdates.value || isUpdating.value) return;
|
||||
isCheckingUpdates.value = true;
|
||||
try {
|
||||
const update = await check();
|
||||
if (update?.available) {
|
||||
pendingUpdate = update;
|
||||
updateInfo.value = {
|
||||
version: update.version,
|
||||
notes: update.body ?? "",
|
||||
date: update.date ?? "",
|
||||
};
|
||||
showUpdateDialog.value = true;
|
||||
addLog("INFO", `发现新版本: v${update.version}`, "UPDATER");
|
||||
} else {
|
||||
addLog("INFO", "当前已是最新版本", "UPDATER");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("WARN", `检查更新失败: ${e.message || e}`, "UPDATER");
|
||||
} finally {
|
||||
isCheckingUpdates.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 确认并执行更新:下载安装后重启应用 */
|
||||
async function confirmUpdate() {
|
||||
if (!pendingUpdate) return;
|
||||
isUpdating.value = true;
|
||||
updateProgress.value = null;
|
||||
try {
|
||||
await pendingUpdate.downloadAndInstall((event) => {
|
||||
if (event.event === "Started") {
|
||||
updateProgress.value = {
|
||||
total: event.data.contentLength ?? 0,
|
||||
downloaded: 0,
|
||||
};
|
||||
} else if (event.event === "Progress") {
|
||||
if (updateProgress.value) {
|
||||
updateProgress.value.downloaded += event.data.chunkLength;
|
||||
}
|
||||
} else if (event.event === "Finished") {
|
||||
addLog("INFO", "更新下载完成,准备重启", "UPDATER");
|
||||
}
|
||||
});
|
||||
await relaunch();
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `更新安装失败: ${e.message || e}`, "UPDATER");
|
||||
isUpdating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭更新对话框(稍后提醒) */
|
||||
function dismissUpdate() {
|
||||
showUpdateDialog.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
showUpdateDialog,
|
||||
updateInfo,
|
||||
updateProgress,
|
||||
isUpdating,
|
||||
currentVersion,
|
||||
isCheckingUpdates,
|
||||
checkForUpdates,
|
||||
confirmUpdate,
|
||||
dismissUpdate,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/** CRM API 根地址 */
|
||||
export const API_BASE = "https://crm.yunvip123.com/api";
|
||||
|
||||
/** 用户登录接口 */
|
||||
export const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
|
||||
|
||||
/** 工单状态计数查询接口 */
|
||||
export const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
|
||||
|
||||
/** 工单列表分页查询接口 */
|
||||
export const WORK_ORDER_URL = `${API_BASE}/DemandManage/GetWorkOrderListPage`;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/** 应用名称,来自环境变量 VITE_APP_NAME */
|
||||
export const APP_NAME = import.meta.env.VITE_APP_NAME;
|
||||
|
||||
/** 工单网站地址 */
|
||||
export const CRM_URL = "https://crm.yunvip123.com";
|
||||
14
src/main.ts
14
src/main.ts
|
|
@ -1,7 +1,7 @@
|
|||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import Antd from "ant-design-vue";
|
||||
import "ant-design-vue/dist/reset.css";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).use(createPinia()).use(Antd).mount("#app");
|
||||
import Antd from "ant-design-vue";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp } from "vue";
|
||||
import "ant-design-vue/dist/reset.css";
|
||||
import App from "@/App.vue";
|
||||
|
||||
createApp(App).use(createPinia()).use(Antd).mount("#app");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
enable as enableAutostart,
|
||||
disable as disableAutostart,
|
||||
} from "@tauri-apps/plugin-autostart";
|
||||
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
const message = ref(""); // 界面状态提示消息
|
||||
const isLoading = ref(false); // 是否处于加载/请求中
|
||||
const autoStartEnabled = ref(false); // 系统自启动是否已启用
|
||||
const config = ref({
|
||||
check_interval: 60, // 轮询间隔(秒)
|
||||
auto_start: false, // 开机自启
|
||||
auto_monitor: false, // 启动后自动开始监测
|
||||
silent_start: false, // 静默启动(最小化到托盘)
|
||||
show_notifications: true, // 是否显示系统通知
|
||||
notification_sound: true, // 通知是否播放声音
|
||||
network_timeout: 30, // 网络请求超时时间(秒)
|
||||
close_action: "ask" as "ask" | "minimize" | "close", // 点击关闭按钮的行为
|
||||
handle_pause_minutes: 20, // 点击"去处理"后的暂停时长(分钟)
|
||||
});
|
||||
const showCloseDialog = ref(false); // 是否显示关闭确认对话框
|
||||
|
||||
const { addLog } = useLogStore();
|
||||
|
||||
/** 清空状态提示消息 */
|
||||
function clearMessage() {
|
||||
message.value = "";
|
||||
}
|
||||
|
||||
/** 设置状态提示消息 */
|
||||
function setMessage(msg: string) {
|
||||
message.value = msg;
|
||||
}
|
||||
|
||||
/** 从 localStorage 加载已保存的运行配置,与默认值合并 */
|
||||
function loadConfig() {
|
||||
try {
|
||||
const saved = localStorage.getItem("crm_config");
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
config.value = { ...config.value, ...data };
|
||||
addLog("INFO", "已加载保存的配置", "CONFIG");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `加载配置失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
}
|
||||
|
||||
/** 将当前配置序列化写入 localStorage */
|
||||
function saveConfigToStorage() {
|
||||
try {
|
||||
localStorage.setItem("crm_config", JSON.stringify(config.value));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用当前配置:同步自启动设置并持久化 */
|
||||
async function updateConfig() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
if (config.value.auto_start) await enableAutostart();
|
||||
else await disableAutostart();
|
||||
autoStartEnabled.value = config.value.auto_start;
|
||||
addLog(
|
||||
"INFO",
|
||||
`自启动设置: ${config.value.auto_start ? "启用" : "禁用"}`,
|
||||
"CONFIG",
|
||||
);
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `自启动设置失败: ${e.message || e}`, "CONFIG");
|
||||
}
|
||||
saveConfigToStorage();
|
||||
setMessage("配置更新成功");
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
/** 处理用户关闭对话框的确认操作,可选记住选择;minimize 隐藏窗口,close 销毁窗口 */
|
||||
async function handleCloseAction(
|
||||
action: "minimize" | "close",
|
||||
remember: boolean,
|
||||
) {
|
||||
showCloseDialog.value = false;
|
||||
if (remember) {
|
||||
config.value.close_action = action;
|
||||
saveConfigToStorage();
|
||||
addLog(
|
||||
"INFO",
|
||||
`关闭行为已设为: ${action === "minimize" ? "最小化到托盘" : "直接退出"}`,
|
||||
"SYSTEM",
|
||||
);
|
||||
}
|
||||
if (action === "minimize") {
|
||||
await getCurrentWindow().hide();
|
||||
} else {
|
||||
await getCurrentWindow().destroy();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
isLoading,
|
||||
autoStartEnabled,
|
||||
config,
|
||||
showCloseDialog,
|
||||
clearMessage,
|
||||
setMessage,
|
||||
loadConfig,
|
||||
saveConfigToStorage,
|
||||
updateConfig,
|
||||
handleCloseAction,
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot));
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { formatTime } from "@/utils/common";
|
||||
|
||||
/** 单条日志记录 */
|
||||
export interface LogEntry {
|
||||
/** 日志记录时间,格式:YYYY-MM-DD HH:mm:ss */
|
||||
timestamp: string;
|
||||
/** 日志级别,如 INFO / WARN / ERROR / DEBUG */
|
||||
level: string;
|
||||
/** 日志正文 */
|
||||
message: string;
|
||||
/** 日志来源分类,如 LOGIN / SCHEDULER / SYSTEM */
|
||||
category: string;
|
||||
}
|
||||
|
||||
export const useLogStore = defineStore("log", () => {
|
||||
/** 日志列表,最多保留 1000 条 */
|
||||
const logs = ref<LogEntry[]>([]);
|
||||
|
||||
/** 追加一条日志,超过 1000 条时自动丢弃最旧一条 */
|
||||
function addLog(level: string, msg: string, category: string) {
|
||||
logs.value.push({
|
||||
timestamp: formatTime(new Date()),
|
||||
level,
|
||||
message: msg,
|
||||
category,
|
||||
});
|
||||
if (logs.value.length > 1000) logs.value.shift();
|
||||
}
|
||||
|
||||
/** 清空所有日志 */
|
||||
function clearLogs() {
|
||||
logs.value = [];
|
||||
}
|
||||
|
||||
/** 将当前日志导出为 .txt 文件并触发浏览器下载,返回操作结果消息 */
|
||||
function exportLogs(): string {
|
||||
if (logs.value.length === 0) {
|
||||
return "暂无日志可导出";
|
||||
}
|
||||
const lines = logs.value.map(
|
||||
(l) => `[${l.timestamp}] [${l.level}] [${l.category}] ${l.message}`,
|
||||
);
|
||||
const blob = new Blob([lines.join("\n")], {
|
||||
type: "text/plain;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `monitor-logs-${formatTime(new Date()).replaceAll(/[: ]/g, "-")}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
addLog("INFO", `已导出 ${logs.value.length} 条日志`, "SYSTEM");
|
||||
return "日志导出成功";
|
||||
}
|
||||
|
||||
return { logs, addLog, clearLogs, exportLogs };
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,88 @@
|
|||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { LOGIN_URL } from "@/constants/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useLogStore } from "@/stores/log";
|
||||
import { formatTime } from "@/utils/common";
|
||||
|
||||
/**
|
||||
* 网络状态 store。
|
||||
* 提供公网连通性检测与 CRM API 可达性检测,结果以响应式状态共享给所有组件。
|
||||
*/
|
||||
export const useNetworkStore = defineStore("network", () => {
|
||||
const appStore = useAppStore();
|
||||
const { addLog } = useLogStore();
|
||||
|
||||
/** 公网是否连通 */
|
||||
const isConnected = ref(false);
|
||||
/** CRM API 是否可达 */
|
||||
const apiReachable = ref(false);
|
||||
/** API 最近一次响应时间(毫秒),未检测时为 null */
|
||||
const responseTime = ref<number | null>(null);
|
||||
/** 最近一次检查的时间字符串 */
|
||||
const lastCheck = ref("");
|
||||
|
||||
/**
|
||||
* 依次检测公网连通性和 API 可达性,并记录响应时间。
|
||||
* 步骤:
|
||||
* 1. GET https://www.baidu.com 判断公网是否可用
|
||||
* 2. 公网可用时,HEAD LOGIN_URL 判断 API 是否可达并测量响应时间
|
||||
* 结果更新到 store 状态,同时写入日志。
|
||||
*/
|
||||
async function checkNetworkStatus() {
|
||||
appStore.isLoading = true;
|
||||
let connected = false;
|
||||
let reachable = false;
|
||||
let respTime: number | null = null;
|
||||
|
||||
try {
|
||||
const resp = await fetch("https://www.baidu.com", {
|
||||
method: "GET",
|
||||
connectTimeout: appStore.config.network_timeout * 1000,
|
||||
});
|
||||
connected = resp.ok;
|
||||
} catch {
|
||||
/* 网络不通 */
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
try {
|
||||
const apiStart = Date.now();
|
||||
const resp = await fetch(LOGIN_URL, {
|
||||
method: "HEAD",
|
||||
connectTimeout: appStore.config.network_timeout * 1000,
|
||||
});
|
||||
respTime = Date.now() - apiStart;
|
||||
reachable = resp.status < 500;
|
||||
} catch {
|
||||
/* API 不可达 */
|
||||
}
|
||||
}
|
||||
|
||||
isConnected.value = connected;
|
||||
apiReachable.value = reachable;
|
||||
responseTime.value = respTime;
|
||||
lastCheck.value = formatTime(new Date());
|
||||
|
||||
addLog(
|
||||
"INFO",
|
||||
`网络检查完成 - 连接: ${connected}, API可达: ${reachable}`,
|
||||
"NETWORK",
|
||||
);
|
||||
appStore.message = "网络状态检查完成";
|
||||
appStore.isLoading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
apiReachable,
|
||||
responseTime,
|
||||
lastCheck,
|
||||
checkNetworkStatus,
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore, import.meta.hot));
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import dayjs from "dayjs";
|
||||
|
||||
/** 将 Date 对象格式化为 'YYYY-MM-DD HH:mm:ss' 字符串 */
|
||||
export function formatTime(date: Date): string {
|
||||
return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { load, type Store } from "@tauri-apps/plugin-store";
|
||||
|
||||
let storeInstance: Store | null = null;
|
||||
|
||||
async function getStore(): Promise<Store> {
|
||||
if (!storeInstance) {
|
||||
storeInstance = await load("config.json", { defaults: {}, autoSave: true });
|
||||
}
|
||||
return storeInstance;
|
||||
}
|
||||
|
||||
export async function getItem<T = unknown>(key: string): Promise<T | null> {
|
||||
const store = await getStore();
|
||||
const value = await store.get<T>(key);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
export async function setItem<T = unknown>(
|
||||
key: string,
|
||||
value: T,
|
||||
): Promise<void> {
|
||||
const store = await getStore();
|
||||
await store.set(key, value);
|
||||
}
|
||||
|
||||
export async function removeItem(key: string): Promise<void> {
|
||||
const store = await getStore();
|
||||
await store.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 localStorage 迁移数据到文件存储(仅首次升级时执行)
|
||||
*/
|
||||
export async function migrateFromLocalStorage(): Promise<boolean> {
|
||||
const store = await getStore();
|
||||
const migrated = await store.get<boolean>("_migrated");
|
||||
if (migrated) return false;
|
||||
|
||||
let hasMigrated = false;
|
||||
for (const key of ["crm_credentials", "crm_config"]) {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
try {
|
||||
await store.set(key, JSON.parse(raw));
|
||||
localStorage.removeItem(key);
|
||||
hasMigrated = true;
|
||||
} catch {
|
||||
/* ignore invalid JSON */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await store.set("_migrated", true);
|
||||
return hasMigrated;
|
||||
}
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_NAME: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
117
vite.config.ts
117
vite.config.ts
|
|
@ -1,56 +1,61 @@
|
|||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [vue()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// Vue 核心库
|
||||
'vue-vendor': ['vue'],
|
||||
// Ant Design Vue 主库
|
||||
'antd-vendor': ['ant-design-vue'],
|
||||
// Ant Design 图标库(体积较大)
|
||||
'antd-icons': ['@ant-design/icons-vue'],
|
||||
// Tauri API 相关
|
||||
'tauri-vendor': [
|
||||
'@tauri-apps/api',
|
||||
'@tauri-apps/plugin-autostart',
|
||||
'@tauri-apps/plugin-http',
|
||||
'@tauri-apps/plugin-notification',
|
||||
'@tauri-apps/plugin-opener',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 提高 chunk 大小警告阈值到 10MB
|
||||
chunkSizeWarningLimit: 1024 * 10,
|
||||
},
|
||||
}));
|
||||
import path from "node:path";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// Vue 核心库
|
||||
"vue-vendor": ["vue"],
|
||||
// Ant Design Vue 主库
|
||||
"antd-vendor": ["ant-design-vue"],
|
||||
// Ant Design 图标库(体积较大)
|
||||
"antd-icons": ["@ant-design/icons-vue"],
|
||||
// Tauri API 相关
|
||||
"tauri-vendor": [
|
||||
"@tauri-apps/api",
|
||||
"@tauri-apps/plugin-autostart",
|
||||
"@tauri-apps/plugin-http",
|
||||
"@tauri-apps/plugin-notification",
|
||||
"@tauri-apps/plugin-opener",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 提高 chunk 大小警告阈值到 10MB
|
||||
chunkSizeWarningLimit: 1024 * 10,
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue