feat: 添加 Tauri 桌面应用

- 创建 packages/desktop 模块
- 实现 Tauri 2.0 + React 桌面应用
- 复用 Web 前端代码
- 添加系统托盘功能
- 实现本地文件访问命令
- 配置 Vite + Tauri 集成
- 更新 .gitignore 添加 Rust/Tauri 相关规则
This commit is contained in:
2025-12-12 13:19:00 +08:00
parent b5d3b7df57
commit 6ef9d95172
86 changed files with 14495 additions and 1 deletions
+118
View File
@@ -0,0 +1,118 @@
use serde::Serialize;
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc;
use tauri_plugin_dialog::DialogExt;
#[derive(Serialize)]
pub struct AppInfo {
pub name: String,
pub version: String,
pub tauri_version: String,
}
#[tauri::command]
pub fn get_app_info() -> AppInfo {
AppInfo {
name: "AI Assistant".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
tauri_version: tauri::VERSION.to_string(),
}
}
#[tauri::command]
pub async fn open_directory_dialog(app: tauri::AppHandle) -> Result<Option<String>, String> {
let (tx, rx) = mpsc::channel();
app.dialog()
.file()
.set_title("Select Working Directory")
.pick_folder(move |path| {
let _ = tx.send(path);
});
match rx.recv() {
Ok(Some(path)) => Ok(Some(path.to_string())),
Ok(None) => Ok(None),
Err(e) => Err(e.to_string()),
}
}
#[derive(Serialize)]
pub struct FileContent {
pub path: String,
pub content: String,
pub size: u64,
}
#[tauri::command]
pub async fn read_local_file(path: String) -> Result<FileContent, String> {
let path_buf = PathBuf::from(&path);
if !path_buf.exists() {
return Err(format!("File not found: {}", path));
}
let metadata = fs::metadata(&path_buf).map_err(|e| e.to_string())?;
if metadata.len() > 10 * 1024 * 1024 {
return Err("File too large (max 10MB)".to_string());
}
let content = fs::read_to_string(&path_buf).map_err(|e| e.to_string())?;
Ok(FileContent {
path,
content,
size: metadata.len(),
})
}
#[derive(Serialize)]
pub struct DirectoryEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
}
#[tauri::command]
pub async fn list_directory(path: String) -> Result<Vec<DirectoryEntry>, String> {
let path_buf = PathBuf::from(&path);
if !path_buf.exists() {
return Err(format!("Directory not found: {}", path));
}
if !path_buf.is_dir() {
return Err(format!("Not a directory: {}", path));
}
let mut entries = Vec::new();
let read_dir = fs::read_dir(&path_buf).map_err(|e| e.to_string())?;
for entry in read_dir {
let entry = entry.map_err(|e| e.to_string())?;
let metadata = entry.metadata().map_err(|e| e.to_string())?;
entries.push(DirectoryEntry {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
is_dir: metadata.is_dir(),
size: metadata.len(),
});
}
entries.sort_by(|a, b| {
if a.is_dir == b.is_dir {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
} else if a.is_dir {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
});
Ok(entries)
}
+66
View File
@@ -0,0 +1,66 @@
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
mod commands;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
setup_tray(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::get_app_info,
commands::open_directory_dialog,
commands::read_local_file,
commands::list_directory,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn setup_tray<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn std::error::Error>> {
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
let _tray = TrayIconBuilder::new()
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
ai_assistant_desktop_lib::run()
}