feat(): 重构界面
This commit is contained in:
parent
71bb171f48
commit
da677a033e
|
|
@ -33,7 +33,11 @@
|
|||
"Bash(cargo check)",
|
||||
"Bash(pnpm run tauri dev)",
|
||||
"Bash(netstat -ano)",
|
||||
"Bash(findstr :1420)"
|
||||
"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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
/**
|
||||
* https://crm.yunvip123.com/api/UsersNotice/GetUsersNoticeList
|
||||
* POST
|
||||
* cookie: ASP.NET_SessionId=td0mirxws4klqpm0bydhvgjj
|
||||
*/
|
||||
|
||||
const requestData='UserGID=512860ad-aa4a-4268-a5d9-6c7d2e28680b'
|
||||
|
||||
const responseBody
|
||||
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
|
@ -7,21 +7,26 @@
|
|||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"tauri-dev": "tauri dev",
|
||||
"tauri-build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.7",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vue-tsc": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
714
pnpm-lock.yaml
714
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -4120,7 +4120,7 @@ dependencies = [
|
|||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
|
@ -4192,7 +4192,7 @@ dependencies = [
|
|||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4210,6 +4210,7 @@ dependencies = [
|
|||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-store",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4389,7 +4390,7 @@ dependencies = [
|
|||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
|
|
@ -4431,7 +4432,7 @@ dependencies = [
|
|||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4457,7 +4458,7 @@ dependencies = [
|
|||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
|
|
@ -4518,7 +4519,7 @@ checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
|||
dependencies = [
|
||||
"quick-xml 0.37.5",
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
|
|
@ -5321,10 +5322,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
|||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5345,7 +5346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -5395,6 +5396,16 @@ dependencies = [
|
|||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
|
|
@ -5417,14 +5428,27 @@ dependencies = [
|
|||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
|
|
@ -5436,8 +5460,8 @@ version = "0.62.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
|
|
@ -5454,6 +5478,17 @@ dependencies = [
|
|||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
|
|
@ -5465,6 +5500,17 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
|
|
@ -5509,6 +5555,15 @@ dependencies = [
|
|||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
|
@ -5527,6 +5582,16 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
|
|
@ -5964,7 +6029,7 @@ dependencies = [
|
|||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
|
|
|||
|
|
@ -29,3 +29,9 @@ serde_json = "1"
|
|||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Data_Xml_Dom",
|
||||
"UI_Notifications",
|
||||
] }
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
"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-unminimize",
|
||||
"core:image:default",
|
||||
"core:image:allow-from-path"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,76 @@
|
|||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
mod notification;
|
||||
|
||||
const APP_NAME: &str = "工单系统监测";
|
||||
static CALLBACK_PORT: AtomicU16 = AtomicU16::new(0);
|
||||
|
||||
/// 启动本地 HTTP 服务器,用于接收 Toast 通知按钮的回调
|
||||
fn start_notification_callback_server(app_handle: tauri::AppHandle) {
|
||||
let listener =
|
||||
TcpListener::bind("127.0.0.1:0").expect("failed to bind notification callback server");
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
CALLBACK_PORT.store(port, Ordering::SeqCst);
|
||||
log::info!("Notification callback server on port {}", port);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
handle_callback_request(&app_handle, stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_callback_request(app_handle: &tauri::AppHandle, mut stream: std::net::TcpStream) {
|
||||
let mut buf = [0u8; 1024];
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
let request = String::from_utf8_lossy(&buf[..n]);
|
||||
|
||||
let (action, html) = if request.contains("GET /postpone") {
|
||||
let _ = app_handle.emit("notification-action", "postpone_1h");
|
||||
if let Some(w) = app_handle.get_webview_window("main") {
|
||||
let _ = w.unminimize();
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
("postpone", "已推迟1小时,监控将在1小时后恢复")
|
||||
} else {
|
||||
("unknown", "未知操作")
|
||||
};
|
||||
|
||||
log::info!("Notification callback: {}", action);
|
||||
|
||||
let body = format!(
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'></head>\
|
||||
<body style='display:flex;justify-content:center;align-items:center;\
|
||||
height:80vh;font-family:system-ui;color:#333'>\
|
||||
<div style='text-align:center'><h2>{}</h2>\
|
||||
<p style='color:#888'>此标签页可以关闭</p></div></body>\
|
||||
<script>setTimeout(()=>window.close(),1500)</script></html>",
|
||||
html
|
||||
);
|
||||
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(response.as_bytes());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn send_clickable_notification(title: String, body: String) -> Result<(), String> {
|
||||
let port = CALLBACK_PORT.load(Ordering::SeqCst);
|
||||
if port == 0 {
|
||||
return Err("Notification callback server not ready".into());
|
||||
}
|
||||
notification::send_interactive_toast(&title, &body, port, APP_NAME)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
env_logger::init();
|
||||
|
|
@ -12,10 +83,15 @@ pub fn run() {
|
|||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![]),
|
||||
))
|
||||
.setup(|app| {
|
||||
start_notification_callback_server(app.handle().clone());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![send_clickable_notification])
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
let _ = window.emit("close-requested", ());
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/// Windows Toast 通知模块
|
||||
/// 通过本地 HTTP 端点桥接按钮点击事件,所有按钮均使用 protocol 激活(兼容非打包应用)
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows::Data::Xml::Dom::XmlDocument;
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows::UI::Notifications::{ToastNotification, ToastNotificationManager};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn xml_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
/// 发送带交互按钮的 Toast 通知
|
||||
///
|
||||
/// - 点击消息体 → 关闭通知(非打包应用不支持 foreground 激活)
|
||||
/// - 点击"推迟1小时" → 通过 localhost 回调暂停监控并弹出应用窗口
|
||||
/// - 点击"去处理" → 直接打开 CRM 网站
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn send_interactive_toast(
|
||||
title: &str,
|
||||
body: &str,
|
||||
port: u16,
|
||||
app_name: &str,
|
||||
) -> Result<(), String> {
|
||||
let toast_xml = format!(
|
||||
r#"<toast activationType="protocol">
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
<text>{title}</text>
|
||||
<text>{body}</text>
|
||||
</binding>
|
||||
</visual>
|
||||
<actions>
|
||||
<action content="推迟1小时" arguments="http://127.0.0.1:{port}/postpone" activationType="protocol"/>
|
||||
<action content="去处理" arguments="https://crm.yunvip123.com/index.html" activationType="protocol"/>
|
||||
</actions>
|
||||
</toast>"#,
|
||||
port = port,
|
||||
title = xml_escape(title),
|
||||
body = xml_escape(body),
|
||||
);
|
||||
|
||||
let xml = XmlDocument::new().map_err(|e| e.to_string())?;
|
||||
xml.LoadXml(&toast_xml.into()).map_err(|e| e.to_string())?;
|
||||
|
||||
let toast =
|
||||
ToastNotification::CreateToastNotification(&xml).map_err(|e| e.to_string())?;
|
||||
|
||||
let notifier =
|
||||
ToastNotificationManager::CreateToastNotifierWithId(&app_name.into())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
notifier.Show(&toast).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn send_interactive_toast(
|
||||
_title: &str,
|
||||
_body: &str,
|
||||
_port: u16,
|
||||
_app_name: &str,
|
||||
) -> Result<(), String> {
|
||||
Err("Toast 通知仅支持 Windows 平台".to_string())
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "tauri-app",
|
||||
"version": "0.1.0",
|
||||
"productName": "工单系统监测",
|
||||
"version": "0.1.1",
|
||||
"identifier": "com.administrator.tauri-app",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "tauri-app",
|
||||
"title": "工单系统监测",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
|
|
@ -23,13 +23,17 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": ["nsis"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"icons/32x32.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
750
src/App.vue
750
src/App.vue
|
|
@ -1,645 +1,141 @@
|
|||
<script setup lang="ts">
|
||||
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, sendNotification } from "@tauri-apps/plugin-notification";
|
||||
import { enable as enableAutostart, disable as disableAutostart, isEnabled as isAutostartEnabled } from "@tauri-apps/plugin-autostart";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DashboardOutlined,
|
||||
ToolOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
// ===== 常量 =====
|
||||
const API_BASE = "https://crm.yunvip123.com/api";
|
||||
const LOGIN_URL = `${API_BASE}/SystemUser/Login`;
|
||||
const CHECK_URL = `${API_BASE}/DemandManage/QueryIndexCount`;
|
||||
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 from "./components/AppStatusBar.vue";
|
||||
import CloseDialog from "./components/CloseDialog.vue";
|
||||
import { useMonitorStore } from "./stores/monitor";
|
||||
|
||||
// ===== 响应式数据 =====
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const message = ref("");
|
||||
const isLoading = ref(false);
|
||||
const isLoggedIn = ref(false);
|
||||
const isMonitoring = ref(false);
|
||||
const autoStartEnabled = ref(false);
|
||||
const logs = ref<{ timestamp: string; level: string; message: string; category: string }[]>([]);
|
||||
const showLogs = ref(false);
|
||||
const showConfig = ref(false);
|
||||
const networkStatus = ref({
|
||||
is_connected: false,
|
||||
api_reachable: false,
|
||||
response_time: null as number | null,
|
||||
last_check: "",
|
||||
});
|
||||
const activeTab = ref("monitor");
|
||||
const store = useMonitorStore();
|
||||
|
||||
const config = ref({
|
||||
check_interval: 60,
|
||||
auto_start: false,
|
||||
show_notifications: true,
|
||||
network_timeout: 30,
|
||||
});
|
||||
|
||||
// ===== 内部状态 =====
|
||||
let sessionId = "";
|
||||
let userGid = "";
|
||||
let userName = "";
|
||||
let monitorTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ===== 日志 =====
|
||||
function addLog(level: string, msg: string, category: string) {
|
||||
const now = new Date();
|
||||
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
||||
logs.value.push({ timestamp, level, message: msg, category });
|
||||
if (logs.value.length > 1000) logs.value.shift();
|
||||
}
|
||||
|
||||
// ===== 通知 =====
|
||||
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) {
|
||||
sendNotification({ title, body, actionTypeId: "crm-open" });
|
||||
addLog("INFO", `通知: ${title} - ${body}`, "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",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
console.log(Object.fromEntries(resp.headers.entries()));
|
||||
|
||||
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,API检查将使用无Cookie模式", "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 apiCheckStatus(): Promise<void> {
|
||||
if (!userGid) {
|
||||
addLog("WARN", "会话已失效,请重新登录", "SCHEDULER");
|
||||
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;
|
||||
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER");
|
||||
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0) {
|
||||
const msgs: string[] = [];
|
||||
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
|
||||
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
|
||||
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
|
||||
await notify("工单提醒", msgs.join(","));
|
||||
}
|
||||
} else {
|
||||
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
|
||||
}
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `API 检查失败: ${e.message || e}`, "SCHEDULER");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 网络检测 =====
|
||||
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 });
|
||||
console.log("Network check response:", resp);
|
||||
console.log(resp.headers);
|
||||
|
||||
isConnected = resp.ok;
|
||||
} catch { /* 网络不通 */ }
|
||||
|
||||
// 检查 API 服务器
|
||||
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 不可达 */ }
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
networkStatus.value = {
|
||||
is_connected: isConnected,
|
||||
api_reachable: apiReachable,
|
||||
response_time: responseTime,
|
||||
last_check: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`,
|
||||
};
|
||||
|
||||
addLog("INFO", `网络检查完成 - 连接: ${isConnected}, API可达: ${apiReachable}`, "NETWORK");
|
||||
message.value = "网络状态检查完成";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ===== 保存设置 =====
|
||||
function saveSettings() {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
message.value = "请输入用户名和密码";
|
||||
return;
|
||||
}
|
||||
message.value = "设置保存成功";
|
||||
addLog("INFO", "用户凭据已保存", "CONFIG");
|
||||
}
|
||||
|
||||
// ===== 监控控制 =====
|
||||
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;
|
||||
}
|
||||
isMonitoring.value = false;
|
||||
isLoggedIn.value = false;
|
||||
sessionId = "";
|
||||
userGid = "";
|
||||
addLog("INFO", "监控已停止", "MONITOR");
|
||||
message.value = "监控已停止";
|
||||
}
|
||||
|
||||
// ===== 手动检查 =====
|
||||
async function manualCheck() {
|
||||
if (!isLoggedIn.value) {
|
||||
message.value = "请先开始监控(登录)";
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
await apiCheckStatus();
|
||||
message.value = "手动检查完成";
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ===== 配置更新 =====
|
||||
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");
|
||||
}
|
||||
|
||||
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.show();
|
||||
await mainWindow.setFocus();
|
||||
}
|
||||
}),
|
||||
await MenuItem.new({
|
||||
id: "quit",
|
||||
text: "退出",
|
||||
action: async () => {
|
||||
await mainWindow.destroy();
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// 创建托盘图标
|
||||
const icon = await Image.fromPath("icons/32x32.png");
|
||||
await TrayIcon.new({
|
||||
icon,
|
||||
tooltip: "工单监控系统",
|
||||
menu,
|
||||
menuOnLeftClick: false,
|
||||
action: async (event) => {
|
||||
if (event.type === "Click" && event.button === "Left") {
|
||||
await mainWindow.show();
|
||||
await mainWindow.setFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addLog("INFO", "系统托盘创建成功", "SYSTEM");
|
||||
} catch (e: any) {
|
||||
addLog("ERROR", `系统托盘创建失败: ${e.message || e}`, "SYSTEM");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 最小化到托盘 =====
|
||||
async function minimizeToTray() {
|
||||
try {
|
||||
const mainWindow = getCurrentWindow();
|
||||
await mainWindow.hide();
|
||||
} catch (e: any) {
|
||||
message.value = `最小化失败: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 日志操作 =====
|
||||
function toggleLogs() {
|
||||
showLogs.value = !showLogs.value;
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs.value = [];
|
||||
message.value = "日志已清除";
|
||||
}
|
||||
|
||||
function toggleConfig() {
|
||||
showConfig.value = !showConfig.value;
|
||||
if (showConfig.value) {
|
||||
config.value.auto_start = autoStartEnabled.value;
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessage() {
|
||||
message.value = "";
|
||||
}
|
||||
|
||||
function formatResponseTime(time: number | null) {
|
||||
return time ? `${time}ms` : "N/A";
|
||||
}
|
||||
|
||||
function getNetworkStatusText() {
|
||||
if (!networkStatus.value.is_connected) return "网络断开";
|
||||
if (!networkStatus.value.api_reachable) return "API不可达";
|
||||
return "网络正常";
|
||||
}
|
||||
|
||||
// ===== 生命周期 =====
|
||||
onMounted(async () => {
|
||||
// 初始化系统托盘
|
||||
await setupTray();
|
||||
|
||||
// 检查自启动状态
|
||||
try {
|
||||
autoStartEnabled.value = await isAutostartEnabled();
|
||||
config.value.auto_start = autoStartEnabled.value;
|
||||
} catch { /* ignore */ }
|
||||
|
||||
addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (monitorTimer) clearInterval(monitorTimer);
|
||||
});
|
||||
onMounted(() => store.initialize());
|
||||
onUnmounted(() => store.cleanup());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="app">
|
||||
<div class="app__header">
|
||||
<h1 class="app__title">系统监控设置</h1>
|
||||
<div class="app__status">
|
||||
<div class="status-row">
|
||||
<span class="status-indicator" :class="{ 'status-indicator--active': isLoggedIn }"></span>
|
||||
<span class="status-text">
|
||||
{{ isLoggedIn ? '已登录' : '未登录' }}
|
||||
{{ isMonitoring ? ' - 监控中' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="network-indicator" :class="{
|
||||
'network-indicator--connected': networkStatus.is_connected,
|
||||
'network-indicator--api': networkStatus.api_reachable
|
||||
}"></span>
|
||||
<span class="network-text">
|
||||
{{ getNetworkStatusText() }}
|
||||
<span v-if="networkStatus.response_time" class="response-time">
|
||||
({{ formatResponseTime(networkStatus.response_time) }})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-layout class="app-layout">
|
||||
<a-layout-header class="app-header">
|
||||
<StatusHeader />
|
||||
</a-layout-header>
|
||||
|
||||
<div class="app__content">
|
||||
<!-- 设置表单 -->
|
||||
<div class="settings-card">
|
||||
<h2 class="settings-card__title">登录设置</h2>
|
||||
<form class="settings-form" @submit.prevent="saveSettings">
|
||||
<div class="form-group">
|
||||
<label for="username" class="form-group__label">用户名</label>
|
||||
<input id="username" v-model="username" type="text" class="form-group__input" placeholder="请输入用户名" :disabled="isLoading" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-group__label">密码</label>
|
||||
<input id="password" v-model="password" type="password" class="form-group__input" placeholder="请输入密码" :disabled="isLoading" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary" :disabled="isLoading">
|
||||
{{ isLoading ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<a-layout-content class="app-content">
|
||||
<a-tabs v-model:activeKey="activeTab" tab-position="left" class="main-tabs">
|
||||
<a-tab-pane key="monitor">
|
||||
<template #tab>
|
||||
<span><DashboardOutlined /> 监控</span>
|
||||
</template>
|
||||
<MonitorControl />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-card">
|
||||
<h2 class="actions-card__title">监控控制</h2>
|
||||
<div class="actions-grid">
|
||||
<button class="btn btn--success" :disabled="isLoading || isMonitoring" @click="startMonitoring">
|
||||
{{ isLoading ? '启动中...' : '开始监控' }}
|
||||
</button>
|
||||
<button class="btn btn--warning" :disabled="isLoading || !isMonitoring" @click="stopMonitoring">
|
||||
停止监控
|
||||
</button>
|
||||
<button class="btn btn--info" :disabled="isLoading || !isLoggedIn" @click="manualCheck">
|
||||
{{ isLoading ? '检查中...' : '手动检查API' }}
|
||||
</button>
|
||||
<button class="btn btn--secondary" :disabled="isLoading" @click="minimizeToTray">
|
||||
最小化到托盘
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a-tab-pane key="login">
|
||||
<template #tab>
|
||||
<span><SettingOutlined /> 账号</span>
|
||||
</template>
|
||||
<LoginForm />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 测试按钮 -->
|
||||
<div class="actions-card">
|
||||
<h2 class="actions-card__title">测试功能</h2>
|
||||
<div class="actions-grid">
|
||||
<button class="btn btn--primary" :disabled="isLoading" @click="checkNetworkStatus">
|
||||
{{ isLoading ? '检查中...' : '网络检测' }}
|
||||
</button>
|
||||
<button class="btn btn--info" @click="toggleLogs">
|
||||
{{ showLogs ? '隐藏日志' : '显示日志' }}
|
||||
</button>
|
||||
<button class="btn btn--primary" @click="toggleConfig">
|
||||
{{ showConfig ? '隐藏配置' : '显示配置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a-tab-pane key="settings">
|
||||
<template #tab>
|
||||
<span><ToolOutlined /> 配置</span>
|
||||
</template>
|
||||
<SettingsPanel />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 配置面板 -->
|
||||
<div v-if="showConfig" class="config-card">
|
||||
<h2 class="config-card__title">高级配置</h2>
|
||||
<div class="config-form">
|
||||
<div class="form-group">
|
||||
<label for="check_interval" class="form-group__label">检查间隔(秒)</label>
|
||||
<input id="check_interval" v-model.number="config.check_interval" type="number" min="10" max="3600" class="form-group__input" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="network_timeout" class="form-group__label">网络超时(秒)</label>
|
||||
<input id="network_timeout" v-model.number="config.network_timeout" type="number" min="5" max="120" class="form-group__input" />
|
||||
</div>
|
||||
<div class="form-group form-group--checkbox">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="config.auto_start" type="checkbox" class="checkbox-input" />
|
||||
<span class="checkbox-text">开机自启动</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group form-group--checkbox">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="config.show_notifications" type="checkbox" class="checkbox-input" />
|
||||
<span class="checkbox-text">显示系统通知</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn--primary" :disabled="isLoading" @click="updateConfig">
|
||||
{{ isLoading ? '保存中...' : '保存配置' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a-tab-pane key="logs">
|
||||
<template #tab>
|
||||
<span><FileTextOutlined /> 日志</span>
|
||||
</template>
|
||||
<LogViewer />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-layout-content>
|
||||
|
||||
<!-- 日志面板 -->
|
||||
<div v-if="showLogs" class="logs-card">
|
||||
<div class="logs-header">
|
||||
<h2 class="logs-card__title">系统日志</h2>
|
||||
<button class="btn btn--small btn--secondary" @click="clearLogs">清除日志</button>
|
||||
</div>
|
||||
<div class="logs-content">
|
||||
<div v-if="logs.length === 0" class="logs-empty">暂无日志记录</div>
|
||||
<div v-else class="logs-list">
|
||||
<div v-for="log in [...logs].reverse()" :key="`${log.timestamp}-${log.message}`" class="log-entry" :class="`log-entry--${log.level.toLowerCase()}`">
|
||||
<span class="log-time">{{ log.timestamp }}</span>
|
||||
<span class="log-category">[{{ log.category }}]</span>
|
||||
<span class="log-level">{{ log.level }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppStatusBar />
|
||||
</a-layout>
|
||||
|
||||
<!-- 消息显示 -->
|
||||
<div v-if="message" class="message-card">
|
||||
<div class="message-card__content">
|
||||
<p class="message-card__text">{{ message }}</p>
|
||||
<button class="message-card__close" @click="clearMessage">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<CloseDialog />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.app__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.app__title { color: white; font-size: 24px; font-weight: 600; margin: 0; }
|
||||
.app__status { display: flex; flex-direction: column; gap: 8px; }
|
||||
.status-row { display: flex; align-items: center; gap: 8px; }
|
||||
.status-indicator {
|
||||
width: 12px; height: 12px; border-radius: 50%;
|
||||
background-color: #dc3545; transition: background-color 0.3s ease;
|
||||
}
|
||||
.status-indicator--active { background-color: #28a745; }
|
||||
.status-text { color: white; font-size: 14px; font-weight: 500; }
|
||||
.network-indicator {
|
||||
width: 12px; height: 12px; border-radius: 50%;
|
||||
background-color: #dc3545; transition: background-color 0.3s ease;
|
||||
}
|
||||
.network-indicator--connected { background-color: #ffc107; }
|
||||
.network-indicator--api { background-color: #28a745; }
|
||||
.network-text { color: white; font-size: 12px; font-weight: 400; }
|
||||
.response-time { color: #ccc; font-size: 11px; }
|
||||
.app__content { display: grid; gap: 20px; max-width: 600px; margin: 0 auto; }
|
||||
.settings-card, .actions-card, .config-card, .logs-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px; padding: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.settings-card__title, .actions-card__title, .config-card__title, .logs-card__title {
|
||||
color: #333; font-size: 18px; font-weight: 600; margin: 0 0 20px 0;
|
||||
}
|
||||
.settings-form, .config-form { display: flex; flex-direction: column; gap: 16px; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.form-group__label { color: #555; font-size: 14px; font-weight: 500; }
|
||||
.form-group__input {
|
||||
padding: 12px 16px; border: 2px solid #e1e5e9; border-radius: 8px;
|
||||
font-size: 14px; transition: border-color 0.3s ease; background: white;
|
||||
}
|
||||
.form-group__input:focus { outline: none; border-color: #667eea; }
|
||||
.form-group__input:disabled { background-color: #f8f9fa; cursor: not-allowed; }
|
||||
.actions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
|
||||
.btn {
|
||||
padding: 12px 20px; border: none; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 500; cursor: pointer;
|
||||
transition: all 0.3s ease; text-align: center;
|
||||
}
|
||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn--primary { background: #667eea; color: white; }
|
||||
.btn--primary:hover:not(:disabled) { background: #5a6fd8; transform: translateY(-2px); }
|
||||
.btn--success { background: #28a745; color: white; }
|
||||
.btn--success:hover:not(:disabled) { background: #218838; transform: translateY(-2px); }
|
||||
.btn--warning { background: #ffc107; color: #212529; }
|
||||
.btn--warning:hover:not(:disabled) { background: #e0a800; transform: translateY(-2px); }
|
||||
.btn--info { background: #17a2b8; color: white; }
|
||||
.btn--info:hover:not(:disabled) { background: #138496; transform: translateY(-2px); }
|
||||
.btn--secondary { background: #6c757d; color: white; }
|
||||
.btn--secondary:hover:not(:disabled) { background: #545b62; transform: translateY(-2px); }
|
||||
.btn--small { padding: 6px 12px; font-size: 12px; }
|
||||
.message-card {
|
||||
background: rgba(255, 255, 255, 0.95); border-radius: 12px;
|
||||
padding: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.message-card__content { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
||||
.message-card__text { color: #333; font-size: 14px; margin: 0; flex: 1; }
|
||||
.message-card__close {
|
||||
background: none; border: none; font-size: 20px; color: #999;
|
||||
cursor: pointer; padding: 0; width: 24px; height: 24px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 4px; transition: all 0.3s ease;
|
||||
}
|
||||
.message-card__close:hover { background: #f8f9fa; color: #333; }
|
||||
.form-group--checkbox { flex-direction: row; align-items: center; }
|
||||
.checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
||||
.checkbox-input { width: 16px; height: 16px; }
|
||||
.checkbox-text { color: #555; font-size: 14px; font-weight: 500; }
|
||||
.logs-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.logs-content {
|
||||
max-height: 400px; overflow-y: auto;
|
||||
border: 1px solid #e1e5e9; border-radius: 8px; background: #f8f9fa;
|
||||
}
|
||||
.logs-empty { padding: 20px; text-align: center; color: #999; font-style: italic; }
|
||||
.logs-list { padding: 8px; }
|
||||
.log-entry {
|
||||
display: grid; grid-template-columns: auto auto auto 1fr; gap: 8px;
|
||||
padding: 4px 8px; font-family: 'Courier New', monospace; font-size: 12px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.log-entry:last-child { border-bottom: none; }
|
||||
.log-entry--info { color: #0066cc; }
|
||||
.log-entry--warn { color: #ff8800; }
|
||||
.log-entry--error { color: #cc0000; }
|
||||
.log-entry--debug { color: #666; }
|
||||
.log-time { color: #666; font-weight: 500; }
|
||||
.log-category { color: #0066cc; font-weight: 600; }
|
||||
.log-level { font-weight: 600; text-transform: uppercase; }
|
||||
.log-message { word-break: break-word; }
|
||||
@media (max-width: 768px) {
|
||||
.app { padding: 16px; }
|
||||
.app__header { flex-direction: column; gap: 12px; text-align: center; }
|
||||
.actions-grid { grid-template-columns: 1fr; }
|
||||
.log-entry { grid-template-columns: 1fr; gap: 4px; }
|
||||
.logs-content { max-height: 300px; }
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.app-header {
|
||||
background: transparent !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
padding: 16px 24px !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.app-content {
|
||||
padding: 0 24px 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 16px;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-nav) {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
padding: 12px 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-nav::before) {
|
||||
border: none;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-tab) {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
padding: 10px 20px !important;
|
||||
margin: 0 !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-tab:hover) {
|
||||
color: #fff !important;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-ink-bar) {
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.main-tabs :deep(.ant-tabs-content-holder) {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-statusbar">
|
||||
<span v-if="store.isPostponed" class="statusbar-item statusbar-item--warning">
|
||||
<ExclamationCircleOutlined /> 监控已推迟,1小时后自动恢复
|
||||
</span>
|
||||
<span v-if="store.isPostponed && store.message" class="statusbar-sep" />
|
||||
<span v-if="store.message" class="statusbar-item statusbar-item--info">
|
||||
<InfoCircleOutlined /> {{ store.message }}
|
||||
</span>
|
||||
</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-item--warning {
|
||||
color: #ffe58f;
|
||||
}
|
||||
.statusbar-item--info {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.statusbar-sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { DeleteOutlined, DownloadOutlined } from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
const reversedLogs = computed(() => [...store.logs].reverse());
|
||||
|
||||
const levelColor: Record<string, string> = {
|
||||
INFO: "blue",
|
||||
WARN: "orange",
|
||||
ERROR: "red",
|
||||
DEBUG: "default",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="log-viewer">
|
||||
<div class="log-viewer__toolbar">
|
||||
<span class="log-viewer__count">共 {{ store.logs.length }} 条日志</span>
|
||||
<a-space>
|
||||
<a-button size="small" :disabled="store.logs.length === 0" @click="store.exportLogs()">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="store.clearLogs()">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
清除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-empty v-if="store.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"
|
||||
>
|
||||
<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>
|
||||
<span class="log-item__msg">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.log-viewer__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.log-viewer__count {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
.log-viewer__list {
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-family: "Courier New", Consolas, monospace;
|
||||
}
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.log-item__time {
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.log-item__tag {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.log-item__msg {
|
||||
word-break: break-word;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
</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-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-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input-password
|
||||
v-model:value="store.password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="store.isLoading"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a-form-item label="ASP.NET_SessionId">
|
||||
<a-input
|
||||
v-model:value="store.tokenSessionId"
|
||||
placeholder="请输入 ASP.NET_SessionId"
|
||||
:disabled="store.isLoading"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<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' }}
|
||||
</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>
|
||||
<a-button danger :disabled="store.isLoading || !store.isMonitoring" @click="store.stopMonitoring()">
|
||||
停止监控
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
ReloadOutlined,
|
||||
MinusCircleOutlined,
|
||||
BellOutlined,
|
||||
WifiOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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' }" />
|
||||
</div>
|
||||
<div class="ticket-card ticket-card--stayclose">
|
||||
<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' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="store.ticketCounts.lastCheck" class="last-check">
|
||||
<ClockCircleOutlined /> 上次检查:{{ store.ticketCounts.lastCheck }}
|
||||
</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>
|
||||
<a-button @click="store.checkNetworkStatus()">
|
||||
<template #icon><WifiOutlined /></template>
|
||||
网络检测
|
||||
</a-button>
|
||||
<a-button @click="store.testNotification()">
|
||||
<template #icon><BellOutlined /></template>
|
||||
测试通知
|
||||
</a-button>
|
||||
<a-button @click="store.minimizeToTray()">
|
||||
<template #icon><MinusCircleOutlined /></template>
|
||||
最小化到托盘
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.monitor-control {
|
||||
padding-top: 4px;
|
||||
}
|
||||
.ticket-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ticket-card {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.ticket-card--pending {
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
.ticket-card--stayclose {
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
.ticket-card--confirm {
|
||||
background: #e6f4ff;
|
||||
border: 1px solid #91caff;
|
||||
}
|
||||
.last-check {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<script setup lang="ts">
|
||||
import { useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="检查间隔(秒)">
|
||||
<a-input-number
|
||||
v-model:value="store.config.check_interval"
|
||||
:min="10"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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-form-item>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开机自启动">
|
||||
<a-switch v-model:checked="store.config.auto_start" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="启动后自动监控">
|
||||
<a-switch v-model:checked="store.config.auto_monitor" />
|
||||
<div class="form-hint">需同时开启「记住密码」</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="显示系统通知">
|
||||
<a-switch v-model:checked="store.config.show_notifications" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="通知提示音">
|
||||
<a-switch
|
||||
v-model:checked="store.config.notification_sound"
|
||||
:disabled="!store.config.show_notifications"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-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>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="store.isLoading" @click="store.updateConfig()">
|
||||
保存配置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
CloseCircleFilled,
|
||||
SyncOutlined,
|
||||
PauseCircleFilled,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { APP_NAME, useMonitorStore } from "../stores/monitor";
|
||||
|
||||
const store = useMonitorStore();
|
||||
|
||||
function getNetworkStatusText() {
|
||||
if (!store.networkStatus.is_connected) return "网络断开";
|
||||
if (!store.networkStatus.api_reachable) 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.status-header__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.response-time {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,547 @@
|
|||
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`;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface TicketCounts {
|
||||
pending: number;
|
||||
stayclose: number;
|
||||
confirm: 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, 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 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;
|
||||
ticketCounts.value = {
|
||||
pending, stayclose, confirm,
|
||||
lastCheck: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER");
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0) {
|
||||
const msgs: string[] = [];
|
||||
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
|
||||
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
|
||||
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +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).mount("#app");
|
||||
createApp(App).use(createPinia()).use(Antd).mount("#app");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,613 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
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`;
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface TicketCounts {
|
||||
pending: number;
|
||||
stayclose: number;
|
||||
confirm: number;
|
||||
lastCheck: string;
|
||||
}
|
||||
|
||||
export const useMonitorStore = defineStore("monitor", () => {
|
||||
// ===== 响应式数据 =====
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const rememberPassword = ref(false);
|
||||
const loginMode = ref<"password" | "token">("password");
|
||||
const tokenSessionId = ref("");
|
||||
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, 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,
|
||||
close_action: "ask" as "ask" | "minimize" | "close",
|
||||
});
|
||||
const showCloseDialog = ref(false);
|
||||
|
||||
// ===== 内部状态 =====
|
||||
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;
|
||||
let unlistenClose: 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 apiCheckStatus(isRetry = false): Promise<void> {
|
||||
if (!userGid && loginMode.value === "password") {
|
||||
addLog("WARN", "会话已失效,尝试重新登录", "SCHEDULER");
|
||||
if (!isRetry) await tryReLogin();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body: Record<string, string> = {};
|
||||
if (userGid) body.UserGID = userGid;
|
||||
const resp = await fetch(CHECK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `ASP.NET_SessionId=${sessionId}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
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;
|
||||
ticketCounts.value = {
|
||||
pending, stayclose, confirm,
|
||||
lastCheck: formatTime(new Date()),
|
||||
};
|
||||
addLog("INFO", `检查完成 - 待处理: ${pending}, 待关闭: ${stayclose}, 待确认: ${confirm}`, "SCHEDULER");
|
||||
if (pending > 0 || stayclose > 0 || confirm > 0) {
|
||||
const msgs: string[] = [];
|
||||
if (pending > 0) msgs.push(`待处理工单: ${pending}`);
|
||||
if (stayclose > 0) msgs.push(`待关闭工单: ${stayclose}`);
|
||||
if (confirm > 0) msgs.push(`待确认工单: ${confirm}`);
|
||||
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 (loginMode.value === "token") {
|
||||
addLog("WARN", "Token 模式下无法自动重登录,请更换新的 Token", "SESSION");
|
||||
message.value = "Token 可能已过期,请更换新的 Token";
|
||||
return;
|
||||
}
|
||||
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;
|
||||
loginMode.value = data.loginMode || "password";
|
||||
tokenSessionId.value = data.tokenSessionId || "";
|
||||
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,
|
||||
loginMode: loginMode.value,
|
||||
tokenSessionId: tokenSessionId.value,
|
||||
})
|
||||
);
|
||||
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 (loginMode.value === "password") {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
message.value = "请输入用户名和密码";
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!tokenSessionId.value.trim()) {
|
||||
message.value = "请输入 Token (ASP.NET_SessionId)";
|
||||
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 (loginMode.value === "password") {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
if (!tokenSessionId.value.trim()) {
|
||||
message.value = "请先输入 Token (ASP.NET_SessionId)";
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
sessionId = tokenSessionId.value.trim();
|
||||
isLoggedIn.value = true;
|
||||
addLog("INFO", `使用 Token 登录,SessionId: ${sessionId.substring(0, 8)}...`, "LOGIN");
|
||||
}
|
||||
isMonitoring.value = true;
|
||||
await apiCheckStatus();
|
||||
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
|
||||
addLog("INFO", "监控已启动", "MONITOR");
|
||||
message.value = loginMode.value === "password"
|
||||
? `监控已开始,欢迎 ${userName}!`
|
||||
: "监控已开始(Token 登录)";
|
||||
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(); },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 生命周期(由 App.vue 调用)=====
|
||||
async function initialize() {
|
||||
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();
|
||||
});
|
||||
unlistenClose = await listen<void>("close-requested", async () => {
|
||||
const action = config.value.close_action;
|
||||
if (action === "minimize") {
|
||||
await getCurrentWindow().hide();
|
||||
} else if (action === "close") {
|
||||
await getCurrentWindow().destroy();
|
||||
} else {
|
||||
showCloseDialog.value = true;
|
||||
}
|
||||
});
|
||||
addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||
checkNetworkStatus();
|
||||
|
||||
if (config.value.auto_monitor && rememberPassword.value) {
|
||||
const canAutoStart = loginMode.value === "token"
|
||||
? !!tokenSessionId.value
|
||||
: !!(username.value && password.value);
|
||||
if (canAutoStart) {
|
||||
addLog("INFO", "自动登录并开始监控...", "SYSTEM");
|
||||
await startMonitoring();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (monitorTimer) clearInterval(monitorTimer);
|
||||
if (postponeTimer) clearTimeout(postponeTimer);
|
||||
unlistenNotification?.();
|
||||
unlistenClose?.();
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
username, password, rememberPassword, loginMode, tokenSessionId,
|
||||
message, isLoading,
|
||||
isLoggedIn, isMonitoring, isPostponed, autoStartEnabled,
|
||||
logs, networkStatus, config, ticketCounts,
|
||||
showCloseDialog,
|
||||
// 方法
|
||||
initialize, cleanup,
|
||||
addLog, clearLogs, clearMessage, saveSettings, exportLogs,
|
||||
startMonitoring, stopMonitoring, manualCheck,
|
||||
testNotification, checkNetworkStatus, updateConfig,
|
||||
minimizeToTray, handleCloseAction,
|
||||
};
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
|
|
|
|||
|
|
@ -29,4 +29,28 @@ export default defineConfig(async () => ({
|
|||
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