2026-04-10 22:44:00 +08:00
|
|
|
use eframe::{NativeOptions, run_native};
|
2026-04-10 23:21:15 +08:00
|
|
|
use egui::{FontDefinitions, FontFamily, FontData};
|
2026-04-10 22:44:00 +08:00
|
|
|
use crate::api::ProxmoxClient;
|
|
|
|
|
use std::sync::{Arc, RwLock, Mutex};
|
|
|
|
|
use dotenv::dotenv;
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::thread;
|
|
|
|
|
|
|
|
|
|
pub type SharedState = Arc<RwLock<AppState>>;
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-11 07:51:19 +08:00
|
|
|
pub host: String,
|
|
|
|
|
pub port: u16,
|
|
|
|
|
pub show_settings: bool,
|
|
|
|
|
pub token_id: String,
|
|
|
|
|
pub token_secret: String,
|
|
|
|
|
pub token_secret_shown: bool,
|
2026-04-10 22:44:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AppState {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
dotenv().ok();
|
|
|
|
|
|
|
|
|
|
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());
|
2026-04-11 07:51:19 +08:00
|
|
|
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);
|
2026-04-10 22:44:00 +08:00
|
|
|
|
2026-04-11 07:51:19 +08:00
|
|
|
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('=') {
|
2026-04-10 22:44:00 +08:00
|
|
|
(token[..pos].to_string(), token[pos+1..].to_string())
|
|
|
|
|
} else {
|
|
|
|
|
(token.clone(), token.clone())
|
2026-04-11 07:51:19 +08:00
|
|
|
}
|
|
|
|
|
} 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);
|
2026-04-10 22:44:00 +08:00
|
|
|
Arc::new(Mutex::new(Some(c)))
|
|
|
|
|
} else {
|
|
|
|
|
Arc::new(Mutex::new(None))
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-11 07:51:19 +08:00
|
|
|
let is_connected = !host.is_empty() && !tok_id.is_empty() && !tok_secret.is_empty();
|
2026-04-10 22:44:00 +08:00
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
client,
|
|
|
|
|
vm_id,
|
|
|
|
|
node,
|
|
|
|
|
vm_status: "未知".to_string(),
|
|
|
|
|
log_buffer: vec!["程序启动".to_string()],
|
|
|
|
|
is_connected,
|
2026-04-11 07:51:19 +08:00
|
|
|
host,
|
|
|
|
|
port,
|
|
|
|
|
show_settings: false,
|
|
|
|
|
token_id: tok_id,
|
|
|
|
|
token_secret: tok_secret,
|
|
|
|
|
token_secret_shown: false,
|
2026-04-10 22:44:00 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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_title("Proxmox VM 控制器"),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-10 23:21:15 +08:00
|
|
|
run_native("Proxmox VM Controller", options, Box::new(|cc| {
|
2026-04-11 00:18:37 +08:00
|
|
|
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);
|
2026-04-10 22:44:00 +08:00
|
|
|
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.heading("Proxmox VM 控制器");
|
|
|
|
|
ui.separator();
|
|
|
|
|
|
|
|
|
|
let mut st = state.write().unwrap();
|
|
|
|
|
|
2026-04-11 07:51:19 +08:00
|
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
|
|
|
|
|
if ui.button("设置").clicked() {
|
|
|
|
|
st.show_settings = !st.show_settings;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-10 22:44:00 +08:00
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label("连接状态: ");
|
|
|
|
|
ui.label(if st.is_connected { "● 已连接" } else { "○ 未连接" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
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 || {
|
|
|
|
|
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 ui.button("停止").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 || {
|
|
|
|
|
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 ui.button("刷新").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 || {
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ui.separator();
|
|
|
|
|
|
|
|
|
|
ui.label("日志:");
|
|
|
|
|
egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
|
|
|
|
for log in &st.log_buffer {
|
|
|
|
|
ui.label(log);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-11 07:51:19 +08:00
|
|
|
|
|
|
|
|
let show_settings = st.show_settings;
|
|
|
|
|
let mut host = st.host.clone();
|
|
|
|
|
let mut port = st.port;
|
|
|
|
|
let token_secret = st.token_secret.clone();
|
|
|
|
|
let token_secret_shown = st.token_secret_shown;
|
|
|
|
|
let token_id = st.token_id.clone();
|
|
|
|
|
let state_clone = state.clone();
|
|
|
|
|
|
|
|
|
|
if show_settings {
|
|
|
|
|
egui::Window::new("设置")
|
|
|
|
|
.collapsible(false)
|
|
|
|
|
.resizable(false)
|
|
|
|
|
.show(ctx, |ui| {
|
|
|
|
|
ui.label("Base URL");
|
|
|
|
|
ui.label(format!("https://{}:{}/api2/json/", host, port));
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label("Host");
|
|
|
|
|
ui.text_edit_singleline(&mut host);
|
|
|
|
|
});
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label("端口");
|
|
|
|
|
ui.add(egui::DragValue::new(&mut port).range(1..=65535));
|
|
|
|
|
});
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label("Token Secret");
|
|
|
|
|
if token_secret_shown {
|
|
|
|
|
ui.label(&token_secret);
|
|
|
|
|
} else if !token_secret.is_empty() {
|
|
|
|
|
ui.label("已保存");
|
|
|
|
|
} else {
|
|
|
|
|
ui.label("未设置");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
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().add_log("已应用设置");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ui.separator();
|
|
|
|
|
ui.label("提示: 修改后需点击应用设置以生效");
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-10 22:44:00 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|