diff --git a/package.json b/package.json index 06f72089..73471210 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.3.2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-shell": "^2.3.0", "@vueuse/core": "^13.6.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d0e21e90..79a0fbe8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,12 +7,14 @@ name = "CodeForge" version = "25.0.0" dependencies = [ "chrono", + "dirs", "fern", "log", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-opener", "tauri-plugin-shell", "tempfile", @@ -80,6 +82,27 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -763,6 +786,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", + "libc", "objc2 0.6.1", ] @@ -777,6 +802,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -800,6 +834,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -2667,7 +2707,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", "indexmap 2.10.0", - "quick-xml", + "quick-xml 0.38.1", "serde", "time", ] @@ -2796,6 +2836,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.1" @@ -2845,6 +2894,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2865,6 +2924,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2883,6 +2952,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3011,6 +3089,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -3111,6 +3214,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3744,6 +3853,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml 0.8.23", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.4.0" @@ -4001,8 +4150,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tracing", "windows-sys 0.59.0", ] @@ -4505,6 +4656,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -5130,6 +5341,7 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", @@ -5249,6 +5461,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 0.7.12", "zvariant_derive", "zvariant_utils", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ac607920..7cfd1379 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,6 +17,7 @@ chrono = { version = "0.4.41", features = ["serde"] } tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-shell = "2.0" +tauri-plugin-dialog = "2.0" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = "1.47.1" @@ -25,3 +26,4 @@ tempfile = "3.0" chrono = { version = "0.4", features = ["serde"] } log = "0.4" fern = "0.7.1" +dirs = "6.0.0" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 47bb51db..19d0ccea 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,7 +8,16 @@ "permissions": [ "core:default", "opener:default", + { + "identifier": "opener:allow-open-path", + "allow": [ + { + "path": "**" + } + ] + }, + "shell:default", "shell:allow-open", - "shell:default" + "dialog:default" ] } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs new file mode 100644 index 00000000..72e290a0 --- /dev/null +++ b/src-tauri/src/config.rs @@ -0,0 +1,160 @@ +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppConfig { + pub log_directory: Option, + pub auto_clear_logs: Option, + pub keep_log_days: Option, + pub theme: Option, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + log_directory: None, + auto_clear_logs: Some(true), + keep_log_days: Some(30), + theme: Some("system".to_string()), + } + } +} + +pub struct ConfigManager { + config_path: PathBuf, + config: AppConfig, +} + +impl ConfigManager { + pub fn new() -> Result { + let config_path = Self::get_config_path()?; + let config = Self::load_config(&config_path)?; + + Ok(Self { + config_path, + config, + }) + } + + fn get_config_path() -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?; + + let config_dir = home_dir.join(".codeforge"); + let config_file = config_dir.join("config.json"); + + // 确保配置目录存在 + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(|e| format!("创建配置目录失败: {}", e))?; + } + + Ok(config_file) + } + + fn load_config(config_path: &PathBuf) -> Result { + if config_path.exists() { + match fs::read_to_string(config_path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(config) => { + info!("读取配置 -> 成功加载配置文件: {:?}", config_path); + Ok(config) + } + Err(e) => { + warn!("读取配置 -> 配置文件格式错误,使用默认配置: {}", e); + Ok(AppConfig::default()) + } + }, + Err(e) => { + warn!("读取配置 -> 读取配置文件失败,使用默认配置: {}", e); + Ok(AppConfig::default()) + } + } + } else { + info!("读取配置 -> 配置文件不存在,使用默认配置"); + Ok(AppConfig::default()) + } + } + + pub fn save_config(&self) -> Result<(), String> { + let content = serde_json::to_string_pretty(&self.config) + .map_err(|e| format!("序列化配置失败: {}", e))?; + + fs::write(&self.config_path, content).map_err(|e| format!("写入配置文件失败: {}", e))?; + + info!("保存配置 -> 配置文件已保存: {:?}", self.config_path); + Ok(()) + } + + pub fn get_config(&self) -> &AppConfig { + &self.config + } + + pub fn get_log_directory(&self) -> Option<&str> { + self.config.log_directory.as_deref() + } + + pub fn set_log_directory(&mut self, path: Option) -> Result<(), String> { + self.config.log_directory = path; + self.save_config() + } +} + +// 全局配置管理器 +use std::sync::Mutex; +static CONFIG_MANAGER: Mutex> = Mutex::new(None); + +// 初始化配置 +pub fn init_config() -> Result<(), String> { + let config_manager = ConfigManager::new()?; + + // 如果配置中有自定义日志目录,设置到日志系统 + if let Some(log_dir) = config_manager.get_log_directory() { + info!("初始化 -> 从配置文件加载日志目录: {}", log_dir); + // 使用内部函数设置,避免循环保存 + if let Err(e) = crate::logger::set_log_directory_internal(log_dir.to_string()) { + warn!("初始化 -> 应用配置中的日志目录失败: {}", e); + } + } + + let mut guard = CONFIG_MANAGER.lock().unwrap(); + *guard = Some(config_manager); + + Ok(()) +} + +pub fn get_config_manager() -> Result>, String> +{ + CONFIG_MANAGER + .lock() + .map_err(|e| format!("获取配置管理器失败: {}", e)) +} + +// Tauri 命令 +use tauri::command; + +#[command] +pub async fn get_app_config() -> Result { + let guard = get_config_manager()?; + if let Some(config_manager) = guard.as_ref() { + Ok(config_manager.get_config().clone()) + } else { + Err("配置管理器未初始化".to_string()) + } +} + +#[command] +pub async fn update_app_config(config: AppConfig) -> Result<(), String> { + let mut guard = get_config_manager()?; + if let Some(config_manager) = guard.as_mut() { + config_manager.config = config; + config_manager.save_config() + } else { + Err("配置管理器未初始化".to_string()) + } +} + +#[command] +pub async fn get_config_path() -> Result { + ConfigManager::get_config_path().map(|path| path.to_string_lossy().to_string()) +} diff --git a/src-tauri/src/logger.rs b/src-tauri/src/logger.rs index cdbac232..222ea406 100644 --- a/src-tauri/src/logger.rs +++ b/src-tauri/src/logger.rs @@ -1,18 +1,33 @@ use chrono::Local; -use log::LevelFilter; +use log::{LevelFilter, info, warn}; use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; use tauri::{AppHandle, Manager}; +static LOG_DIRECTORY: Mutex> = Mutex::new(None); + pub fn setup_logger(app: &AppHandle) -> Result<(), fern::InitError> { - // 获取应用数据目录 - let app_data_dir = app - .path() - .app_data_dir() - .expect("Failed to get app data dir"); + // 获取日志目录(可能是自定义的,也可能是默认的) + let log_dir = get_effective_log_directory(app); // 创建日志目录 - let log_dir = app_data_dir.join("logs"); - fs::create_dir_all(&log_dir).expect("Failed to create log directory"); + if let Err(e) = fs::create_dir_all(&log_dir) { + eprintln!("Failed to create log directory: {}", e); + // 如果自定义目录创建失败,回退到默认目录 + let default_dir = get_default_log_directory(app); + fs::create_dir_all(&default_dir).expect("Failed to create default log directory"); + + // 更新为默认目录 + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = None; // 清除自定义设置 + } + + warn!("日志目录创建失败,使用默认目录: {:?}", default_dir); + } else { + info!("日志目录: {:?}", log_dir); + } // 生成当天的日志文件名 let today = Local::now().format("%Y-%m-%d").to_string(); @@ -33,12 +48,81 @@ pub fn setup_logger(app: &AppHandle) -> Result<(), fern::InitError> { .level(LevelFilter::Debug) // 设置日志级别 .level_for("hyper", LevelFilter::Warn) // 减少第三方库的日志 .level_for("reqwest", LevelFilter::Warn) + .level_for("tauri", LevelFilter::Info) // Tauri 框架日志 .chain(std::io::stdout()) // 同时输出到控制台 .chain(fern::log_file(&log_file)?) // 输出到文件 .apply()?; - log::info!("日志系统初始化完成"); - log::info!("日志文件: {:?}", log_file); + info!("CodeForge 应用启动"); + info!("应用版本: {}", env!("CARGO_PKG_VERSION")); + info!("日志文件: {:?}", log_file); + + Ok(()) +} + +// 获取有效的日志目录(考虑自定义设置) +fn get_effective_log_directory(app: &AppHandle) -> PathBuf { + // 首先检查是否有自定义日志目录 + let custom_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + guard.clone() + }; + + match custom_dir { + Some(dir) => { + info!("使用自定义日志目录: {:?}", dir); + dir + } + None => { + let default_dir = get_default_log_directory(app); + info!("使用默认日志目录: {:?}", default_dir); + default_dir + } + } +} + +// 获取默认日志目录 +fn get_default_log_directory(app: &AppHandle) -> PathBuf { + app.path() + .app_data_dir() + .expect("Failed to get app data dir") + .join("logs") +} + +// 公共函数,供其他模块调用 +pub fn get_log_directory(app: &AppHandle) -> PathBuf { + get_effective_log_directory(app) +} + +// 内部函数,用于配置系统调用(不保存到配置文件) +pub fn set_log_directory_internal(path: String) -> Result<(), String> { + let new_path = PathBuf::from(&path); + info!("设置日志 -> 内部设置日志目录为: {}", path); + + // 验证目录是否存在,如果不存在则创建 + if !new_path.exists() { + std::fs::create_dir_all(&new_path).map_err(|e| format!("无法创建目录: {}", e))?; + } + + // 验证是否为目录 + if !new_path.is_dir() { + return Err("指定的路径不是目录".to_string()); + } + + // 更新全局日志目录 + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = Some(new_path); + } + + info!("设置日志 -> 日志目录已内部设置为: {}", path); + Ok(()) +} +// 重新初始化日志系统(当日志目录改变时调用) +pub fn reinit_logger(app: &AppHandle) -> Result<(), String> { + // 重新设置日志系统 + setup_logger(app).map_err(|e| format!("重新初始化日志系统失败: {}", e))?; + info!("日志系统已重新初始化"); Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0278e46f..87b84fb1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,11 +3,18 @@ windows_subsystem = "windows" )] +mod config; mod logger; mod plugins; mod setup; +mod utils; use crate::setup::app::get_app_info; +use crate::utils::logger::{ + clear_logs, get_log_directory, get_log_files, reset_log_directory, set_log_directory, +}; +use config::{get_app_config, get_config_path, init_config, update_app_config}; + use log::{debug, info}; use plugins::{CodeExecutionRequest, ExecutionResult, LanguageInfo, PluginManager}; use std::fs; @@ -220,16 +227,23 @@ async fn clear_execution_history(history: State<'_, ExecutionHistory>) -> Result fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) .manage(ExecutionHistory::default()) .manage(PluginManagerState::new(PluginManager::new())) .setup(|app| { - // 初始化日志系统 + // 第一步:初始化配置系统 + if let Err(e) = init_config() { + eprintln!("Failed to initialize config: {}", e); + } + + // 第二步:初始化日志系统 if let Err(e) = logger::setup_logger(app.handle()) { eprintln!("Failed to setup logger: {}", e); } - info!("CodeForge 应用启动"); - info!("应用版本: {}", env!("CARGO_PKG_VERSION")); + // 初始化应用菜单 + info!("初始化 -> 初始化应用菜单"); let menu = setup::menu::create_menu(app.handle())?; app.set_menu(menu)?; setup::menu::setup_menu_handler(app.handle()); @@ -241,7 +255,17 @@ fn main() { get_supported_languages, get_execution_history, clear_execution_history, - get_app_info + get_app_info, + // 日志相关命令 + get_log_directory, + set_log_directory, + reset_log_directory, + get_log_files, + clear_logs, + // 配置相关命令 + get_app_config, + update_app_config, + get_config_path ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/setup/menus/app.rs b/src-tauri/src/setup/menus/app.rs index e1524c00..3a7c31be 100644 --- a/src-tauri/src/setup/menus/app.rs +++ b/src-tauri/src/setup/menus/app.rs @@ -13,9 +13,16 @@ pub fn create_app_submenu(app: &AppHandle) -> tauri::Result> .accelerator("CmdOrCtrl+Q") .build(app)?; + let settings_item = MenuItemBuilder::new("设置") + .id("settings") + .accelerator("CmdOrCtrl+,") + .build(app)?; + let app_submenu = SubmenuBuilder::new(app, "CodeForge") .item(&about_item) .separator() + .item(&settings_item) + .separator() .item(&quit_item) .build()?; @@ -27,6 +34,9 @@ pub fn handle_app_menu_event(app: &AppHandle, event_id: &str) { "about" => { let _event = app.emit("show-about", ()); } + "settings" => { + let _event = app.emit("show-settings", ()); + } "quit" => { app.exit(0); } diff --git a/src-tauri/src/utils/logger.rs b/src-tauri/src/utils/logger.rs new file mode 100644 index 00000000..8888d704 --- /dev/null +++ b/src-tauri/src/utils/logger.rs @@ -0,0 +1,182 @@ +use log::{error, info, warn}; +use std::path::PathBuf; +use std::sync::Mutex; +use tauri::{AppHandle, command}; + +// 全局日志目录状态 +static LOG_DIRECTORY: Mutex> = Mutex::new(None); + +// 获取日志目录 +#[command] +pub async fn get_log_directory(app: AppHandle) -> Result { + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + guard.clone() + }; + + let dir = match log_dir { + Some(dir) => dir, + None => crate::logger::get_log_directory(&app), + }; + + Ok(dir.to_string_lossy().to_string()) +} + +// 设置日志目录 +#[command] +pub async fn set_log_directory(app: AppHandle, path: String) -> Result<(), String> { + let new_path = PathBuf::from(&path); + info!("设置日志 -> 用户设置日志目录为: {}", path); + + // 验证目录是否存在,如果不存在则创建 + if !new_path.exists() { + std::fs::create_dir_all(&new_path).map_err(|e| format!("无法创建目录: {}", e))?; + } + + // 验证是否为目录 + if !new_path.is_dir() { + error!("设置日志 -> 指定的路径不是目录"); + return Err("指定的路径不是目录".to_string()); + } + + // 更新全局日志目录 + { + info!("设置日志 -> 更新全局日志目录"); + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = Some(new_path); + } + + // 保存到配置文件 + if let Ok(mut guard) = crate::config::get_config_manager() { + if let Some(config_manager) = guard.as_mut() { + if let Err(e) = config_manager.set_log_directory(Some(path.clone())) { + warn!("设置日志 -> 保存配置失败: {}", e); + } else { + info!("设置日志 -> 配置已保存到文件"); + } + } + } else { + warn!("设置日志 -> 无法获取配置管理器"); + } + + // 可选:重新初始化日志系统以立即使用新目录 + // 注意:这会导致当前日志文件切换,可能会丢失一些日志 + if let Err(e) = crate::logger::reinit_logger(&app) { + warn!("重新初始化日志系统失败: {}", e); + } + + info!("设置日志 -> 日志目录已设置为: {}", path); + Ok(()) +} + +// 重置日志目录 +#[command] +pub async fn reset_log_directory(_app: AppHandle) -> Result<(), String> { + { + let mut guard = LOG_DIRECTORY.lock().unwrap(); + *guard = None; + } + + // 从配置文件中移除自定义日志目录 + if let Ok(mut guard) = crate::config::get_config_manager() { + if let Some(config_manager) = guard.as_mut() { + if let Err(e) = config_manager.set_log_directory(None) { + warn!("重置日志 -> 保存配置失败: {}", e); + } else { + info!("重置日志 -> 配置已保存到文件"); + } + } + } else { + warn!("重置日志 -> 无法获取配置管理器"); + } + + info!("重置日志 -> 重置日志目录为默认目录"); + Ok(()) +} + +// 获取日志文件列表 +#[command] +pub async fn get_log_files(app: AppHandle) -> Result, String> { + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + match &*guard { + Some(dir) => dir.clone(), + None => crate::logger::get_log_directory(&app), + } + }; + info!("获取日志 -> 日志目录为: {}", log_dir.display()); + + let mut log_files = Vec::new(); + + if let Ok(entries) = std::fs::read_dir(&log_dir) { + for entry in entries.flatten() { + if let Some(filename) = entry.file_name().to_str() { + if filename.ends_with(".log") && filename.starts_with("codeforge-") { + log_files.push(filename.to_string()); + info!("获取日志 -> 发现日志文件: {}", filename); + } + } + } + } + + info!("获取日志 -> 找到 {} 个日志文件", log_files.len()); + log_files.sort(); + log_files.reverse(); // 最新的在前面 + + Ok(log_files) +} + +// 清除日志 +#[command] +pub async fn clear_logs(app: AppHandle, keep_days: u32) -> Result { + use chrono::{Duration, Local}; + + let log_dir = { + let guard = LOG_DIRECTORY.lock().unwrap(); + match &*guard { + Some(dir) => dir.clone(), + None => crate::logger::get_log_directory(&app), + } + }; + info!("清理日志 -> 日志目录为: {}", log_dir.display()); + + let cutoff_date = Local::now() - Duration::days(keep_days as i64); + let mut deleted_count = 0; + let mut scanned_count = 0; + + if let Ok(entries) = std::fs::read_dir(&log_dir) { + for entry in entries.flatten() { + if let Some(filename) = entry.file_name().to_str() { + if filename.ends_with(".log") && filename.starts_with("codeforge-") { + scanned_count += 1; + // 从文件名提取日期 codeforge-2024-08-09.log + if let Some(date_str) = filename + .strip_prefix("codeforge-") + .and_then(|s| s.strip_suffix(".log")) + { + if let Ok(file_date) = + chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") + { + if file_date < cutoff_date.date_naive() { + if let Err(e) = std::fs::remove_file(entry.path()) { + warn!("清理日志 -> 删除日志文件失败 {}: {}", filename, e); + } else { + info!("清理日志 -> 已删除日志文件: {}", filename); + deleted_count += 1; + } + } else { + info!("清理日志 -> 日志文件文件未到期,将被保留: {}", filename); + } + } + } + } + } + } + } + info!( + "清理日志 -> 扫描 {} 个日志文件,删除 {} 个", + scanned_count, deleted_count + ); + + Ok(deleted_count) +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs new file mode 100644 index 00000000..d9917284 --- /dev/null +++ b/src-tauri/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod logger; diff --git a/src/App.vue b/src/App.vue index fa12e08c..289daf13 100644 --- a/src/App.vue +++ b/src/App.vue @@ -51,6 +51,9 @@ + + + @@ -64,6 +67,7 @@ import OutputPanel from './components/OutputPanel.vue' import StatusBar from './components/StatusBar.vue' import Toast from './components/Toast.vue' import About from './components/About.vue' +import Settings from './components/Settings.vue' interface ExecutionResult { @@ -158,15 +162,20 @@ const isRunning = ref(false) const isSuccess = ref(false) const lastExecutionTime = ref(0) const activeTab = ref('output') -const showSettings = ref(false) const supportedLanguages = ref([]) const showAbout = ref(false) -let unlistenFn: UnlistenFn | null = null +let unlistenAboutFn: UnlistenFn | null = null +const showSettings = ref(false) +let unlistenSettingsFn: UnlistenFn | null = null const closeAbout = () => { showAbout.value = false } +const closeSettings = () => { + showSettings.value = false +} + const envInfo = ref({ installed: false, version: '检查中...', @@ -312,14 +321,23 @@ onMounted(async () => { } // 监听来自 Rust 的 show-about 事件 - unlistenFn = await listen('show-about', () => { + unlistenAboutFn = await listen('show-about', () => { showAbout.value = true }) + + // 监听来自 Rust 的 show-settings 事件 + unlistenSettingsFn = await listen('show-settings', () => { + showSettings.value = true + }) }) onUnmounted(() => { - if (unlistenFn) { - unlistenFn() + if (unlistenAboutFn) { + unlistenAboutFn() + } + + if (unlistenSettingsFn) { + unlistenSettingsFn() } }) \ No newline at end of file diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index bbb8a000..f6ee65a7 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -4,18 +4,14 @@
- +
@@ -45,6 +41,7 @@ diff --git a/src/ui/Select.vue b/src/ui/Select.vue new file mode 100644 index 00000000..3762827f --- /dev/null +++ b/src/ui/Select.vue @@ -0,0 +1,297 @@ + + +