From 43e57468928a5fcd343b0360b40bc11133809c2b Mon Sep 17 00:00:00 2001 From: xiaji Date: Tue, 14 Apr 2026 07:30:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E6=9C=BA=E5=88=97=E8=A1=A8=E8=8E=B7=E5=8F=96=E5=92=8C=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++---- src/gui.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 140 insertions(+), 11 deletions(-) diff --git a/src/api.rs b/src/api.rs index ad99903..dd13ec6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -66,18 +66,31 @@ impl ProxmoxClient { })?; let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + println!("[API] 响应状态: {}", status); + println!("[API] 响应内容: {}", body); + if !status.is_success() { - let body = resp.text().await.unwrap_or_default(); - println!("[API] HTTP错误 {}: {}", status, body); return Err(format!("HTTP {}: {}", status, body)); } #[derive(Deserialize)] - struct NodeResponse { - data: Option>, + struct NodeItem { + #[serde(rename = "node")] + node: String, } - let response: NodeResponse = resp.json().await.map_err(|e| e.to_string())?; + #[derive(Deserialize)] + struct NodeResponse { + data: Option>, + } + + let response: NodeResponse = serde_json::from_str(&body).map_err(|e| { + let err = format!("JSON解析失败: {}", e); + println!("[API] {}", err); + err + })?; + let nodes = response.data .unwrap_or_default() .into_iter() @@ -87,6 +100,58 @@ impl ProxmoxClient { Ok(nodes) } + pub async fn get_vms(&self, node: &str) -> Result, String> { + let url = format!("{}/nodes/{}/qemu", self.base_url, node); + println!("[API] 获取虚拟机列表: GET {}", url); + + let resp = self.client + .get(&url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(|e| { + let err = format!("网络错误: {}", e); + println!("[API] {}", err); + err + })?; + + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + println!("[API] 响应状态: {}", status); + println!("[API] 响应内容: {}", body); + + if !status.is_success() { + return Err(format!("HTTP {}: {}", status, body)); + } + + #[derive(Deserialize)] + struct VmItem { + #[serde(rename = "vmid")] + vmid: u32, + #[serde(rename = "name")] + name: Option, + } + + #[derive(Deserialize)] + struct VmResponse { + data: Option>, + } + + let response: VmResponse = serde_json::from_str(&body).map_err(|e| { + let err = format!("JSON解析失败: {}", e); + println!("[API] {}", err); + err + })?; + + let vms: Vec<(u32, String)> = response.data + .unwrap_or_default() + .into_iter() + .map(|v| (v.vmid, v.name.unwrap_or_else(|| format!("VM{}", v.vmid)))) + .collect(); + println!("[API] 虚拟机列表: {:?}", vms); + Ok(vms) + } + pub async fn get_cluster_resources(&self) -> Result, String> { let url = format!("{}/cluster/resources", self.base_url); println!("[API] 获取集群资源: GET {}", url); diff --git a/src/gui.rs b/src/gui.rs index 0d2407d..83f03e9 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -135,6 +135,12 @@ pub struct NodesData { pub selected_index: usize, } +#[derive(Default)] +pub struct VmListData { + pub vms: Vec<(u32, String)>, + pub selected_index: usize, +} + pub fn gui_run() { let options = NativeOptions { viewport: egui::ViewportBuilder::default() @@ -169,6 +175,7 @@ struct App { show_settings: bool, settings: SettingsData, nodes: NodesData, + vms: VmListData, } impl App { @@ -186,6 +193,7 @@ impl App { show_settings: false, settings, nodes: NodesData::default(), + vms: VmListData::default(), } } } @@ -220,11 +228,6 @@ impl eframe::App for App { 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("节点:"); if self.nodes.nodes.is_empty() { @@ -238,9 +241,10 @@ impl eframe::App for App { } }); } - if ui.button("🔄").clicked() { + if ui.button("🔄节点").clicked() { let client = st.client.clone(); let state_clone = state.clone(); + let nodes_clone = Arc::new(RwLock::new(Vec::::new())); st.add_log("正在获取节点列表..."); ctx.request_repaint(); thread::spawn(move || { @@ -250,6 +254,7 @@ impl eframe::App for App { if let Some(c) = client.as_ref() { match c.get_nodes().await { Ok(nodes) => { + *nodes_clone.write().unwrap() = nodes.clone(); if !nodes.is_empty() { state_clone.write().unwrap().add_log(&format!("找到节点: {:?}", nodes)); } else { @@ -268,6 +273,65 @@ impl eframe::App for App { } }); ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("虚拟机:"); + if self.vms.vms.is_empty() { + ui.label("无虚拟机"); + } else { + let vm_names: Vec = self.vms.vms.iter().map(|(id, name)| format!("{} ({})", name, id)).collect(); + egui::ComboBox::from_id_salt("vm_combo") + .selected_text(&vm_names[self.vms.selected_index.min(vm_names.len().saturating_sub(1))]) + .show_ui(ui, |ui| { + for (i, (_, name)) in self.vms.vms.iter().enumerate() { + if ui.selectable_value(&mut self.vms.selected_index, i, format!("{} ({})", name, self.vms.vms[i].0)).clicked() { + st.vm_id = self.vms.vms[i].0; + } + } + }); + } + if ui.button("🔄VM").clicked() { + if self.nodes.nodes.is_empty() { + st.add_log("请先获取节点列表"); + } else { + let node = self.nodes.nodes[self.nodes.selected_index.min(self.nodes.nodes.len().saturating_sub(1))].clone(); + let client = st.client.clone(); + let state_clone = state.clone(); + 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.get_vms(&node).await { + Ok(vms) => { + if !vms.is_empty() { + state_clone.write().unwrap().add_log(&format!("找到 {} 台虚拟机", vms.len())); + for (id, name) in &vms { + state_clone.write().unwrap().add_log(&format!(" - {} ({})", name, id)); + } + } else { + state_clone.write().unwrap().add_log("未找到虚拟机"); + } + } + Err(e) => { + state_clone.write().unwrap().add_log(&format!("获取虚拟机失败: {}", e)); + } + } + } else { + state_clone.write().unwrap().add_log("未连接到服务器"); + } + }); + }); + } + } + }); + 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("VM 状态:"); ui.label(&st.vm_status);