import { useCallback, useEffect, useState, type ReactNode } from 'react' import { Download, Trash2, RefreshCw, CheckCircle2, XCircle, ToggleLeft, ToggleRight, Loader2, AlertCircle, X, } from 'lucide-react' import { getImages, downloadImage, cancelImageDownload, deleteImage, toggleImage, ImageInfo } from '../services/api' import { useDialog } from '../components/Dialog' export default function ImageManagement() { const dialog = useDialog() const [images, setImages] = useState([]) const [loading, setLoading] = useState(true) const [actionLoading, setActionLoading] = useState(null) const [error, setError] = useState('') const fetchImages = useCallback(async () => { try { const res = await getImages() setImages(res.data.data || []) setError('') } catch { setError('获取镜像列表失败') } finally { setLoading(false) } }, []) useEffect(() => { fetchImages() }, [fetchImages]) useEffect(() => { const hasDownloads = images.some((img) => img.downloading) const interval = setInterval(fetchImages, hasDownloads ? 1500 : 5000) return () => clearInterval(interval) }, [fetchImages, images]) const handleDownload = async (templateId: string) => { setActionLoading(templateId) setError('') try { await downloadImage(templateId) await fetchImages() } catch (err: unknown) { setError(apiErrorMessage(err, '下载失败')) } finally { setActionLoading(null) } } const handleCancelDownload = async (templateId: string) => { setActionLoading(templateId) setError('') try { await cancelImageDownload(templateId) await fetchImages() } catch (err: unknown) { setError(apiErrorMessage(err, '取消失败')) } finally { setActionLoading(null) } } const handleDelete = async (templateId: string) => { if (!(await dialog.confirm('删除镜像', '确定要删除该镜像缓存吗?删除后需要重新下载才能使用。'))) return setActionLoading(templateId) setError('') try { await deleteImage(templateId) await fetchImages() } catch (err: unknown) { const msg = err instanceof Error ? err.message : '删除失败' setError(msg) } finally { setActionLoading(null) } } const handleToggle = async (templateId: string, enabled: boolean) => { setActionLoading(templateId) setError('') try { await toggleImage(templateId, !enabled) await fetchImages() } catch (err: unknown) { const msg = err instanceof Error ? err.message : '操作失败' setError(msg) } finally { setActionLoading(null) } } const downloadedCount = images.filter((img) => img.downloaded).length const lxcImages = images.filter((img) => img.type === 'lxc') const kvmImages = images.filter((img) => img.type === 'kvm') if (loading) { return (
) } return (

镜像管理

管理 LXC / KVM 系统镜像,下载后的镜像才能用于创建容器/虚拟机。 已下载 {downloadedCount}/{images.length}

{error && (
{error}
)} img.downloaded).length} totalCount={lxcImages.length} onDownload={handleDownload} onCancelDownload={handleCancelDownload} onDelete={handleDelete} onToggle={handleToggle} /> {kvmImages.length > 0 && ( img.downloaded).length} totalCount={kvmImages.length} onDownload={handleDownload} onCancelDownload={handleCancelDownload} onDelete={handleDelete} onToggle={handleToggle} /> )}
) } function ImageTable({ title, images, actionLoading, downloadedCount, totalCount, onDownload, onCancelDownload, onDelete, onToggle, }: { title: string images: ImageInfo[] actionLoading: string | null downloadedCount: number totalCount: number onDownload: (id: string) => void onCancelDownload: (id: string) => void onDelete: (id: string) => void onToggle: (id: string, enabled: boolean) => void }) { return (

{title}

已下载 {downloadedCount}/{totalCount}
{images.map((img) => { const isBusy = actionLoading === img.id return ( ) })}
系统镜像 发行版 架构 大小 状态 操作
{getTemplateIcon(img.id)}
{img.name}

{img.description}

{img.distro} {img.release} {img.arch} {formatSize(img.size_bytes)}
{!img.downloaded && !img.downloading && ( )} {img.downloading && ( )} {img.downloaded && !img.downloading && ( <> )}
) } function StatusBadge({ img }: { img: ImageInfo }) { if (img.downloading) { const progress = Math.max(0, Math.min(100, img.progress || 0)) const showProgress = img.stage === 'downloading' && progress > 0 return (
{downloadStatusLabel(img)} {showProgress && ( )}
) } if (img.error) { return ( 下载失败 ) } if (img.downloaded && img.enabled) { return ( 可用 ) } if (img.downloaded && !img.enabled) { return ( 已禁用 ) } return ( 未下载 ) } function downloadStatusLabel(img: ImageInfo) { if (img.stage === 'canceling') return '取消中' if (img.stage === 'converting') return '转换中' if (img.stage === 'lxc-create') return '下载中' if (img.progress > 0) return `下载中 ${Math.min(100, img.progress)}%` return '下载中' } function downloadStatusTitle(img: ImageInfo) { const parts = [downloadStatusLabel(img)] if (img.stage) parts.push(`阶段:${img.stage}`) if (img.downloaded_bytes > 0 || img.total_bytes > 0) { parts.push(`${formatSize(img.downloaded_bytes)} / ${formatSize(img.total_bytes)}`) } return parts.join(',') } function isWindowsImage(img: ImageInfo) { return img.distro === 'windows' || img.id.toLowerCase().includes('windows') } function getTemplateIcon(id: string): ReactNode { const size = 'w-5 h-5' id = id.startsWith('kvm-') ? id.slice(4) : id if (id.startsWith('debian')) return if (id.startsWith('ubuntu')) return if (id.startsWith('alpine')) return if (id.startsWith('centos')) return if (id.startsWith('archlinux')) return if (id.startsWith('fedora')) return if (id.startsWith('rockylinux')) return if (id.startsWith('windows')) return return null } function apiErrorMessage(err: unknown, fallback: string) { const error = err as { response?: { data?: { message?: string } }; message?: string } return error.response?.data?.message || error.message || fallback } function formatSize(bytes: number): string { if (bytes <= 0) return '-' if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB` }