feat(): 重构界面

This commit is contained in:
tsl
2026-03-10 14:41:13 +08:00
parent 71bb171f48
commit da677a033e
25 changed files with 4697 additions and 2766 deletions
+99 -23
View File
@@ -1,23 +1,99 @@
use tauri_plugin_autostart::MacosLauncher;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
env_logger::init();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![]),
))
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window.hide();
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
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();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_autostart::init(
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.emit("close-requested", ());
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+71
View File
@@ -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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
/// 发送带交互按钮的 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())
}