Files
proxmox-task/src/gui.rs

468 lines
18 KiB
Rust

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<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,
pub node: String,
pub vm_status: String,
pub log_buffer: Vec<String>,
pub is_connected: bool,
}
impl AppState {
pub fn new(config: &Config) -> Self {
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.clone(),
vm_status: "未知".to_string(),
log_buffer: vec!["程序启动".to_string()],
is_connected,
}
}
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 struct SettingsData {
pub host: String,
pub port: u16,
pub token_id: String,
pub token_secret: String,
}
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,
show_settings: bool,
settings: SettingsData,
}
impl App {
fn new() -> Self {
let config = Config::load();
let settings = SettingsData {
host: config.host.clone(),
port: config.port,
token_id: config.token_id.clone(),
token_secret: config.token_secret.clone(),
};
Self {
state: Arc::new(RwLock::new(AppState::new(&config))),
show_settings: false,
settings,
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let state = self.state.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() {
self.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.label("← 请输入正确的节点名称");
});
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(&format!("节点: {}", node));
ctx.request_repaint();
thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let client = client.lock().unwrap();
if let Some(c) = client.as_ref() {
match c.shutdown_node(&node).await {
Ok(response) => {
state.write().unwrap().add_log("✓ 关机命令发送成功");
state.write().unwrap().add_log(&format!("响应: {}", response));
}
Err(e) => {
state.write().unwrap().add_log(&format!("✗ 关机失败: {}", e));
}
}
} else {
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);
}
});
});
drop(st);
// 设置窗口
if self.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/", self.settings.host, self.settings.port));
ui.add_space(8.0);
// Host 输入
ui.horizontal(|ui| {
ui.label("Host:");
ui.add(egui::TextEdit::singleline(&mut self.settings.host).desired_width(200.0));
});
// Port 输入
ui.horizontal(|ui| {
ui.label("端口:");
ui.add(egui::DragValue::new(&mut self.settings.port).range(1..=65535).speed(1));
});
// 令牌ID 输入
ui.horizontal(|ui| {
ui.label("令牌ID:");
ui.add(egui::TextEdit::singleline(&mut self.settings.token_id).desired_width(200.0));
});
// 密钥 输入(密码框)
ui.horizontal(|ui| {
ui.label("密钥:");
ui.add(egui::TextEdit::singleline(&mut self.settings.token_secret).password(true).desired_width(200.0));
});
ui.add_space(12.0);
// 按钮
ui.horizontal(|ui| {
if ui.button("💾 保存").clicked() {
if !self.settings.host.is_empty() && !self.settings.token_id.is_empty() && !self.settings.token_secret.is_empty() {
let client = ProxmoxClient::new(
&self.settings.host,
self.settings.port,
&self.settings.token_id,
&self.settings.token_secret
);
let config = Config {
host: self.settings.host.clone(),
port: self.settings.port,
token_id: self.settings.token_id.clone(),
token_secret: self.settings.token_secret.clone(),
vm_id: state.read().unwrap().vm_id,
node: state.read().unwrap().node.clone(),
};
config.save();
let mut st = state.write().unwrap();
st.client = Arc::new(Mutex::new(Some(client)));
st.is_connected = true;
st.add_log("配置已保存");
self.show_settings = false;
}
}
if ui.button("取消").clicked() {
self.show_settings = false;
}
});
ui.add_space(8.0);
ui.separator();
ui.colored_label(egui::Color32::GRAY, "💡 提示: 修改配置后点击保存以生效");
});
}
});
}
}