// 服务器文件浏览器 — SFTP 浏览/上传/下载(参考 deployment FileBrowserDialog 设计) import { useState, useEffect, useRef, useCallback } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Folder, FolderOpen, File, Upload, Download, Trash2, ArrowLeft, Home, RefreshCw, Loader2, FolderPlus, ChevronRight, X, Maximize2, Minimize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { serverApi } from '@/services/api'; interface FileItem { name: string; path: string; size: number; is_dir: boolean; mod_time: string; mode: string; } interface Breadcrumb { name: string; path: string; } const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1048576).toFixed(2)} MB`; }; interface Props { open: boolean; onOpenChange: (open: boolean) => void; serverId: number | null; serverName: string; } export function ServerFileBrowser({ open, onOpenChange, serverId, serverName }: Props) { const [items, setItems] = useState([]); const [breadcrumbs, setBreadcrumbs] = useState([]); const [currentPath, setCurrentPath] = useState('/'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [uploading, setUploading] = useState(false); const [fullscreen, setFullscreen] = useState(false); const inputRef = useRef(null); const fetchDir = useCallback(async (path: string) => { if (!serverId) return; setLoading(true); setError(''); try { const resp = await serverApi.listFiles(serverId, path) as any; const data = resp?.data || resp; setItems(data.items || []); setBreadcrumbs(data.breadcrumbs || []); setCurrentPath(data.path || path); } catch (err: any) { setError(err?.message || '加载目录失败'); setItems([]); } finally { setLoading(false); } }, [serverId]); useEffect(() => { if (open && serverId) { setFullscreen(false); fetchDir('/'); } }, [open, serverId, fetchDir]); const handleDownload = async (filePath: string) => { if (!serverId) return; try { await serverApi.downloadFile(serverId, filePath); } catch (err) { setError('下载失败: ' + ((err as any)?.message || '')); } }; const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !serverId) return; setUploading(true); try { await serverApi.uploadFile(serverId, currentPath, file); await fetchDir(currentPath); } catch (err: any) { setError(err?.message || '上传失败'); } finally { setUploading(false); if (inputRef.current) inputRef.current.value = ''; } }; const handleDelete = async (filePath: string, isDir: boolean) => { if (!serverId || !confirm(`确认删除 ${isDir ? '目录' : '文件'} "${filePath}"?`)) return; try { await serverApi.deleteFile(serverId, filePath); await fetchDir(currentPath); } catch (err: any) { setError(err?.message || '删除失败'); } }; const handleMkdir = async () => { const name = prompt('输入目录名:'); if (!name || !serverId) return; const newPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`; try { await serverApi.mkdir(serverId, newPath); await fetchDir(currentPath); } catch (err: any) { setError(err?.message || '创建失败'); } }; const sortedItems = [...items].sort((a, b) => { if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; return a.name.localeCompare(b.name); }); return ( { // 阻止点击外部关闭,上传中避免误关 if (uploading) e.preventDefault(); }} className={cn( 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0', '!duration-0 !transition-none', fullscreen ? '!max-w-none !w-screen !h-screen !rounded-none' : '' )} style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '95vw', width: '1100px', maxHeight: '92vh' } } > 文件浏览 — {serverName} SFTP 文件管理 · 上传 · 下载 · 删除 {/* Header */}
{serverName} · SFTP 文件管理
{/* Toolbar */}
{error && ( setError('')} /> {error} )}
{/* Breadcrumbs */}
{breadcrumbs.map((crumb, i) => ( {i === 0 ? ( ) : ( )} {i < breadcrumbs.length - 1 && ( )} ))}
{/* File List */}
{loading ? (
) : sortedItems.length === 0 ? (

空目录

) : ( {currentPath !== '/' && ( { const parent = currentPath.split('/').slice(0, -1).join('/') || '/'; fetchDir(parent); }}> )} {sortedItems.map((item) => ( ))}
名称 大小 修改时间 权限 操作
..
{item.is_dir ? ( ) : ( )} {item.is_dir ? ( ) : ( {item.name} )} {item.is_dir ? '-' : formatSize(item.size)} {item.mod_time} {item.mode}
{!item.is_dir && ( )}
)}
); }