diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 3919480..17a0c7d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -302,6 +302,7 @@ define_save_config_command!( enum ComponentCommandAction { Install, Reinstall, + Uninstall, } async fn run_component_command( @@ -321,6 +322,11 @@ async fn run_component_command( ComponentCommandAction::Reinstall => { component::reinstall_component(&client, id, Some(app_handle)).await } + ComponentCommandAction::Uninstall => { + tokio::task::spawn_blocking(move || component::uninstall_component(id)) + .await + .map_err(|e| AppError::process(format!("Uninstall task panicked: {}", e)))? + } } } @@ -354,6 +360,21 @@ pub async fn reinstall_component( .await } +#[tauri::command] +pub async fn uninstall_component( + app_handle: AppHandle, + state: State<'_, AppState>, + component_id: String, +) -> Result { + run_component_command( + &app_handle, + &state, + &component_id, + ComponentCommandAction::Uninstall, + ) + .await +} + // === GitHub === #[tauri::command] diff --git a/src-tauri/src/component/mod.rs b/src-tauri/src/component/mod.rs index 0c330b0..88240bc 100644 --- a/src-tauri/src/component/mod.rs +++ b/src-tauri/src/component/mod.rs @@ -63,6 +63,23 @@ pub async fn install_component( result } +/// Uninstall a component by id, dispatching to the appropriate sub-module. +pub fn uninstall_component(id: ComponentId) -> Result { + log::info!("Uninstalling component {:?}", id); + let result = match id { + ComponentId::Python => python::uninstall_component(), + ComponentId::Nodejs => nodejs::uninstall_nodejs(), + ComponentId::UV => uv::uninstall_uv(), + }; + + match &result { + Ok(msg) => log::info!("Component {:?} uninstalled: {}", id, msg), + Err(e) => log::error!("Failed to uninstall component {:?}: {}", id, e), + } + + result +} + /// Reinstall a component by id, dispatching to the appropriate sub-module. pub async fn reinstall_component( client: &Client, diff --git a/src-tauri/src/component/nodejs.rs b/src-tauri/src/component/nodejs.rs index cf6b18d..f5db1e5 100644 --- a/src-tauri/src/component/nodejs.rs +++ b/src-tauri/src/component/nodejs.rs @@ -125,6 +125,18 @@ pub async fn install_nodejs(client: &Client, app_handle: Option<&AppHandle>) -> Ok(format!("已安装 Node.js (LTS): {}", version)) } +/// Uninstall Node.js LTS. +pub fn uninstall_nodejs() -> Result { + let dir = get_component_dir("nodejs"); + if dir.exists() { + std::fs::remove_dir_all(&dir) + .map_err(|e| AppError::io(format!("Failed to remove Node.js: {}", e)))?; + Ok("已卸载 Node.js (LTS)".to_string()) + } else { + Ok("Node.js (LTS) 组件未安装".to_string()) + } +} + /// Reinstall Node.js LTS (always removes existing and re-downloads). pub async fn reinstall_nodejs(client: &Client, app_handle: Option<&AppHandle>) -> Result { let version = do_install_nodejs(client, app_handle).await?; diff --git a/src-tauri/src/component/python.rs b/src-tauri/src/component/python.rs index ea80851..975c358 100644 --- a/src-tauri/src/component/python.rs +++ b/src-tauri/src/component/python.rs @@ -70,6 +70,32 @@ pub(super) async fn install_component( } } +/// Uninstall unified Python component (3.10 + 3.12). +pub(super) fn uninstall_component() -> Result { + let py310_dir = get_python_runtime_dir(RUNTIME_PY310); + let py312_dir = get_python_runtime_dir(RUNTIME_PY312); + + let mut removed = Vec::new(); + for dir in [&py310_dir, &py312_dir] { + if dir.exists() { + std::fs::remove_dir_all(dir) + .map_err(|e| AppError::io(format!("Failed to remove Python runtime: {}", e)))?; + removed.push( + dir.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + ); + } + } + + if removed.is_empty() { + Ok("Python 组件未安装".to_string()) + } else { + Ok(format!("已卸载 Python: {}", removed.join(", "))) + } +} + /// Reinstall unified Python component (3.10 + 3.12). pub(super) async fn reinstall_component( client: &Client, diff --git a/src-tauri/src/component/uv.rs b/src-tauri/src/component/uv.rs index 188cc19..1b229af 100644 --- a/src-tauri/src/component/uv.rs +++ b/src-tauri/src/component/uv.rs @@ -100,6 +100,18 @@ pub async fn install_uv(client: &Client, app_handle: Option<&AppHandle>) -> Resu Ok(format!("已安装 uv: {}", version)) } +/// Uninstall uv. +pub fn uninstall_uv() -> Result { + let dir = get_component_dir("uv"); + if dir.exists() { + std::fs::remove_dir_all(&dir) + .map_err(|e| AppError::io(format!("Failed to remove uv: {}", e)))?; + Ok("已卸载 uv".to_string()) + } else { + Ok("uv 组件未安装".to_string()) + } +} + pub async fn reinstall_uv(client: &Client, app_handle: Option<&AppHandle>) -> Result { let version = do_install_uv(client, app_handle).await?; Ok(format!("已重新安装 uv: {}", version)) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3024aab..83680eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -178,6 +178,7 @@ pub fn run() { // Components commands::install_component, commands::reinstall_component, + commands::uninstall_component, // GitHub commands::fetch_releases, commands::fetch_launcher_release_notes, diff --git a/src/api.ts b/src/api.ts index d537a4b..254dedb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -57,6 +57,8 @@ export const api = { installComponent: (componentId: string) => invoke('install_component', { componentId }), reinstallComponent: (componentId: string) => invoke('reinstall_component', { componentId }), + uninstallComponent: (componentId: string) => + invoke('uninstall_component', { componentId }), // ======================================== // GitHub diff --git a/src/constants/operationKeys.ts b/src/constants/operationKeys.ts index c68f096..dbde240 100644 --- a/src/constants/operationKeys.ts +++ b/src/constants/operationKeys.ts @@ -7,6 +7,7 @@ export const OPERATION_KEYS = { uninstallVersion: (version: string) => `uninstall:${version}`, installComponent: (componentId: string) => `install-component:${componentId}`, reinstallComponent: (componentId: string) => `reinstall-component:${componentId}`, + uninstallComponent: (componentId: string) => `uninstall-component:${componentId}`, fetchReleases: 'fetch-releases', backupCreate: 'backup:create', diff --git a/src/pages/Versions.tsx b/src/pages/Versions.tsx index 7fc12b0..0b1f8fa 100644 --- a/src/pages/Versions.tsx +++ b/src/pages/Versions.tsx @@ -34,6 +34,11 @@ export default function Versions() { const [detailOpen, setDetailOpen] = useState(false); const [uninstallOpen, setUninstallOpen] = useState(false); const [versionToUninstall, setVersionToUninstall] = useState(null); + const [componentUninstallOpen, setComponentUninstallOpen] = useState(false); + const [componentToUninstall, setComponentToUninstall] = useState<{ + id: string; + display_name: string; + } | null>(null); const [releases, setReleases] = useState([]); const releasesLoading = operations[OPERATION_KEYS.fetchReleases] || false; @@ -145,6 +150,23 @@ export default function Versions() { [runOperation, clearDownloadProgress] ); + const handleComponentUninstall = useCallback(async () => { + if (!componentToUninstall) return; + const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id); + await runOperation({ + key, + task: () => api.uninstallComponent(componentToUninstall.id), + onSuccess: (result) => { + message.success(result); + }, + onError: (error) => { + handleApiError(error); + }, + }); + setComponentUninstallOpen(false); + setComponentToUninstall(null); + }, [componentToUninstall, runOperation]); + const isInstalled = (tagName: string) => versions.some((v) => v.version === tagName); const availableReleases = releases.filter((r) => !isInstalled(r.tag_name)); const getInstalledRelease = (version: string) => releases.find((r) => r.tag_name === version); @@ -211,6 +233,21 @@ export default function Versions() { onClick={() => runComponentInstallAction(comp.id, 'reinstall')} /> , + +