ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Tauri+Vue3构建环世界风格桌面告警工具

基于Tauri+Vue3构建环世界风格桌面告警工具 你是否厌倦了那些在系统托盘闪烁一下就消失、或者用弹窗粗暴打断你工作的系统通知当服务器宕机、CI/CD流水线失败、或者监控指标异常时你希望告警信息能以一种更优雅、更持久、且不打扰你当前工作流的方式呈现。这正是我开发这个桌面告警小工具的初衷。它不是一个复杂的监控平台而是一个轻量级的“信息展示终端”。其灵感直接来源于我非常喜爱的游戏《环世界》RimWorld中的信件系统当有事件发生时一封风格独特的信件会从屏幕边缘滑入停留在桌面一角你可以随时点击查看详情也可以暂时忽略它让它安静地待在那里直到你手动处理。这种设计既保证了信息的可达性又最大程度地尊重了用户的工作沉浸感。本文将详细介绍这个开源小工具的实现。它不仅仅是一个玩具而是一个具备实用价值的工程实践。你将了解到如何用现代前端技术Vue 3 Vite构建一个无边框、可拖拽的桌面应用窗口如何设计一个简洁的HTTP API来接收告警信息以及如何将其打包成真正的Windows桌面应用。更重要的是我会分享在开发过程中遇到的真实“坑点”比如跨进程通信、窗口置顶与穿透、以及生产环境下的打包优化。无论你是想为自己的项目添加一个酷炫的桌面通知组件还是想学习如何将Web技术应用于桌面端这篇文章都将提供一条清晰的路径。1. 核心问题我们到底需要什么样的告警方式在深入代码之前我们必须先厘清需求。传统的告警方式主要有以下几种但各有弊端邮件/短信延迟高容易被淹没且无法快速定位问题。适合最终兜底不适合需要即时关注的开发/运维场景。即时通讯工具如钉钉/企业微信机器人信息流式展示历史消息容易被刷走需要主动打开特定会话查看。系统托盘通知一闪而过无法留存历史。如果你当时恰好没看屏幕就错过了。弹窗/声音告警极具侵入性会打断正在进行中的工作比如编码、会议容易引起反感。《环世界》的信件系统给了我们一个完美的隐喻“非侵入式的持久化通知”。它的核心优势在于优雅入场信息滑入而非弹出减少惊吓感。持久留存信息以“信件”形式停留在桌面特定区域不会自动消失。按需处理用户可以在方便的时候点击查阅详情拥有控制权。状态可视通过信件的样式如颜色、图标可以直观区分告警级别正常、警告、错误。因此我们这个小工具的目标非常明确构建一个Windows桌面应用它常驻后台提供一个HTTP接口。任何能发送HTTP请求的程序如脚本、监控系统、后端服务都可以向它发送“信件”这些信件会以美观的UI形式展示在桌面角落供用户随时查看。2. 技术选型与整体架构要实现这个目标我们面临几个关键选择如何构建桌面应用原生开发C#/C学习成本高且跨平台性差。我们选择Electron或Tauri。鉴于我们的工具需要常驻后台且资源占用应尽可能小Tauri是更优的选择。它使用Rust构建核心应用前端使用Web技术最终打包出的二进制文件体积远小于Electron。前端框架Vue 3 Composition API script setup语法能让我们用最简洁的代码实现响应式UI。如何通信Tauri 提供了强大的前后端Rust ⇄ Web通信机制。我们可以利用它在Rust后端启动一个HTTP服务器接收外部请求再通过Tauri的事件系统传递给前端UI进行渲染。整体架构图如下概念描述外部系统 (Python脚本、Node.js服务、Golang程序等) | | HTTP POST /alert V [Tauri后端 - Rust] |- 启动 HTTP Server (监听本地端口如 15555) |- 接收请求解析JSON数据 |- 通过 Tauri emit 事件将数据发送到前端 | V [Tauri前端 - Vue 3] |- 监听 Tauri 事件 |- 将新告警信件添加到信件列表 |- 渲染信件UI滑动入场、桌面置顶显示这个架构清晰地将“接收逻辑”和“显示逻辑”解耦后端只负责通信和转发前端专注UI交互。3. 环境准备与项目初始化请确保你的开发环境满足以下要求操作系统Windows 10/11本教程主要面向WindowsTauri也支持macOS和Linux。Node.js请安装最新的LTS版本如18.x或20.x。可在命令行中通过node -v和npm -v检查。RustTauri 依赖 Rust 工具链。安装最简单的方法是使用rustup访问 rustup.rs 按指引安装。安装后运行rustc --version和cargo --version验证。IDE推荐 Visual Studio Code并安装rust-analyzer和Volar(Vue) 扩展。接下来我们使用 Tauri 官方推荐的create-tauri-app脚手架快速初始化项目。# 打开命令行进入你的工作目录 npm create tauri-applatest my-desktop-alert在创建过程中命令行会交互式地让你选择配置项目名称直接回车使用my-desktop-alert或自定义。前端框架选择vue-ts使用Vue 3 和 TypeScript。包管理器选择npm。UI 模板选择none我们将自己构建UI保持最小化。创建完成后进入项目目录并安装依赖cd my-desktop-alert npm install此时项目结构大致如下my-desktop-alert/ ├── src/ │ ├── App.vue # 主Vue组件 │ ├── main.ts # 前端入口 │ └── ... ├── src-tauri/ # Rust后端代码目录 │ ├── Cargo.toml # Rust项目配置和依赖 │ ├── src/ │ │ └── main.rs # Rust程序入口 │ └── ... ├── index.html # 前端HTML入口 ├── package.json └── vite.config.ts # Vite配置运行npm run tauri dev来启动开发模式。如果一切顺利你会看到一个默认的Tauri窗口。至此基础环境搭建完成。4. 核心实现Rust后端HTTP服务Tauri 的强大之处在于可以轻松在 Rust 端执行任何操作。我们要在src-tauri/src/main.rs中启动一个HTTP服务器。首先需要添加必要的 Rust 依赖。打开src-tauri/Cargo.toml文件在[dependencies]部分添加[dependencies] tauri { version 2.0, features [api-all] } serde { version 1.0, features [derive] } serde_json 1.0 tokio { version 1.0, features [full] } # 异步运行时 warp 0.3 # 一个轻量级、高性能的Web框架我们将使用warp来创建HTTP服务器tokio作为异步运行时serde用于JSON序列化。接下来修改src-tauri/src/main.rs文件// src-tauri/src/main.rs #![cfg_attr(not(debug_assertions), windows_subsystem windows)] // 在发布版本中隐藏控制台窗口 use tauri::{Emitter, Manager}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; // 定义告警数据的结构体对应前端Vue组件需要的字段 #[derive(Debug, Clone, Serialize, Deserialize)] struct AlertMessage { id: String, // 唯一标识可以用时间戳或UUID title: String, // 告警标题如 服务器CPU告警 content: String, // 告警详情 level: String, // 级别info, warning, error time: String, // 接收时间格式化为字符串 } // 存储所有告警信件的共享状态 struct AppState { alerts: ArcMutexVecAlertMessage, } #[tauri::command] fn greet(name: str) - String { format!(Hello, {}! Youve been greeted from Rust!, name) } #[tokio::main] async fn main() { // 初始化共享状态 let app_state Arc::new(AppState { alerts: Arc::new(Mutex::new(Vec::new())), }); // 克隆状态用于HTTP服务器 let http_state app_state.clone(); // 启动HTTP服务器的异步任务 tauri::async_runtime::spawn(async move { start_http_server(http_state).await; }); tauri::Builder::default() .manage(app_state) // 将状态注入到Tauri应用中 .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect(error while running tauri application); } async fn start_http_server(state: ArcAppState) { use warp::Filter; // 定义 POST /alert 接口 let alerts state.alerts.clone(); let post_alert warp::post() .and(warp::path(alert)) .and(warp::body::json()) .map(move |new_alert: AlertMessage| { let alerts alerts.clone(); // 在实际应用中这里应该通过Tauri的AppHandle发射事件到前端 // 但为了示例简化我们先存入共享状态 // 注意在生产代码中你需要获取AppHandle并调用emit tauri::async_runtime::block_on(async { let mut guard alerts.lock().await; guard.push(new_alert.clone()); }); warp::reply::json(serde_json::json!({status: ok})) }); // 定义 GET /alerts 接口用于测试 let get_alerts warp::get() .and(warp::path(alerts)) .map(move || { let alerts state.alerts.clone(); let guard tauri::async_runtime::block_on(async { alerts.lock().await.clone() }); warp::reply::json(guard) }); let routes post_alert.or(get_alerts); println!(HTTP Server started on http://127.0.0.1:15555); warp::serve(routes).run(([127, 0, 0, 1], 15555)).await; }关键点解析AppState我们用一个结构体来存储所有告警信息并用ArcMutexT包装以实现多线程安全共享。start_http_server这个异步函数在Tauri应用启动时被调用。它使用warp创建了两个路由POST /alert接收外部系统发送的告警JSON数据。GET /alerts一个简单的测试接口用于查看当前存储的所有告警。端口服务器监听127.0.0.1:15555。你可以根据需要更改端口但要确保不与系统其他服务冲突。事件发射上面的示例代码将告警存入了共享状态但并未实时通知前端UI。要实现实时通知我们需要在post_alert处理器中获取Tauri的AppHandle并调用app.emit(new-alert, new_alert)。这需要更复杂的上下文传递我们将在下一节结合前端一起实现。5. 前端UI实现环世界风格的信件列表现在我们来构建前端的信件UI。修改src/App.vue文件。首先我们定义组件的类型和基础样式!-- src/App.vue -- script setup langts import { ref, onMounted, onUnmounted } from vue import { listen } from tauri-apps/api/event // 定义信件类型与Rust后端的AlertMessage对应 interface Letter { id: string title: string content: string level: info | warning | error time: string isNew?: boolean // 用于控制入场动画 } // 信件列表 const letters refLetter[]([ // 可以放一些初始示例数据 { id: 1, title: 系统启动, content: 桌面告警小工具已就绪。, level: info, time: new Date().toLocaleTimeString(), isNew: false } ]) // 监听来自Rust后端的事件 onMounted(async () { // 监听名为 “new-alert” 的事件事件负载就是新的Letter数据 const unlisten await listenLetter(new-alert, (event) { const newLetter { ...event.payload, isNew: true // 标记为新信件触发动画 } letters.value.unshift(newLetter) // 新信件添加到列表顶部 // 1.5秒后取消“新信件”状态动画结束 setTimeout(() { const index letters.value.findIndex(l l.id newLetter.id) if (index ! -1) { letters.value[index].isNew false } }, 1500) }) // 组件卸载时取消监听 onUnmounted(() { unlisten() }) }) // 关闭信件 const closeLetter (id: string) { const index letters.value.findIndex(l l.id id) if (index ! -1) { letters.value.splice(index, 1) } } /script template div classapp-container !-- 信件列表容器固定在桌面右上角 -- div classletters-container transition-group nameletter-list tagdiv div v-forletter in letters :keyletter.id classletter :class[letter.level, { new: letter.isNew }] clickcloseLetter(letter.id) div classletter-header span classletter-title{{ letter.title }}/span span classletter-time{{ letter.time }}/span /div div classletter-content{{ letter.content }}/div div classletter-footer span classletter-level{{ letter.level.toUpperCase() }}/span button classclose-btn click.stopcloseLetter(letter.id)×/button /div /div /transition-group /div /div /template style scoped .app-container { font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; user-select: none; /* 防止文字被选中 */ } .letters-container { position: fixed; top: 20px; right: 20px; width: 320px; max-height: 90vh; overflow-y: auto; z-index: 99999; /* 确保在最顶层 */ pointer-events: auto; } .letter { background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%); border-left: 4px solid #4299e1; border-radius: 8px; padding: 16px; margin-bottom: 12px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); color: #e2e8f0; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(10px); /* 毛玻璃效果 */ } .letter:hover { transform: translateY(-2px); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.1); } .letter.info { border-left-color: #4299e1; /* 蓝色 */ } .letter.warning { border-left-color: #ed8936; /* 橙色 */ } .letter.error { border-left-color: #f56565; /* 红色 */ } .letter.new { animation: slideInRight 0.5s ease-out; } keyframes slideInRight { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } .letter-list-move { transition: transform 0.5s ease; } .letter-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .letter-title { font-weight: bold; font-size: 1.1em; color: white; } .letter-time { font-size: 0.85em; color: #a0aec0; } .letter-content { margin-bottom: 12px; line-height: 1.5; color: #cbd5e0; } .letter-footer { display: flex; justify-content: space-between; align-items: center; } .letter-level { font-size: 0.75em; padding: 2px 8px; border-radius: 10px; background-color: rgba(255, 255, 255, 0.1); } .close-btn { background: none; border: none; color: #a0aec0; font-size: 1.5em; cursor: pointer; line-height: 1; padding: 0 4px; border-radius: 4px; } .close-btn:hover { color: #f56565; background-color: rgba(255, 255, 255, 0.1); } /style这个前端组件实现了固定位置信件容器固定在桌面右上角。级别区分通过border-left-color区分info蓝、warning橙、error红。入场动画新信件通过slideInRight动画从右侧滑入。交互点击信件任意区域或右上角的关闭按钮可以关闭移除该信件。毛玻璃效果使用backdrop-filter: blur(10px)实现亚克力质感背景需系统支持。6. 打通前后端实时事件通信现在我们需要修改Rust后端使其在收到HTTP请求时能主动通知前端更新。这需要用到Tauri的AppHandle。首先更新src-tauri/src/main.rs将start_http_server函数修改为接收AppHandle// 在 main.rs 顶部引入 use tauri::AppHandle; // 修改 start_http_server 函数签名和实现 async fn start_http_server(state: ArcAppState, app_handle: AppHandle) { use warp::Filter; let post_alert warp::post() .and(warp::path(alert)) .and(warp::body::json()) .map(move |new_alert: AlertMessage| { let app_handle app_handle.clone(); // 发射事件到前端 let _ app_handle.emit(new-alert, new_alert); // 使用 emit 而非 emit_all // 也可以选择存入共享状态以备后用 // let alerts state.alerts.clone(); // tauri::async_runtime::block_on(async { // let mut guard alerts.lock().await; // guard.push(new_alert.clone()); // }); warp::reply::json(serde_json::json!({status: ok})) }); // ... 其余代码如GET路由保持不变 ... let get_alerts warp::get()...; // 保留用于调试 let routes post_alert.or(get_alerts); println!(HTTP Server started on http://127.0.0.1:15555); warp::serve(routes).run(([127, 0, 0, 1], 15555)).await; }然后在main函数中我们需要在Tauri应用构建完成后再启动HTTP服务器以便能获取到AppHandle#[tokio::main] async fn main() { let app_state Arc::new(AppState { alerts: Arc::new(Mutex::new(Vec::new())), }); tauri::Builder::default() .manage(app_state.clone()) .setup(|app| { // 在setup钩子中获取app.handle()的克隆 let app_handle app.handle().clone(); let state_for_http app_state.clone(); // 启动HTTP服务器 tauri::async_runtime::spawn(async move { start_http_server(state_for_http, app_handle).await; }); Ok(()) }) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect(error while running tauri application); }关键改动setup钩子Tauri的setup钩子在应用初始化时运行在这里我们可以拿到AppHandle。app_handle.emit这是核心。当HTTP服务器收到POST /alert请求时它使用app_handle向前端发射一个名为new-alert的事件并携带告警数据。前端监听前端Vue组件已经在onMounted中通过listen(new-alert, ...)监听了此事件。一旦Rust端发射事件前端就会收到并更新UI。至此从外部HTTP请求到前端桌面显示的完整链路已经打通。7. 配置Tauri窗口实现真正的“桌面小工具”默认的Tauri窗口带有标题栏和边框这不符合我们“桌面小工具”的定位。我们需要一个无边框、透明背景、始终置顶且能穿透点击的窗口。修改src-tauri/tauri.conf.json文件如果不存在请检查tauri.conf.json是否在根目录或参考官方文档{ build: { beforeDevCommand: npm run dev, beforeBuildCommand: npm run build, devPath: http://localhost:1420, distDir: ../dist }, package: { productName: Desktop Alert, version: 0.1.0 }, tauri: { allowlist: { all: false, window: { all: true } }, bundle: { active: true, targets: all, identifier: com.example.desktopalert, icon: [ icons/32x32.png, icons/128x128.png, icons/128x1282x.png, icons/icon.icns, icons/icon.ico ] }, security: { csp: null }, windows: [ { title: Desktop Alert, width: 400, height: 600, resizable: false, fullscreen: false, decorations: false, // 关键去掉窗口边框和标题栏 transparent: true, // 关键窗口背景透明 alwaysOnTop: true, // 关键窗口始终置顶 skipTaskbar: true, // 可选不在任务栏显示 focus: false, // 启动时不抢占焦点 visibleOnAllWorkspaces: true // 在所有虚拟桌面显示 } ] } }关键配置解析decorations: false移除窗口标题栏、边框和系统菜单。transparent: true使窗口背景透明。这样我们Vue组件中信件区域的背景如毛玻璃效果才能正确显示而不是显示一个不透明的窗口背景。alwaysOnTop: true确保告警窗口始终显示在其他窗口之上。skipTaskbar: true让应用不出现在任务栏更像一个后台小工具。focus: false启动时不会抢走你当前窗口的焦点非常友好。为了让透明背景生效我们还需要微调前端样式。在src/App.vue的style部分为根元素添加/* 确保应用背景透明 */ .app-container { /* ... 其他样式 ... */ background: transparent !important; } /* 如果发现窗口有默认背景色可以强制覆盖 */ body, html { background: transparent; margin: 0; padding: 0; overflow: hidden; /* 防止出现滚动条 */ }8. 测试与使用发送你的第一条告警现在让我们来测试整个流程。启动应用在项目根目录运行npm run tauri dev。你应该能看到一个无边框、透明的窗口出现在桌面右上角里面有一条初始的“系统启动”信件。发送测试告警我们可以使用任何能发送HTTP POST请求的工具。这里用curlWindows 10/11 自带或可通过Git Bash获取和 Python 脚本两种方式。方法一使用curl(命令行)curl -X POST http://127.0.0.1:15555/alert \ -H Content-Type: application/json \ -d { \id\: \$(date %s)\, \title\: \数据库连接异常\, \content\: \主数据库连接池耗尽当前活跃连接数95/100。\, \level\: \error\, \time\: \$(date %H:%M:%S)\ }方法二使用 Python 脚本创建一个send_alert.py文件import requests import json import time url http://127.0.0.1:15555/alert alert_data { id: str(int(time.time())), title: CI/CD Pipeline Failed, content: Build #123 for project my-app failed at unit test stage. Check Jenkins for details., level: warning, time: time.strftime(%H:%M:%S) } response requests.post(url, jsonalert_data) print(fStatus Code: {response.status_code}) print(fResponse: {response.text})运行python send_alert.py。执行上述任一命令后你应该立刻能在桌面小工具的窗口里看到一条新的、带有对应颜色和动画的信件滑入。9. 常见问题与排查思路在开发和运行过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案应用启动失败提示 Rust 编译错误Rust 工具链未安装或版本不匹配依赖下载失败。1. 运行rustc --version和cargo --version确认安装。2. 查看命令行错误信息通常是Cargo.toml中的依赖项问题。1. 重新安装 Rust (rustup update)。2. 检查网络尝试cargo build单独编译后端。3. 清除缓存cargo clean后重试。HTTP 请求发送成功但前端无反应1. 端口被占用或HTTP服务器未启动。2. Tauri 事件名不匹配。3. 前端监听代码未生效。1. 检查应用启动日志确认HTTP Server started on ...出现。2. 用浏览器或curl访问GET http://127.0.0.1:15555/alerts看是否有数据。3. 在 Rust 后端emit前后加日志在前端listen回调里加console.log。1. 更换端口如15556。2. 确保前后端事件名完全一致大小写敏感。3. 检查前端组件是否已正确挂载 (onMounted)。窗口有白色背景不是透明1.tauri.conf.json中transparent: true未生效。2. 前端CSS有背景色覆盖。1. 确认配置文件已保存且重启了开发服务器。2. 在浏览器开发者工具中检查body和根元素的background样式。1. 确保使用的是tauri dev而不是直接打开index.html。2. 在前端样式表中强制设置background: transparent !important;。窗口无法拖动无边框 (decorations: false) 后默认失去了拖动能力。点击窗口任何地方都无法移动。在前端为可拖动区域如信件容器的顶部添加CSS样式-webkit-app-region: drag;。注意按钮等交互元素需设置为-webkit-app-region: no-drag;。打包后的应用无法启动或报错1. 依赖缺失。2. 防病毒软件误报。3. 路径或资源问题。1. 在命令行中运行生成的.exe查看具体错误。2. 检查tauri.conf.json中的bundle和icon配置。1. 使用npm run tauri build重新打包。2. 将应用安装目录添加到杀毒软件白名单。3. 确保图标文件路径正确。10. 生产环境进阶安全、打包与自启动1. API 安全加固当前HTTP服务器监听在127.0.0.1只能本机访问这提供了基础安全。但如果你需要从局域网其他机器发送告警则必须考虑身份验证最简单的方案是在HTTP请求头中添加一个密钥Token。修改Rust后端在post_alert路由中检查Authorization头。修改所有发送告警的客户端在请求中携带该Token。HTTPS对于公网环境务必使用HTTPS。Tauri本身不直接提供HTTPS服务器你可以考虑使用反向代理如Nginx为本地15555端口提供HTTPS终结。或者将HTTP服务器升级为HTTPS使用warp的Tls功能但这需要管理证书。2. 应用打包与分发使用npm run tauri build命令进行打包。这会在src-tauri/target/release目录下生成.exe安装包或可执行文件。减小体积Tauri应用本身已经很小通常几MB到十几MB。确保在Cargo.toml中启用Rust的编译优化release模式默认启用。代码签名对于Windows为.exe进行代码签名可以避免系统SmartScreen警告。但这需要购买代码签名证书。3. 开机自启动让工具在开机后自动运行对于监控告警场景是必要的。Tauri 插件社区有tauri-plugin-autostart插件可以方便地管理自启动。手动配置也可以将打包好的快捷方式放入系统的启动文件夹shell:startup。4. 日志与持久化当前信件存储在内存中应用关闭后即丢失。对于需要历史记录的场景你需要前端持久化使用localStorage或IndexedDB在浏览器侧存储信件。后端持久化在Rust端使用sqlite或sled等嵌入式数据库存储告警记录并提供查询接口。这个模仿《环世界》信件系统的桌面告警小工具从创意到实现完整地展示了一个现代桌面应用的开发流程。它融合了Rust的高效后端、Vue 3的响应式前端以及Tauri的精致封装。你收获的不仅仅是一个工具更是一套将Web技术应用于特定桌面场景的解决方案。你可以基于此代码轻松地修改UI主题、增加声音提示、支持更多消息格式如Markdown甚至将其改造成一个团队内部的简易通知中心。项目的完整源代码已开源在 GitHubhttps://github.com/mewamew/my_ai_town根据输入材料此链接为相关项目可作参考。建议你将代码克隆到本地运行起来并尝试发送几条告警亲身体验一下这种“优雅的打扰”。如果在实践过程中遇到任何问题或有了更酷的改进想法欢迎在评论区分享交流。
返回列表