From 316f6a07b2a1a1b2f38caef4a15066a201b31b14 Mon Sep 17 00:00:00 2001 From: xiaji Date: Sat, 11 Apr 2026 08:16:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BE=8E=E5=8C=96=E7=95=8C=E9=9D=A2=20?= =?UTF-8?q?+=20=E6=B7=BB=E5=8A=A0=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=AF=BB=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui.rs | 335 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 233 insertions(+), 102 deletions(-) diff --git a/src/gui.rs b/src/gui.rs index 9fd6270..00d6dff 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,13 +1,89 @@ use eframe::{NativeOptions, run_native}; -use egui::{FontDefinitions, FontFamily, FontData}; +use egui::{FontDefinitions, FontFamily, FontData, Vec2}; use crate::api::ProxmoxClient; +use serde::{Deserialize, Serialize}; use std::sync::{Arc, RwLock, Mutex}; use dotenv::dotenv; use std::env; use std::thread; +use std::fs; +use std::path::PathBuf; pub type SharedState = Arc>; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub host: String, + pub port: u16, + pub token_id: String, + pub token_secret: String, + pub vm_id: u32, + pub node: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + host: String::new(), + port: 8006, + token_id: String::new(), + token_secret: String::new(), + vm_id: 100, + node: "proxmox".to_string(), + } + } +} + +impl Config { + fn path() -> PathBuf { + let mut path = dirs_config_path(); + path.push("proxmox-vm-gui.json"); + path + } + + fn load() -> Self { + let path = Self::path(); + if path.exists() { + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(config) = serde_json::from_str(&content) { + return config; + } + } + } + + // 尝试从环境变量加载 + dotenv().ok(); + Self { + host: env::var("PROXMOX_HOST").unwrap_or_default(), + port: env::var("PROXMOX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8006), + token_id: env::var("PROXMOX_TOKEN_ID").unwrap_or_default(), + token_secret: env::var("PROXMOX_TOKEN_SECRET").unwrap_or_default(), + vm_id: env::var("VM_ID").ok().and_then(|p| p.parse().ok()).unwrap_or(100), + node: env::var("NODE").unwrap_or_else(|_| "proxmox".to_string()), + } + } + + fn save(&self) { + let path = Self::path(); + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + if let Ok(content) = serde_json::to_string_pretty(self) { + let _ = fs::write(&path, content); + } + } +} + +fn dirs_config_path() -> PathBuf { + if let Some(data_dir) = env::var_os("APPDATA") { + PathBuf::from(data_dir) + } else if let Some(home) = env::var_os("USERPROFILE") { + PathBuf::from(home).join("AppData").join("Roaming") + } else { + PathBuf::from(".") + } +} + pub struct AppState { pub client: Arc>>, pub vm_id: u32, @@ -20,55 +96,33 @@ pub struct AppState { pub show_settings: bool, pub token_id: String, pub token_secret: String, - pub token_secret_shown: bool, } impl AppState { pub fn new() -> Self { - dotenv().ok(); + let config = Config::load(); - let host = env::var("PROXMOX_HOST").unwrap_or_default(); - let token = env::var("PROXMOX_TOKEN").unwrap_or_default(); - let vm_id: u32 = env::var("VM_ID").unwrap_or_default().parse().unwrap_or(100); - let node = env::var("NODE").unwrap_or_else(|_| "proxmox".to_string()); - let token_id = env::var("PROXMOX_TOKEN_ID").unwrap_or_default(); - let token_secret = env::var("PROXMOX_TOKEN_SECRET").unwrap_or_default(); - let port: u16 = env::var("PROXMOX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8006); - - let (tok_id, tok_secret) = if !token_id.is_empty() && !token_secret.is_empty() { - (token_id.clone(), token_secret.clone()) - } else if !token.is_empty() { - if let Some(pos) = token.find('=') { - (token[..pos].to_string(), token[pos+1..].to_string()) - } else { - (token.clone(), token.clone()) - } - } else { - (String::new(), String::new()) - }; - - let client = if !host.is_empty() && !tok_id.is_empty() && !tok_secret.is_empty() { - let c = ProxmoxClient::new(&host, &tok_id, &tok_secret); + let client = if !config.host.is_empty() && !config.token_id.is_empty() && !config.token_secret.is_empty() { + let c = ProxmoxClient::new(&config.host, &config.token_id, &config.token_secret); Arc::new(Mutex::new(Some(c))) } else { Arc::new(Mutex::new(None)) }; - let is_connected = !host.is_empty() && !tok_id.is_empty() && !tok_secret.is_empty(); + let is_connected = !config.host.is_empty() && !config.token_id.is_empty() && !config.token_secret.is_empty(); Self { client, - vm_id, - node, + vm_id: config.vm_id, + node: config.node, vm_status: "未知".to_string(), log_buffer: vec!["程序启动".to_string()], is_connected, - host, - port, + host: config.host, + port: config.port, show_settings: false, - token_id: tok_id, - token_secret: tok_secret, - token_secret_shown: false, + token_id: config.token_id, + token_secret: config.token_secret, } } @@ -79,13 +133,25 @@ impl AppState { self.log_buffer.remove(0); } } + + pub fn save_config(&self) { + let config = Config { + host: self.host.clone(), + port: self.port, + token_id: self.token_id.clone(), + token_secret: self.token_secret.clone(), + vm_id: self.vm_id, + node: self.node.clone(), + }; + config.save(); + } } pub fn gui_run() { let options = NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([500.0, 450.0]) - .with_min_inner_size([400.0, 300.0]) + .with_inner_size([520.0, 480.0]) + .with_min_inner_size([450.0, 400.0]) .with_title("Proxmox VM 控制器"), ..Default::default() }; @@ -128,58 +194,73 @@ impl eframe::App for App { let ctx_clone = ctx.clone(); egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Proxmox VM 控制器"); + // 标题栏 + ui.horizontal(|ui| { + ui.heading("Proxmox VM 控制器"); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + if ui.button("⚙ 设置").clicked() { + state.write().unwrap().show_settings = true; + } + }); + }); + ui.add_space(8.0); ui.separator(); + ui.add_space(8.0); let mut st = state.write().unwrap(); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { - if ui.button("设置").clicked() { - st.show_settings = !st.show_settings; - } + // 状态信息卡片 + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label("连接状态:"); + let status_text = if st.is_connected { "● 已连接" } else { "○ 未连接" }; + let status_color = if st.is_connected { egui::Color32::GREEN } else { egui::Color32::RED }; + ui.colored_label(status_color, status_text); + }); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("VM ID:"); + ui.add(egui::DragValue::new(&mut st.vm_id).range(100..=999).speed(1)); + }); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("节点:"); + ui.text_edit_singleline(&mut st.node); + }); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("VM 状态:"); + ui.label(&st.vm_status); + }); + }); }); + ui.add_space(12.0); + + // 控制按钮 ui.horizontal(|ui| { - ui.label("连接状态: "); - ui.label(if st.is_connected { "● 已连接" } else { "○ 未连接" }); - }); + let btn_start = ui.add(egui::Button::new("▶ 启动").min_size(Vec2::new(80.0, 32.0))); + let btn_stop = ui.add(egui::Button::new("■ 停止").min_size(Vec2::new(80.0, 32.0))); + let btn_refresh = ui.add(egui::Button::new("↻ 刷新").min_size(Vec2::new(80.0, 32.0))); + + let vm_id_start = st.vm_id; + let node_start = st.node.clone(); + let client_start = st.client.clone(); + + let vm_id_stop = st.vm_id; + let node_stop = st.node.clone(); + let client_stop = st.client.clone(); + + let vm_id_refresh = st.vm_id; + let node_refresh = st.node.clone(); + let client_refresh = st.client.clone(); - ui.horizontal(|ui| { - ui.label("VM ID: "); - ui.add(egui::DragValue::new(&mut st.vm_id).range(100..=999)); - }); - - ui.horizontal(|ui| { - ui.label("节点: "); - ui.text_edit_singleline(&mut st.node); - }); - - ui.horizontal(|ui| { - ui.label("VM 状态: "); - ui.label(&st.vm_status); - }); - - ui.separator(); - - let vm_id_start = st.vm_id; - let node_start = st.node.clone(); - let client_start = st.client.clone(); - - let vm_id_stop = st.vm_id; - let node_stop = st.node.clone(); - let client_stop = st.client.clone(); - - let vm_id_refresh = st.vm_id; - let node_refresh = st.node.clone(); - let client_refresh = st.client.clone(); - - ui.horizontal(|ui| { - if ui.button("启动").clicked() { + if btn_start.clicked() { let client = client_start.clone(); let node = node_start.clone(); let vm_id = vm_id_start; let state = state.clone(); - let ctx = ctx_clone.clone(); st.add_log("正在启动 VM..."); ctx.request_repaint(); thread::spawn(move || { @@ -200,12 +281,11 @@ impl eframe::App for App { }); } - if ui.button("停止").clicked() { + if btn_stop.clicked() { let client = client_stop.clone(); let node = node_stop.clone(); let vm_id = vm_id_stop; let state = state.clone(); - let ctx = ctx_clone.clone(); st.add_log("正在停止 VM..."); ctx.request_repaint(); thread::spawn(move || { @@ -226,12 +306,11 @@ impl eframe::App for App { }); } - if ui.button("刷新").clicked() { + if btn_refresh.clicked() { let client = client_refresh.clone(); let node = node_refresh.clone(); let vm_id = vm_id_refresh; let state = state.clone(); - let ctx = ctx_clone.clone(); st.add_log("正在获取状态..."); ctx.request_repaint(); thread::spawn(move || { @@ -253,15 +332,23 @@ impl eframe::App for App { } }); + ui.add_space(12.0); ui.separator(); + ui.add_space(8.0); - ui.label("日志:"); - egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { - for log in &st.log_buffer { - ui.label(log); - } - }); + // 日志区域 + ui.label("📋 日志:"); + egui::Frame::default() + .inner_margin(4.0) + .show(ui, |ui| { + egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { + for log in &st.log_buffer { + ui.label(log); + } + }); + }); + // 设置窗口 let show_settings = st.show_settings; let mut host = st.host.clone(); let mut port = st.port; @@ -269,38 +356,82 @@ impl eframe::App for App { let token_id = st.token_id.clone(); let state_clone = state.clone(); + drop(st); + if show_settings { egui::Window::new("设置") .collapsible(false) .resizable(false) + .min_size(Vec2::new(350.0, 300.0)) + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { - ui.label("Base URL"); - ui.label(format!("https://{}:{}/api2/json/", host, port)); + ui.set_width(320.0); + + ui.heading("连接配置"); + ui.separator(); + ui.add_space(8.0); + + // Base URL 显示 + ui.label("API 地址:"); + ui.colored_label(egui::Color32::GRAY, format!("https://{}:{}/api2/json/", host, port)); + + ui.add_space(8.0); + + // Host 输入 ui.horizontal(|ui| { - ui.label("Host"); + ui.label("Host:"); ui.text_edit_singleline(&mut host); }); + + // Port 输入 ui.horizontal(|ui| { - ui.label("端口"); - ui.add(egui::DragValue::new(&mut port).range(1..=65535)); + ui.label("端口:"); + ui.add(egui::DragValue::new(&mut port).range(1..=65535).speed(1)); }); + + // Token ID 输入 ui.horizontal(|ui| { - ui.label("Token Secret"); + ui.label("Token ID:"); + ui.text_edit_singleline(&mut token_id.clone()); + }); + + // Token Secret 输入(密码框) + ui.horizontal(|ui| { + ui.label("Token Secret:"); ui.add(egui::TextEdit::singleline(&mut token_secret).password(true)); }); - if ui.button("应用设置").clicked() { - let ts = token_secret.clone(); - let tid = token_id.clone(); - if !host.is_empty() && !tid.is_empty() && !ts.is_empty() { - let client = ProxmoxClient::new(&host, &tid, &ts); - state_clone.write().unwrap().client = Arc::new(Mutex::new(Some(client))); - state_clone.write().unwrap().is_connected = true; - state_clone.write().unwrap().token_secret = ts; - state_clone.write().unwrap().add_log("已应用设置"); + + ui.add_space(12.0); + + // 按钮 + ui.horizontal(|ui| { + if ui.button("💾 保存").clicked() { + let ts = token_secret.clone(); + let tid = token_id.clone(); + if !host.is_empty() && !tid.is_empty() && !ts.is_empty() { + let mut state = state_clone.write().unwrap(); + state.host = host.clone(); + state.port = port; + state.token_id = tid.clone(); + state.token_secret = ts.clone(); + + let client = ProxmoxClient::new(&host, &tid, &ts); + state.client = Arc::new(Mutex::new(Some(client))); + state.is_connected = true; + + state.save_config(); + state.add_log("配置已保存"); + } } - } + + if ui.button("取消").clicked() { + state_clone.write().unwrap().show_settings = false; + } + }); + + ui.add_space(8.0); ui.separator(); - ui.label("提示: 修改后需点击应用设置以生效"); + ui.colored_label(egui::Color32::GRAY, "💡 提示: 修改配置后点击保存以生效"); }); } });