feat: 美化界面 + 添加配置文件读写
This commit is contained in:
335
src/gui.rs
335
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<RwLock<AppState>>;
|
||||
|
||||
#[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<Mutex<Option<ProxmoxClient>>>,
|
||||
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, "💡 提示: 修改配置后点击保存以生效");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user