import { useEffect, useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { FolderOpen, FileText, Download, ChevronRight, Home, ArrowLeft, X, Maximize2, Minimize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { pipelineApi } from '@/api'; import { useUserStore } from '@/store'; import { useTranslation } from 'react-i18next'; interface FileItem { name: string; path: string; is_dir: boolean; size: number; mod_time: string; } interface ListingData { current: string; dirs: FileItem[]; files: FileItem[]; } interface FileBrowserDialogProps { open: boolean; onOpenChange: (open: boolean) => void; runId: number | null; buildNumber: number; } 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'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } /** * 工作区文件浏览器 — 浏览/下载流水线构建目录文件 */ export function FileBrowserDialog({ open, onOpenChange, runId, buildNumber, }: FileBrowserDialogProps) { const { t } = useTranslation(); const [listing, setListing] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [currentPath, setCurrentPath] = useState(''); const [fullscreen, setFullscreen] = useState(false); // 加载目录 const loadDir = async (path: string) => { if (!runId) return; setLoading(true); setError(''); try { const data = await pipelineApi.listRunFiles(runId, path || undefined); setListing(data); setCurrentPath(data?.current || ''); } catch (err) { setError(err?.message || t('common.loadFailed')); setListing(null); } finally { setLoading(false); } }; useEffect(() => { if (open && runId) { setCurrentPath(''); loadDir(''); } }, [open, runId]); // 下载文件 const downloadFile = async (file: FileItem) => { const token = useUserStore.getState().token || ''; const url = `${location.protocol}//${location.host}/api/v1/pipelines/runs/${runId}/files/download?path=${encodeURIComponent(file.path)}&token=${encodeURIComponent(token)}`; window.open(url, '_blank'); }; // 面包屑导航 const parts = currentPath ? currentPath.split('/').filter(Boolean) : []; const breadcrumbs = [''].concat( parts.map((_, i) => '/' + parts.slice(0, i + 1).join('/')) ); return ( 文件浏览器 #{buildNumber} {t('deploy.pipeline.buildDirFiles')} {/* Header */}
#{buildNumber} 工作区文件
{/* Breadcrumb */}
{breadcrumbs.map((path, i) => ( {i === 0 ? ( ) : ( <> )} ))}
{/* Content */}
{loading && (
加载中...
)} {error && (
{error}
)} {listing && !loading && ( {/* Parent dir */} {currentPath && ( { const parent = currentPath.split('/').slice(0, -1).join('/'); loadDir(parent); }} > )} {/* Directories */} {listing.dirs?.map((dir) => ( loadDir(dir.path)} > ))} {/* Files */} {listing.files?.map((file) => ( ))} {/* Empty */} {listing.dirs?.length === 0 && listing.files?.length === 0 && !currentPath && ( )}
{t('common.name')} {t('deploy.table.size')} {t('deploy.table.modifyTime')}
..
{dir.name} - {dir.mod_time}
{file.name} {formatSize(file.size)} {file.mod_time}
{loading ? t('common.loading') : t('deploy.label.emptyDir')}
)}
); }