import React, { useState, useRef, useCallback, useEffect } from "react"; import { cn } from "@/lib/utils"; import { Folder, File, FileText, FileImage, FileVideo, FileAudio, Archive, Code, Settings, Download, ChevronLeft, ChevronRight, MoreHorizontal, RefreshCw, ArrowUp } from "lucide-react"; import { useTranslation } from "react-i18next"; interface FileItem { name: string; type: "file" | "directory" | "link"; path: string; size?: number; modified?: string; permissions?: string; owner?: string; group?: string; } interface FileManagerGridProps { files: FileItem[]; selectedFiles: FileItem[]; onFileSelect: (file: FileItem, multiSelect?: boolean) => void; onFileOpen: (file: FileItem) => void; onSelectionChange: (files: FileItem[]) => void; currentPath: string; isLoading?: boolean; onPathChange: (path: string) => void; onRefresh: () => void; onUpload?: (files: FileList) => void; onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void; viewMode?: 'grid' | 'list'; } const getFileIcon = (fileName: string, isDirectory: boolean, viewMode: 'grid' | 'list' = 'grid') => { const iconClass = viewMode === 'grid' ? "w-8 h-8" : "w-6 h-6"; if (isDirectory) { return ; } const ext = fileName.split('.').pop()?.toLowerCase(); switch (ext) { case 'txt': case 'md': case 'readme': return ; case 'png': case 'jpg': case 'jpeg': case 'gif': case 'bmp': case 'svg': return ; case 'mp4': case 'avi': case 'mkv': case 'mov': return ; case 'mp3': case 'wav': case 'flac': case 'ogg': return ; case 'zip': case 'tar': case 'gz': case 'rar': case '7z': return ; case 'js': case 'ts': case 'jsx': case 'tsx': case 'py': case 'java': case 'cpp': case 'c': case 'cs': case 'php': case 'rb': case 'go': case 'rs': return ; case 'json': case 'xml': case 'yaml': case 'yml': case 'toml': case 'ini': case 'conf': case 'config': return ; default: return ; } }; const formatFileSize = (bytes?: number): string => { if (!bytes) return ''; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; export function FileManagerGrid({ files, selectedFiles, onFileSelect, onFileOpen, onSelectionChange, currentPath, isLoading, onPathChange, onRefresh, onUpload, onContextMenu, viewMode = 'grid' }: FileManagerGridProps) { const { t } = useTranslation(); const gridRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [dragCounter, setDragCounter] = useState(0); const [isSelecting, setIsSelecting] = useState(false); const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null); const [selectionRect, setSelectionRect] = useState<{ x: number; y: number; width: number; height: number } | null>(null); // 导航历史管理 const [navigationHistory, setNavigationHistory] = useState([currentPath]); const [historyIndex, setHistoryIndex] = useState(0); // 更新导航历史 useEffect(() => { const lastPath = navigationHistory[historyIndex]; if (currentPath !== lastPath) { const newHistory = navigationHistory.slice(0, historyIndex + 1); newHistory.push(currentPath); setNavigationHistory(newHistory); setHistoryIndex(newHistory.length - 1); } }, [currentPath]); // 导航函数 const goBack = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); onPathChange(navigationHistory[newIndex]); } }; const goForward = () => { if (historyIndex < navigationHistory.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); onPathChange(navigationHistory[newIndex]); } }; const goUp = () => { const parts = currentPath.split('/').filter(Boolean); if (parts.length > 0) { parts.pop(); const parentPath = '/' + parts.join('/'); onPathChange(parentPath); } else if (currentPath !== '/') { onPathChange('/'); } }; // 路径导航 const pathParts = currentPath.split('/').filter(Boolean); const navigateToPath = (index: number) => { if (index === -1) { onPathChange('/'); } else { const newPath = '/' + pathParts.slice(0, index + 1).join('/'); onPathChange(newPath); } }; // 拖放处理 const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragCounter(prev => prev + 1); if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { setIsDragging(true); } }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragCounter(prev => prev - 1); if (dragCounter <= 1) { setIsDragging(false); } }, [dragCounter]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); setDragCounter(0); if (onUpload && e.dataTransfer.files.length > 0) { onUpload(e.dataTransfer.files); } }, [onUpload]); // 文件选择处理 const handleFileClick = (file: FileItem, event: React.MouseEvent) => { event.stopPropagation(); console.log('File clicked:', file.name, 'Current selected:', selectedFiles.length); if (event.detail === 2) { // 双击打开 console.log('Double click - opening file'); onFileOpen(file); } else { // 单击选择 const multiSelect = event.ctrlKey || event.metaKey; const rangeSelect = event.shiftKey; console.log('Single click - multiSelect:', multiSelect, 'rangeSelect:', rangeSelect); if (rangeSelect && selectedFiles.length > 0) { // 范围选择 (Shift+点击) console.log('Range selection'); const lastSelected = selectedFiles[selectedFiles.length - 1]; const currentIndex = files.findIndex(f => f.path === file.path); const lastIndex = files.findIndex(f => f.path === lastSelected.path); if (currentIndex !== -1 && lastIndex !== -1) { const start = Math.min(currentIndex, lastIndex); const end = Math.max(currentIndex, lastIndex); const rangeFiles = files.slice(start, end + 1); console.log('Range selection result:', rangeFiles.length, 'files'); onSelectionChange(rangeFiles); } } else if (multiSelect) { // 多选 (Ctrl+点击) console.log('Multi selection'); const isSelected = selectedFiles.some(f => f.path === file.path); if (isSelected) { console.log('Removing from selection'); onSelectionChange(selectedFiles.filter(f => f.path !== file.path)); } else { console.log('Adding to selection'); onSelectionChange([...selectedFiles, file]); } } else { // 单选 console.log('Single selection - should select only:', file.name); onSelectionChange([file]); } } }; // 空白区域点击取消选择 const handleGridClick = (event: React.MouseEvent) => { if (event.target === event.currentTarget) { onSelectionChange([]); } }; // 键盘支持 useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!gridRef.current?.contains(document.activeElement)) return; switch (event.key) { case 'Escape': onSelectionChange([]); break; case 'a': case 'A': if (event.ctrlKey || event.metaKey) { event.preventDefault(); console.log('Ctrl+A pressed - selecting all files:', files.length); onSelectionChange([...files]); } break; case 'Delete': if (selectedFiles.length > 0) { // 触发删除操作 console.log('Delete selected files:', selectedFiles); } break; case 'F2': if (selectedFiles.length === 1) { // 触发重命名 console.log('Rename file:', selectedFiles[0]); } break; case 'F5': event.preventDefault(); onRefresh(); break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [selectedFiles, files, onSelectionChange, onRefresh]); if (isLoading) { return (

{t("common.loading")}

); } return (
{/* 工具栏和路径导航 */}
{/* 导航按钮 */}
{/* 面包屑导航 */}
{pathParts.map((part, index) => ( / ))}
{/* 主文件网格 */}
onContextMenu?.(e)} tabIndex={0} > {isDragging && (

{t("fileManager.dragFilesToUpload")}

)} {files.length === 0 ? (

{t("fileManager.emptyFolder")}

) : viewMode === 'grid' ? (
{files.map((file) => { const isSelected = selectedFiles.some(f => f.path === file.path); // 详细调试路径比较 if (selectedFiles.length > 0) { console.log(`\n=== File: ${file.name} ===`); console.log(`File path: "${file.path}"`); console.log(`Selected files:`, selectedFiles.map(f => `"${f.path}"`)); console.log(`Path comparison results:`, selectedFiles.map(f => `"${f.path}" === "${file.path}" -> ${f.path === file.path}` )); console.log(`Final isSelected: ${isSelected}`); } return (
handleFileClick(file, e)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(e, file); }} >
{/* 文件图标 */}
{getFileIcon(file.name, file.type === 'directory', viewMode)}
{/* 文件名 */}

{file.name}

{file.size && file.type === 'file' && (

{formatFileSize(file.size)}

)}
); })}
) : ( /* 列表视图 */
{files.map((file) => { const isSelected = selectedFiles.some(f => f.path === file.path); return (
handleFileClick(file, e)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(e, file); }} > {/* 文件图标 */}
{getFileIcon(file.name, file.type === 'directory', viewMode)}
{/* 文件信息 */}

{file.name}

{file.modified && (

{file.modified}

)}
{/* 文件大小 */}
{file.type === 'file' && file.size && (

{formatFileSize(file.size)}

)}
{/* 权限信息 */}
{file.permissions && (

{file.permissions}

)}
); })}
)}
{/* 状态栏 */}
{files.length} {t("fileManager.itemCount", { count: files.length })} {selectedFiles.length > 0 && ( {t("fileManager.selectedCount", { count: selectedFiles.length })} )}
); }