Initial commit
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
# `call-client`
|
||||||
|
|
||||||
|
`call-client` 是一个基于 `Tauri 2 + Vue 3 + TypeScript + Vite` 的桌面客户端项目,已从原 `Electron + Vue` 项目迁移出主要业务骨架。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
- 已完成 `login -> main -> ticketList` 三窗口流程
|
||||||
|
- 已接入配置、日志、Session、窗口事件通信
|
||||||
|
- 已接入主业务页面迁移骨架
|
||||||
|
- 已补齐 `src-tauri/icons/` 图标资源
|
||||||
|
- 已开启 Tauri Linux 打包目标:`deb`、`appimage`
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm run tauri dev
|
||||||
|
npm run tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `npm run dev`:启动前端开发服务
|
||||||
|
- `npm run build`:执行前端类型检查和构建
|
||||||
|
- `npm run tauri dev`:启动 Tauri 开发模式
|
||||||
|
- `npm run tauri build`:执行 Tauri 打包
|
||||||
|
|
||||||
|
## 文档入口
|
||||||
|
|
||||||
|
- 项目结构、关键文件、运行方式、打包说明:`PROJECT_GUIDE.md`
|
||||||
|
- 迁移设计文档:`TAURI-MIGRATION.md`
|
||||||
|
- 当前迁移任务清单:`TAURI-MIGRATION-TASKS.md`
|
||||||
|
|
||||||
|
## 运行前提
|
||||||
|
|
||||||
|
- Node.js / npm 已安装
|
||||||
|
- 运行 `tauri dev` 或 `tauri build` 前,需要本机已安装 Rust / Cargo
|
||||||
|
- Linux 打包是否成功还取决于本机 Tauri 打包依赖是否完整
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
# `call-client` Tauri 迁移任务清单
|
||||||
|
|
||||||
|
## 已完成
|
||||||
|
|
||||||
|
- 建立 `vue-router` 路由骨架,使用 `hash` 路由承载登录页、主叫号页、票号列表页
|
||||||
|
- 新增前端适配层:
|
||||||
|
- `src/host/session.ts`
|
||||||
|
- `src/host/config.ts`
|
||||||
|
- `src/host/logger.ts`
|
||||||
|
- `src/host/window.ts`
|
||||||
|
- `src/host/dialog.ts`
|
||||||
|
- `src/host/events.ts`
|
||||||
|
- 新增 Rust 宿主命令:
|
||||||
|
- `session_get`
|
||||||
|
- `session_set`
|
||||||
|
- `session_clear`
|
||||||
|
- `config_get_all`
|
||||||
|
- `config_merge`
|
||||||
|
- `app_log`
|
||||||
|
- `emit_to_window`
|
||||||
|
- `list_windows`
|
||||||
|
- 新增窗口生命周期命令:
|
||||||
|
- `open_main_window`
|
||||||
|
- `open_login_window`
|
||||||
|
- `open_ticket_window`
|
||||||
|
- `focus_window`
|
||||||
|
- `quit_app`
|
||||||
|
- 配置文件持久化已按文档要求保留为 `config.json`
|
||||||
|
- 日志已迁移到 Rust 侧,包含基础轮转和过期清理
|
||||||
|
- 已恢复 `login -> main -> ticketList` 三窗口切换
|
||||||
|
- 主界面“更多”菜单与暂停原因菜单已迁移为前端可用交互
|
||||||
|
- `tauri.conf.json` 已切换为 `npm` 构建命令并补充 Linux `deb` / `AppImage` 目标
|
||||||
|
- `src-tauri/icons/*` 图标资源已补齐,`bundle.active` 已开启
|
||||||
|
- 新增 `dialog` 插件接入
|
||||||
|
|
||||||
|
## 待接入原业务源码
|
||||||
|
|
||||||
|
- Tauri 真机联调三窗口行为
|
||||||
|
- Rust 端实际编译与运行验证
|
||||||
|
- 按最终需求决定是否进一步恢复原生系统菜单
|
||||||
|
- Linux 真机打包验证
|
||||||
|
|
||||||
|
## 验收结果
|
||||||
|
|
||||||
|
- `npm run build` 已通过
|
||||||
|
- Rust 代码未能本机编译验证,因为当前环境缺少 `cargo`
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + Vue + Typescript App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "call-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.2",
|
||||||
|
"axios": "^1.14.0",
|
||||||
|
"element-plus": "^2.13.6",
|
||||||
|
"sass": "^1.98.0",
|
||||||
|
"unplugin-auto-import": "^21.0.0",
|
||||||
|
"unplugin-vue-components": "^32.0.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.3",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "call-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "call_client_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tauri-plugin-store = "2.4.2"
|
||||||
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main", "login", "ticketList"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"dialog:default",
|
||||||
|
"opener:default",
|
||||||
|
"store:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,113 @@
|
|||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
env,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
|
const APP_NAME: &str = "call-client";
|
||||||
|
const CONFIG_FILE_NAME: &str = "config.json";
|
||||||
|
|
||||||
|
fn get_home_dir() -> PathBuf {
|
||||||
|
if let Ok(value) = env::var("HOME") {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("USERPROFILE") {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_dir() -> PathBuf {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
if let Ok(value) = env::var("XDG_CONFIG_HOME") {
|
||||||
|
return PathBuf::from(value).join(APP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_home_dir().join(".config").join(APP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("APPDATA") {
|
||||||
|
return PathBuf::from(value).join(APP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_home_dir().join(".").join(APP_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_file_path() -> PathBuf {
|
||||||
|
get_config_dir().join(CONFIG_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_parent_dir(path: &Path) -> Result<(), String> {
|
||||||
|
let Some(parent) = path.parent() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(parent).map_err(|error| format!("创建配置目录失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_config() -> Result<Value, String> {
|
||||||
|
let path = get_config_file_path();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(Value::Object(Map::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&path).map_err(|error| format!("读取配置文件失败: {error}"))?;
|
||||||
|
if content.trim().is_empty() {
|
||||||
|
return Ok(Value::Object(Map::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_str::<Value>(&content).map_err(|error| format!("解析配置文件失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config(value: &Value) -> Result<(), String> {
|
||||||
|
let path = get_config_file_path();
|
||||||
|
ensure_parent_dir(&path)?;
|
||||||
|
let content = serde_json::to_string_pretty(value).map_err(|error| format!("序列化配置失败: {error}"))?;
|
||||||
|
fs::write(path, content).map_err(|error| format!("写入配置文件失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_value(target: &mut Value, patch: Value) {
|
||||||
|
match (target, patch) {
|
||||||
|
(Value::Object(target_map), Value::Object(patch_map)) => {
|
||||||
|
for (key, value) in patch_map {
|
||||||
|
if let Some(existing) = target_map.get_mut(&key) {
|
||||||
|
merge_value(existing, value);
|
||||||
|
} else {
|
||||||
|
target_map.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(target_value, patch_value) => {
|
||||||
|
*target_value = patch_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn config_get_all() -> Result<BTreeMap<String, Value>, String> {
|
||||||
|
let value = read_config()?;
|
||||||
|
let Value::Object(map) = value else {
|
||||||
|
return Ok(BTreeMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(map.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn config_merge(partial: Value) -> Result<BTreeMap<String, Value>, String> {
|
||||||
|
let mut current = read_config()?;
|
||||||
|
merge_value(&mut current, partial);
|
||||||
|
write_config(¤t)?;
|
||||||
|
|
||||||
|
let Value::Object(map) = current else {
|
||||||
|
return Ok(BTreeMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(map.into_iter().collect())
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn emit_to_window(
|
||||||
|
app: AppHandle,
|
||||||
|
label: String,
|
||||||
|
event: String,
|
||||||
|
payload: Value,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
app.emit_to(label, event.as_str(), payload)
|
||||||
|
.map_err(|error| format!("发送窗口事件失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_windows(app: AppHandle) -> Vec<String> {
|
||||||
|
app.webview_windows().keys().cloned().collect()
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::{self, OpenOptions},
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
const APP_NAME: &str = "call-client";
|
||||||
|
const LOG_FILE_NAME: &str = "app.log";
|
||||||
|
const MAX_LOG_SIZE_BYTES: u64 = 100 * 1024 * 1024;
|
||||||
|
const LOG_RETENTION_DAYS: u64 = 7;
|
||||||
|
|
||||||
|
fn get_home_dir() -> PathBuf {
|
||||||
|
if let Ok(value) = env::var("HOME") {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("USERPROFILE") {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_state_dir() -> PathBuf {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
if let Ok(value) = env::var("XDG_STATE_HOME") {
|
||||||
|
return PathBuf::from(value).join(APP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_home_dir().join(".local").join("state").join(APP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("LOCALAPPDATA") {
|
||||||
|
return PathBuf::from(value).join(APP_NAME).join("state");
|
||||||
|
}
|
||||||
|
|
||||||
|
get_home_dir().join(".").join(APP_NAME).join("state")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_parent_dir(path: &Path) -> Result<(), String> {
|
||||||
|
let Some(parent) = path.parent() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(parent).map_err(|error| format!("创建日志目录失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_old_logs(dir: &Path) -> Result<(), String> {
|
||||||
|
if !dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let retention = Duration::from_secs(LOG_RETENTION_DAYS * 24 * 60 * 60);
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let entries = fs::read_dir(dir).map_err(|error| format!("读取日志目录失败: {error}"))?;
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.map_err(|error| format!("遍历日志目录失败: {error}"))?;
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = entry
|
||||||
|
.metadata()
|
||||||
|
.map_err(|error| format!("读取日志元数据失败: {error}"))?;
|
||||||
|
|
||||||
|
let Ok(modified_at) = metadata.modified() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if now.duration_since(modified_at).unwrap_or_default() > retention {
|
||||||
|
fs::remove_file(&path).map_err(|error| format!("清理过期日志失败: {error}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_log_if_needed(path: &Path) -> Result<(), String> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = fs::metadata(path).map_err(|error| format!("读取日志文件失败: {error}"))?;
|
||||||
|
if metadata.len() < MAX_LOG_SIZE_BYTES {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|error| format!("生成日志时间戳失败: {error}"))?
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let rotated_path = path.with_file_name(format!("app-{timestamp}.log"));
|
||||||
|
fs::rename(path, rotated_path).map_err(|error| format!("轮转日志失败: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn app_log(level: String, message: String) -> Result<(), String> {
|
||||||
|
let dir = get_state_dir();
|
||||||
|
let path = dir.join(LOG_FILE_NAME);
|
||||||
|
ensure_parent_dir(&path)?;
|
||||||
|
cleanup_old_logs(&dir)?;
|
||||||
|
rotate_log_if_needed(&path)?;
|
||||||
|
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|error| format!("生成日志时间失败: {error}"))?
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
.map_err(|error| format!("打开日志文件失败: {error}"))?;
|
||||||
|
|
||||||
|
writeln!(file, "[{timestamp}] [{}] {message}", level.to_uppercase())
|
||||||
|
.map_err(|error| format!("写入日志失败: {error}"))
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod events;
|
||||||
|
pub mod logger;
|
||||||
|
pub mod session;
|
||||||
|
pub mod window;
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::state::{AppState, SessionState};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn session_get(state: State<'_, AppState>) -> Result<SessionState, String> {
|
||||||
|
let session = state
|
||||||
|
.session
|
||||||
|
.lock()
|
||||||
|
.map_err(|error| format!("读取会话锁失败: {error}"))?;
|
||||||
|
|
||||||
|
Ok(session.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn session_set(
|
||||||
|
payload: SessionState,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<SessionState, String> {
|
||||||
|
let mut session = state
|
||||||
|
.session
|
||||||
|
.lock()
|
||||||
|
.map_err(|error| format!("写入会话锁失败: {error}"))?;
|
||||||
|
|
||||||
|
*session = payload.clone();
|
||||||
|
Ok(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn session_clear(state: State<'_, AppState>) -> Result<SessionState, String> {
|
||||||
|
let mut session = state
|
||||||
|
.session
|
||||||
|
.lock()
|
||||||
|
.map_err(|error| format!("清空会话锁失败: {error}"))?;
|
||||||
|
|
||||||
|
*session = SessionState::default();
|
||||||
|
Ok(session.clone())
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
mod commands;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
use commands::{
|
||||||
|
config::{config_get_all, config_merge},
|
||||||
|
events::{emit_to_window, list_windows},
|
||||||
|
logger::app_log,
|
||||||
|
session::{session_clear, session_get, session_set},
|
||||||
|
window::{
|
||||||
|
close_ticket_window, ensure_main_window, focus_window, open_login_window, open_main_window,
|
||||||
|
open_ticket_window, quit_app,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use state::AppState;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
|
.manage(AppState::default())
|
||||||
|
.setup(|app| {
|
||||||
|
ensure_main_window(app.handle())?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
session_get,
|
||||||
|
session_set,
|
||||||
|
session_clear,
|
||||||
|
config_get_all,
|
||||||
|
config_merge,
|
||||||
|
app_log,
|
||||||
|
emit_to_window,
|
||||||
|
list_windows,
|
||||||
|
open_ticket_window,
|
||||||
|
close_ticket_window,
|
||||||
|
focus_window,
|
||||||
|
open_main_window,
|
||||||
|
open_login_window,
|
||||||
|
quit_app
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
call_client_lib::run()
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SessionState {
|
||||||
|
pub emp_uid: Option<i64>,
|
||||||
|
pub win_uid: Option<i64>,
|
||||||
|
pub queue_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub session: Mutex<SessionState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
session: Mutex::new(SessionState::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "call-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.ziyun.callclient",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "login",
|
||||||
|
"title": "登录",
|
||||||
|
"url": "/#/login",
|
||||||
|
"width": 480,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": false,
|
||||||
|
"maximizable": false,
|
||||||
|
"decorations": false,
|
||||||
|
"alwaysOnTop": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "ticketList",
|
||||||
|
"title": "票号列表",
|
||||||
|
"url": "/#/ticketList",
|
||||||
|
"width": 1024,
|
||||||
|
"height": 720,
|
||||||
|
"visible": false,
|
||||||
|
"resizable": false,
|
||||||
|
"maximizable": false,
|
||||||
|
"decorations": false,
|
||||||
|
"alwaysOnTop": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["deb", "appimage"],
|
||||||
|
"linux": {
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"appimage": {
|
||||||
|
"bundleMediaFramework": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import type { CallRequest, CallResponse, PauseRequest, ReCallRequest } from "../types/action";
|
||||||
|
import type {
|
||||||
|
IsRankData,
|
||||||
|
IsRankRequest,
|
||||||
|
QueueCountData,
|
||||||
|
QueueCountRequest,
|
||||||
|
} from "../types/rank";
|
||||||
|
import type { TicketPoolRequest, TicketPoolResponse } from "../types/ticket";
|
||||||
|
import type { UserRequest, UserResponse } from "../types/user";
|
||||||
|
import type { WindowResponse } from "../types/window";
|
||||||
|
import { http } from "../utils/service";
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
user: {
|
||||||
|
login: (data: UserRequest) => http.post<UserResponse>("/auth/login", data),
|
||||||
|
},
|
||||||
|
|
||||||
|
window: {
|
||||||
|
list: () => http.get<WindowResponse>("/windows/list"),
|
||||||
|
},
|
||||||
|
|
||||||
|
action: {
|
||||||
|
call: (data: CallRequest) => http.post<CallResponse>("/call-terminal/call", data),
|
||||||
|
init: (data: CallRequest) => http.post<CallResponse>("/call-terminal/init", data),
|
||||||
|
recall: (data: ReCallRequest) => http.post<CallResponse>("/call-terminal/recall", data),
|
||||||
|
abandon: (data: ReCallRequest) => http.post<CallResponse>("/call-terminal/abandon", data),
|
||||||
|
pause: (data: PauseRequest) => http.post<CallResponse>("/call-terminal/pause", data),
|
||||||
|
resume: (data: CallRequest) => http.post<CallResponse>("/call-terminal/resume", data),
|
||||||
|
start: (data: ReCallRequest) => http.post<CallResponse>("/call-terminal/start", data),
|
||||||
|
complete: (data: ReCallRequest) => http.post<CallResponse>("/call-terminal/complete", data),
|
||||||
|
evaluate: (data: ReCallRequest) => http.post<CallResponse>("/call-terminal/evaluate", data),
|
||||||
|
pool: (params: TicketPoolRequest) => http.get<TicketPoolResponse>("/call-terminal/pool", params),
|
||||||
|
isRank: (params: IsRankRequest) => http.get<IsRankData>("/call-terminal/is-rank", params),
|
||||||
|
getQueueCount: (params: QueueCountRequest) =>
|
||||||
|
http.get<QueueCountData>("/call-terminal/queue-count", params),
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
:root {
|
||||||
|
--ev-c-white: #ffffff;
|
||||||
|
--ev-c-white-soft: #f8f8f8;
|
||||||
|
--ev-c-white-mute: #f2f2f2;
|
||||||
|
--ev-c-black: #1b1b1f;
|
||||||
|
--ev-c-black-soft: #222222;
|
||||||
|
--ev-c-black-mute: #282828;
|
||||||
|
--ev-c-gray-1: #515c67;
|
||||||
|
--ev-c-gray-2: #414853;
|
||||||
|
--ev-c-gray-3: #32363f;
|
||||||
|
--ev-c-text-1: rgba(255, 255, 245, 0.86);
|
||||||
|
--ev-c-text-2: rgba(235, 235, 245, 0.6);
|
||||||
|
--ev-c-text-3: rgba(235, 235, 245, 0.38);
|
||||||
|
--ev-button-alt-border: transparent;
|
||||||
|
--ev-button-alt-text: var(--ev-c-text-1);
|
||||||
|
--ev-button-alt-bg: var(--ev-c-gray-3);
|
||||||
|
--ev-button-alt-hover-border: transparent;
|
||||||
|
--ev-button-alt-hover-text: var(--ev-c-text-1);
|
||||||
|
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
|
||||||
|
--color-background: var(--ev-c-black);
|
||||||
|
--color-background-soft: var(--ev-c-black-soft);
|
||||||
|
--color-background-mute: var(--ev-c-black-mute);
|
||||||
|
--color-text: var(--ev-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Fira Sans",
|
||||||
|
"Droid Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
@import "./base.css";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background-image: url("./wavy-lines.svg");
|
||||||
|
background-size: cover;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="1600" height="1000" viewBox="0 0 1600 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="1600" height="1000" fill="#0F172A"/>
|
||||||
|
<path d="M0 145C140 205 300 216 442 176C600 132 742 36 901 24C1086 10 1261 115 1444 145C1498 154 1549 157 1600 155V1000H0V145Z" fill="#1E3A8A" fill-opacity="0.35"/>
|
||||||
|
<path d="M0 295C138 334 298 320 432 269C597 205 754 76 929 84C1102 92 1240 240 1406 281C1471 297 1535 305 1600 305V1000H0V295Z" fill="#2563EB" fill-opacity="0.28"/>
|
||||||
|
<path d="M0 455C132 497 267 492 400 448C587 386 767 250 960 272C1137 292 1274 455 1454 487C1503 495 1552 497 1600 494V1000H0V455Z" fill="#38BDF8" fill-opacity="0.18"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 667 B |
@ -0,0 +1,24 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { AppConfig } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取完整应用配置。
|
||||||
|
*/
|
||||||
|
export async function getAllConfig(): Promise<AppConfig> {
|
||||||
|
try {
|
||||||
|
return await invoke<AppConfig>("config_get_all");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`读取配置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并写入应用配置。
|
||||||
|
*/
|
||||||
|
export async function mergeConfig(partial: AppConfig): Promise<AppConfig> {
|
||||||
|
try {
|
||||||
|
return await invoke<AppConfig>("config_merge", { partial });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`写入配置失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||||
|
import type { NativeConfirmOptions } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一封装原生确认框。
|
||||||
|
*/
|
||||||
|
export async function confirmNative(options: NativeConfirmOptions): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await confirm(options.message, {
|
||||||
|
title: options.title,
|
||||||
|
okLabel: options.okLabel,
|
||||||
|
cancelLabel: options.cancelLabel,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`打开确认框失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { emit, listen, type Event } from "@tauri-apps/api/event";
|
||||||
|
import type { TicketActionPayload } from "./types";
|
||||||
|
|
||||||
|
const MAIN_TICKET_EVENT = "main:ticket-action";
|
||||||
|
const TICKET_MAIN_EVENT = "ticket:main-action";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向主叫号页发送票号动作事件。
|
||||||
|
*/
|
||||||
|
export async function emitTicketAction(payload: TicketActionPayload): Promise<void> {
|
||||||
|
try {
|
||||||
|
await emit(MAIN_TICKET_EVENT, payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`发送票号事件失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅来自票号列表页的动作事件。
|
||||||
|
*/
|
||||||
|
export async function listenTicketAction(
|
||||||
|
handler: (event: Event<TicketActionPayload>) => void,
|
||||||
|
): Promise<() => void> {
|
||||||
|
try {
|
||||||
|
return await listen<TicketActionPayload>(MAIN_TICKET_EVENT, handler);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`订阅票号事件失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向主叫号页发送呼叫动作。
|
||||||
|
*/
|
||||||
|
export async function emitCallAction(payload: Omit<TicketActionPayload, "action">): Promise<void> {
|
||||||
|
try {
|
||||||
|
await emit(TICKET_MAIN_EVENT, { action: "call", ...payload });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`发送主窗口事件失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向主叫号页发送评价动作。
|
||||||
|
*/
|
||||||
|
export async function emitEvaluateAction(
|
||||||
|
payload: Omit<TicketActionPayload, "action">,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await emit(TICKET_MAIN_EVENT, { action: "evaluate", ...payload });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`发送主窗口事件失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅来自票号列表页的主叫号动作。
|
||||||
|
*/
|
||||||
|
export async function listenMainAction(
|
||||||
|
handler: (action: "call" | "evaluate", payload?: Omit<TicketActionPayload, "action">) => void,
|
||||||
|
): Promise<() => void> {
|
||||||
|
try {
|
||||||
|
return await listen<TicketActionPayload>(TICKET_MAIN_EVENT, (event) => {
|
||||||
|
handler(event.payload.action, {
|
||||||
|
ticketUid: event.payload.ticketUid,
|
||||||
|
tktNum: event.payload.tktNum,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`订阅主窗口事件失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { LogLevel } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送日志到 Rust 文件日志模块。
|
||||||
|
*/
|
||||||
|
export async function log(level: LogLevel, message: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await invoke("app_log", { level, message });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`写入日志失败: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
export type JsonPrimitive = string | number | boolean | null;
|
||||||
|
|
||||||
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
export interface SessionState {
|
||||||
|
empUid: number | null;
|
||||||
|
winUid: number | null;
|
||||||
|
queueToken: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeConfirmOptions {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
okLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketActionPayload {
|
||||||
|
action: "call" | "evaluate";
|
||||||
|
ticketUid?: number;
|
||||||
|
tktNum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppConfig = Record<string, JsonValue>;
|
||||||
|
|
||||||
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import "./assets/main.css";
|
||||||
|
import { getAllConfig } from "./host/config";
|
||||||
|
import { applyServerIpToHttp } from "./utils/service";
|
||||||
|
import { router } from "./router";
|
||||||
|
|
||||||
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`timeout after ${timeoutMs}ms`));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((value) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(value);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按配置初始化前端运行环境。
|
||||||
|
*/
|
||||||
|
async function bootstrap(): Promise<void> {
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(router).mount("#app");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await withTimeout(getAllConfig(), 1500);
|
||||||
|
const serverIp = typeof config.server_ip === "string" ? config.server_ip.trim() : "";
|
||||||
|
if (serverIp) {
|
||||||
|
applyServerIpToHttp(serverIp);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 配置缺失时保持默认空地址,由路由守卫引导到设置页。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
|
||||||
|
import { getAllConfig } from "../host/config";
|
||||||
|
import type { AppConfig } from "../host/types";
|
||||||
|
import TicketListView from "../views/TicketListView.vue";
|
||||||
|
|
||||||
|
const LoginView = () => import("../views/LoginView.vue");
|
||||||
|
const MainView = () => import("../views/MainView.vue");
|
||||||
|
const ServerSetupView = () => import("../views/ServerSetupView.vue");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从配置中读取服务器地址。
|
||||||
|
*/
|
||||||
|
function getServerIpFromConfig(config: AppConfig): string {
|
||||||
|
const value = config.server_ip;
|
||||||
|
return typeof value === "string" ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{ path: "/", redirect: "/login" },
|
||||||
|
{ path: "/setup", name: "setup", component: ServerSetupView },
|
||||||
|
{ path: "/login", name: "login", component: LoginView },
|
||||||
|
{ path: "/main", name: "main", component: MainView },
|
||||||
|
{ path: "/ticketList", name: "ticketList", component: TicketListView },
|
||||||
|
// 避免在窗口初始 URL/hash 不匹配时出现空白页面
|
||||||
|
{ path: "/:pathMatch(.*)*", redirect: "/ticketList" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beforeEach(async (to, _from, next) => {
|
||||||
|
// 票号列表窗口由主窗口显式打开,优先保证路由可渲染,避免守卫异步阻塞导致白屏。
|
||||||
|
if (to.path === "/ticketList") {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = "";
|
||||||
|
try {
|
||||||
|
const config = await getAllConfig();
|
||||||
|
ip = getServerIpFromConfig(config);
|
||||||
|
} catch (error) {
|
||||||
|
// 如果当前窗口没有权限/IPC 失败,避免导航被中断导致页面空白
|
||||||
|
console.error("[router] getAllConfig failed, skip guard:", error);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ip && to.path !== "/setup") {
|
||||||
|
next({ path: "/setup", replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip && to.path === "/setup") {
|
||||||
|
next({ path: "/login", replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
export type CallStatus = "idle" | "calling" | "paused" | "working" | "evaluating" | "transferring";
|
||||||
|
|
||||||
|
export interface ActionButton {
|
||||||
|
icon: unknown;
|
||||||
|
label: string;
|
||||||
|
action: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallRequest {
|
||||||
|
windowUid: number;
|
||||||
|
empUid: number;
|
||||||
|
ticketUid?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallResponse {
|
||||||
|
action: string;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
windowUid: number;
|
||||||
|
windowName: string;
|
||||||
|
ticketNo: string;
|
||||||
|
ticketStatus: number;
|
||||||
|
ticketStatusText: string;
|
||||||
|
ticketUid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReCallRequest {
|
||||||
|
windowUid: number;
|
||||||
|
empUid: number;
|
||||||
|
ticketUid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PauseRequest {
|
||||||
|
windowUid: number;
|
||||||
|
empUid: number;
|
||||||
|
pauseReason: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
declare module "element-plus/dist/locale/zh-cn.mjs" {
|
||||||
|
import type { Language } from "element-plus/es/locale";
|
||||||
|
const locale: Language;
|
||||||
|
export default locale;
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
export interface IsRankData {
|
||||||
|
hasRank: boolean;
|
||||||
|
ticketUid?: number;
|
||||||
|
isEvaluated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IsRankRequest {
|
||||||
|
ticketUid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueueCountData {
|
||||||
|
queueCount: number;
|
||||||
|
windowUid?: number;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueueCountRequest {
|
||||||
|
windowUid: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
export interface TicketPoolRequest {
|
||||||
|
winUid: number;
|
||||||
|
keyword: string;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketPoolItem {
|
||||||
|
id?: number;
|
||||||
|
ticketUid?: number;
|
||||||
|
tktNum?: string;
|
||||||
|
ticketNo?: string;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
status?: string | number;
|
||||||
|
ticketStatusText?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketPoolResponse {
|
||||||
|
list?: TicketPoolItem[];
|
||||||
|
records?: TicketPoolItem[];
|
||||||
|
items?: TicketPoolItem[];
|
||||||
|
total?: number;
|
||||||
|
count?: number;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
export interface UserRequest {
|
||||||
|
loginMode: string;
|
||||||
|
clientType: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
hallRegNum: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
empUid: number;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
tenantId: string;
|
||||||
|
hallCode: string;
|
||||||
|
hallName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserResponse {
|
||||||
|
queueToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
tokenType: string;
|
||||||
|
expiresIn: string;
|
||||||
|
operatorProfile: UserInfo;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export interface ServiceWindow {
|
||||||
|
windowUid: number;
|
||||||
|
windowCode: string;
|
||||||
|
windowName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WindowResponse {
|
||||||
|
windows: ServiceWindow[];
|
||||||
|
}
|
||||||
@ -0,0 +1,171 @@
|
|||||||
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getSession } from "../host/session";
|
||||||
|
import type { SessionState } from "../host/types";
|
||||||
|
|
||||||
|
export const API_QUEUE_CALLER_PATH = "/api/queue/caller";
|
||||||
|
const DEFAULT_API_PORT = 8845;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据配置中的服务器地址拼出后端 baseURL。
|
||||||
|
*/
|
||||||
|
export function buildBaseUrlFromServerIp(serverIp: string): string {
|
||||||
|
const raw = serverIp.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
||||||
|
return `${raw.replace(/\/$/, "")}${API_QUEUE_CALLER_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostPort = raw.replace(/^\/+/, "");
|
||||||
|
const hasPort = /:\d+$/.test(hostPort) || /^\[.+\]:\d+$/.test(hostPort);
|
||||||
|
if (hasPort) {
|
||||||
|
return `http://${hostPort}${API_QUEUE_CALLER_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://${hostPort}:${DEFAULT_API_PORT}${API_QUEUE_CALLER_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: "",
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将配置中的服务器地址应用到 axios 实例。
|
||||||
|
*/
|
||||||
|
export function applyServerIpToHttp(serverIp: string): void {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
instance.defaults.baseURL = API_QUEUE_CALLER_PATH;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.defaults.baseURL = buildBaseUrlFromServerIp(serverIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
async (config) => {
|
||||||
|
if (!instance.defaults.baseURL) {
|
||||||
|
throw new Error("未配置服务器地址,请先完成服务地址设置");
|
||||||
|
}
|
||||||
|
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionState: SessionState = await getSession();
|
||||||
|
token = sessionState.queueToken;
|
||||||
|
} catch {
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && !config.url?.includes("/login")) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const { data } = response;
|
||||||
|
if (data.code !== 200) {
|
||||||
|
ElMessage.error(data.message || "请求失败");
|
||||||
|
|
||||||
|
if (data.code === 401) {
|
||||||
|
ElMessage.error("请求 token 过期,请重新登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.message || "请求失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 400:
|
||||||
|
ElMessage.error("请求错误");
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
ElMessage.error("未授权,请重新登录");
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
ElMessage.error("拒绝访问");
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
ElMessage.error("请求地址不存在");
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
ElMessage.error("服务器内部错误");
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
ElMessage.error("网关错误");
|
||||||
|
break;
|
||||||
|
case 503:
|
||||||
|
ElMessage.error("服务不可用");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ElMessage.error("请求失败");
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
ElMessage.error("网络连接异常");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const http = {
|
||||||
|
/**
|
||||||
|
* 发送原生 axios 请求。
|
||||||
|
*/
|
||||||
|
request<T = unknown>(config: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.request(config);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 GET 请求。
|
||||||
|
*/
|
||||||
|
get<T = unknown>(url: string, params?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.get(url, { params, ...config });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 POST 请求。
|
||||||
|
*/
|
||||||
|
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.post(url, data, config);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 PUT 请求。
|
||||||
|
*/
|
||||||
|
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.put(url, data, config);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 DELETE 请求。
|
||||||
|
*/
|
||||||
|
delete<T = unknown>(url: string, params?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.delete(url, { params, ...config });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 PATCH 请求。
|
||||||
|
*/
|
||||||
|
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return instance.patch(url, data, config);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "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" }]
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// @ts-expect-error process is a nodejs global
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
const APP_NAME = "call-client";
|
||||||
|
const CONFIG_FILE_NAME = "config.json";
|
||||||
|
const API_QUEUE_CALLER_PATH = "/api/queue/caller";
|
||||||
|
const DEFAULT_API_PORT = 8845;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户目录。
|
||||||
|
*/
|
||||||
|
function getHomeDirectory(): string {
|
||||||
|
return process.env.HOME || process.env.USERPROFILE || os.homedir() || ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开发环境下的配置文件路径。
|
||||||
|
*/
|
||||||
|
function getConfigFilePath(): string {
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
||||||
|
const baseDirectory =
|
||||||
|
xdgConfigHome && path.isAbsolute(xdgConfigHome)
|
||||||
|
? xdgConfigHome
|
||||||
|
: path.join(getHomeDirectory(), ".config");
|
||||||
|
|
||||||
|
return path.join(baseDirectory, APP_NAME, CONFIG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.APPDATA) {
|
||||||
|
return path.join(process.env.APPDATA, APP_NAME, CONFIG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(getHomeDirectory(), `.${APP_NAME}`, CONFIG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将配置中的 server_ip 规范化为 Vite 代理 target。
|
||||||
|
*/
|
||||||
|
function buildProxyTarget(serverIp: string): string {
|
||||||
|
const raw = serverIp.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
||||||
|
return raw.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostPort = raw.replace(/^\/+/, "");
|
||||||
|
const hasPort = /:\d+$/.test(hostPort) || /^\[.+\]:\d+$/.test(hostPort);
|
||||||
|
if (hasPort) {
|
||||||
|
return `http://${hostPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://${hostPort}:${DEFAULT_API_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地 config.json 读取开发代理目标地址。
|
||||||
|
*/
|
||||||
|
function resolveProxyTarget(): string {
|
||||||
|
const configFilePath = getConfigFilePath();
|
||||||
|
if (!fs.existsSync(configFilePath)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(configFilePath, "utf8");
|
||||||
|
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||||
|
const serverIp = typeof parsed.server_ip === "string" ? parsed.server_ip : "";
|
||||||
|
return buildProxyTarget(serverIp);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyTarget = resolveProxyTarget();
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: ["vue", "vue-router"],
|
||||||
|
dts: false,
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
dts: false,
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (!id.includes("node_modules")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes("element-plus") || id.includes("@element-plus")) {
|
||||||
|
return "vendor-element-plus";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes("vue-router")) {
|
||||||
|
return "vendor-router";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes("axios")) {
|
||||||
|
return "vendor-axios";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes("node_modules/vue/") || id.includes("node_modules/@vue/")) {
|
||||||
|
return "vendor-vue";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "vendor-misc";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
proxy: proxyTarget
|
||||||
|
? {
|
||||||
|
[API_QUEUE_CALLER_PATH]: {
|
||||||
|
target: proxyTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||