From 0e08df76a40dfa44d1778100db3613bb1cb52eba Mon Sep 17 00:00:00 2001 From: xiaji Date: Mon, 13 Apr 2026 23:35:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E8=8E=B7=E5=8F=96=E5=92=8C=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/gui.rs | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 7d64ced..ad99903 100644 --- a/src/api.rs +++ b/src/api.rs @@ -19,6 +19,19 @@ pub struct ApiResponse { pub data: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeInfo { + pub node: String, + pub status: String, + pub type_info: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ClusterResources { + pub node: Option, + pub type_info: Option, +} + impl ProxmoxClient { pub fn new(host: &str, port: u16, token_id: &str, token_secret: &str) -> Self { let base_url = format!("https://{}:{}/api2/json", host, port); @@ -37,6 +50,74 @@ impl ProxmoxClient { format!("PVEAPIToken={}={}", self.token_id, self.token_secret) } + pub async fn get_nodes(&self) -> Result, String> { + let url = format!("{}/nodes", self.base_url); + 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(); + 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>, + } + + let response: NodeResponse = resp.json().await.map_err(|e| e.to_string())?; + let nodes = response.data + .unwrap_or_default() + .into_iter() + .map(|n| n.node) + .collect(); + println!("[API] 节点列表: {:?}", nodes); + Ok(nodes) + } + + pub async fn get_cluster_resources(&self) -> Result, String> { + let url = format!("{}/cluster/resources", self.base_url); + 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(); + 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 ResourceResponse { + data: Option>, + } + + let response: ResourceResponse = resp.json().await.map_err(|e| e.to_string())?; + Ok(response.data.unwrap_or_default()) + } + pub async fn get_vm_status(&self, node: &str, vm_id: u32) -> Result { let url = format!("{}/nodes/{}/qemu/{}/status/current", self.base_url, node, vm_id); println!("[API] 获取状态: GET {}", url); diff --git a/src/gui.rs b/src/gui.rs index d6e1994..0d2407d 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -129,6 +129,12 @@ pub struct SettingsData { pub token_secret: String, } +#[derive(Default)] +pub struct NodesData { + pub nodes: Vec, + pub selected_index: usize, +} + pub fn gui_run() { let options = NativeOptions { viewport: egui::ViewportBuilder::default() @@ -162,6 +168,7 @@ struct App { state: SharedState, show_settings: bool, settings: SettingsData, + nodes: NodesData, } impl App { @@ -178,6 +185,7 @@ impl App { state: Arc::new(RwLock::new(AppState::new(&config))), show_settings: false, settings, + nodes: NodesData::default(), } } } @@ -219,8 +227,45 @@ impl eframe::App for App { ui.add_space(4.0); ui.horizontal(|ui| { ui.label("节点:"); - ui.text_edit_singleline(&mut st.node); - ui.label("← 请输入正确的节点名称"); + if self.nodes.nodes.is_empty() { + ui.label("无节点"); + } else { + egui::ComboBox::from_id_salt("node_combo") + .selected_text(&self.nodes.nodes[self.nodes.selected_index.min(self.nodes.nodes.len().saturating_sub(1))]) + .show_ui(ui, |ui| { + for (i, node) in self.nodes.nodes.iter().enumerate() { + ui.selectable_value(&mut self.nodes.selected_index, i, node); + } + }); + } + if ui.button("🔄").clicked() { + let client = st.client.clone(); + let state_clone = state.clone(); + st.add_log("正在获取节点列表..."); + 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_nodes().await { + Ok(nodes) => { + if !nodes.is_empty() { + state_clone.write().unwrap().add_log(&format!("找到节点: {:?}", nodes)); + } 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| {