use eframe::{NativeOptions, run_native}; 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, pub node: String, pub vm_status: String, pub log_buffer: Vec, pub is_connected: bool, pub host: String, pub port: u16, pub show_settings: bool, pub token_id: String, pub token_secret: String, } impl AppState { pub fn new() -> Self { let config = Config::load(); let client = if !config.host.is_empty() && !config.token_id.is_empty() && !config.token_secret.is_empty() { let c = ProxmoxClient::new(&config.host, config.port, &config.token_id, &config.token_secret); Arc::new(Mutex::new(Some(c))) } else { Arc::new(Mutex::new(None)) }; let is_connected = !config.host.is_empty() && !config.token_id.is_empty() && !config.token_secret.is_empty(); Self { client, vm_id: config.vm_id, node: config.node, vm_status: "未知".to_string(), log_buffer: vec!["程序启动".to_string()], is_connected, host: config.host, port: config.port, show_settings: false, token_id: config.token_id, token_secret: config.token_secret, } } pub fn add_log(&mut self, msg: &str) { let time = chrono::Local::now().format("%H:%M:%S").to_string(); self.log_buffer.push(format!("{} {}", time, msg)); if self.log_buffer.len() > 100 { 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([520.0, 480.0]) .with_min_inner_size([450.0, 400.0]) .with_title("Proxmox VM 控制器"), ..Default::default() }; run_native("Proxmox VM Controller", options, Box::new(|cc| { let mut fonts = FontDefinitions::default(); fonts.font_data.insert( "my_font".to_owned(), FontData::from_static(include_bytes!(r"C:\Windows\Fonts\msyh.ttc")), ); fonts.families.get_mut(&FontFamily::Proportional) .unwrap() .insert(0, "my_font".to_owned()); fonts.families.get_mut(&FontFamily::Monospace) .unwrap() .insert(0, "my_font".to_owned()); cc.egui_ctx.set_fonts(fonts); Ok(Box::new(App::new())) })).unwrap(); } struct App { state: SharedState, } impl App { fn new() -> Self { Self { state: Arc::new(RwLock::new(AppState::new())), } } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let state = self.state.clone(); let ctx_clone = ctx.clone(); egui::CentralPanel::default().show(ctx, |ui| { // 标题栏 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(); // 状态信息卡片 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| { let btn_start = ui.add(egui::Button::new("▶ 启动").min_size(Vec2::new(70.0, 32.0))); let btn_stop = ui.add(egui::Button::new("■ 停止").min_size(Vec2::new(70.0, 32.0))); let btn_refresh = ui.add(egui::Button::new("↻ 刷新").min_size(Vec2::new(70.0, 32.0))); let btn_shutdown = ui.add(egui::Button::new("🔴 关机").min_size(Vec2::new(70.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(); let client_shutdown = st.client.clone(); let node_shutdown = st.node.clone(); let state_shutdown = state.clone(); if btn_start.clicked() { let client = client_start.clone(); let node = node_start.clone(); let vm_id = vm_id_start; let state = state.clone(); st.add_log("正在启动 VM..."); ctx.request_repaint(); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { let client = client.lock().unwrap(); if let Some(c) = client.as_ref() { c.start_vm(&node, vm_id).await } else { Err("未连接".to_string()) } }); let msg = match result { Ok(_) => "启动命令已发送".to_string(), Err(e) => format!("启动失败: {}", e), }; state.write().unwrap().add_log(&msg); }); } if btn_stop.clicked() { let client = client_stop.clone(); let node = node_stop.clone(); let vm_id = vm_id_stop; let state = state.clone(); st.add_log("正在停止 VM..."); ctx.request_repaint(); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { let client = client.lock().unwrap(); if let Some(c) = client.as_ref() { c.stop_vm(&node, vm_id).await } else { Err("未连接".to_string()) } }); let msg = match result { Ok(_) => "停止命令已发送".to_string(), Err(e) => format!("停止失败: {}", e), }; state.write().unwrap().add_log(&msg); }); } if btn_refresh.clicked() { let client = client_refresh.clone(); let node = node_refresh.clone(); let vm_id = vm_id_refresh; let state = state.clone(); st.add_log("正在获取状态..."); ctx.request_repaint(); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { let client = client.lock().unwrap(); if let Some(c) = client.as_ref() { c.get_vm_status(&node, vm_id).await } else { Err("未连接".to_string()) } }); let msg = match result { Ok(status) => format!("VM状态: {}", status), Err(e) => format!("获取状态失败: {}", e), }; state.write().unwrap().add_log(&msg); }); } if btn_shutdown.clicked() { let client = client_shutdown.clone(); let node = node_shutdown.clone(); let state = state_shutdown.clone(); st.add_log("正在执行关机流程..."); st.add_log("1. 停止所有虚拟机..."); ctx.request_repaint(); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); // 先停止所有运行的虚拟机 let _ = rt.block_on(async { // 获取节点上所有虚拟机 let client = client.lock().unwrap(); if let Some(c) = client.as_ref() { // 发送关闭节点命令 c.shutdown_node(&node).await.ok(); } }); state.write().unwrap().add_log("2. 正在关闭 Proxmox 主机..."); state.write().unwrap().add_log("关机命令已发送"); }); } }); ui.add_space(12.0); ui.separator(); ui.add_space(8.0); // 日志区域 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 state_clone = state.clone(); let mut st_host = st.host.clone(); let mut st_port = st.port; let mut st_token_id = st.token_id.clone(); let mut st_token_secret = st.token_secret.clone(); drop(st); if show_settings { egui::Window::new("设置") .collapsible(false) .resizable(false) .min_size(Vec2::new(350.0, 340.0)) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { 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/", st_host, st_port)); ui.add_space(8.0); // Host 输入 ui.horizontal(|ui| { ui.label("Host:"); ui.add(egui::TextEdit::singleline(&mut st_host).desired_width(200.0)); }); // Port 输入 ui.horizontal(|ui| { ui.label("端口:"); ui.add(egui::DragValue::new(&mut st_port).range(1..=65535).speed(1)); }); // 令牌ID 输入 ui.horizontal(|ui| { ui.label("令牌ID:"); ui.add(egui::TextEdit::singleline(&mut st_token_id).desired_width(200.0)); }); // 密钥 输入(密码框) ui.horizontal(|ui| { ui.label("密钥:"); ui.add(egui::TextEdit::singleline(&mut st_token_secret).password(true).desired_width(200.0)); }); ui.add_space(12.0); // 按钮 ui.horizontal(|ui| { let save_clicked = ui.button("💾 保存").clicked(); let cancel_clicked = ui.button("取消").clicked(); if save_clicked { if !st_host.is_empty() && !st_token_id.is_empty() && !st_token_secret.is_empty() { let client = ProxmoxClient::new(&st_host, st_port, &st_token_id, &st_token_secret); let mut state = state_clone.write().unwrap(); state.host = st_host; state.port = st_port; state.token_id = st_token_id; state.token_secret = st_token_secret; state.client = Arc::new(Mutex::new(Some(client))); state.is_connected = true; state.save_config(); state.add_log("配置已保存"); state.show_settings = false; } } if cancel_clicked { state_clone.write().unwrap().show_settings = false; } }); ui.add_space(8.0); ui.separator(); ui.colored_label(egui::Color32::GRAY, "💡 提示: 修改配置后点击保存以生效"); }); } }); } }