diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx index a5344b85..8284d531 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx @@ -68,6 +68,8 @@ interface FileManagerGridProps { onCut?: (files: FileItem[]) => void; onPaste?: () => void; onUndo?: () => void; + onFileDrop?: (draggedFiles: FileItem[], targetFile: FileItem) => void; + onFileDiff?: (file1: FileItem, file2: FileItem) => void; } const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => { @@ -161,13 +163,19 @@ export function FileManagerGrid({ onCopy, onCut, onPaste, - onUndo + onUndo, + onFileDrop, + onFileDiff }: FileManagerGridProps) { const { t } = useTranslation(); const gridRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [dragCounter, setDragCounter] = useState(0); const [editingName, setEditingName] = useState(''); + + // 拖拽状态管理 + const [draggedFiles, setDraggedFiles] = useState([]); + const [dragOverTarget, setDragOverTarget] = useState(null); const editInputRef = useRef(null); // 开始编辑时设置初始名称 @@ -206,6 +214,77 @@ export function FileManagerGrid({ handleEditCancel(); } }; + + // 文件拖拽处理函数 + const handleFileDragStart = (e: React.DragEvent, file: FileItem) => { + // 如果拖拽的文件已选中,则拖拽所有选中的文件 + const filesToDrag = selectedFiles.includes(file) ? selectedFiles : [file]; + setDraggedFiles(filesToDrag); + + // 设置拖拽数据,添加内部拖拽标识 + const dragData = { + type: 'internal_files', + files: filesToDrag.map(f => f.path) + }; + e.dataTransfer.setData('text/plain', JSON.stringify(dragData)); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleFileDragOver = (e: React.DragEvent, targetFile: FileItem) => { + e.preventDefault(); + e.stopPropagation(); + + // 只有拖拽到不同文件且不是被拖拽的文件时才设置目标 + if (draggedFiles.length > 0 && !draggedFiles.some(f => f.path === targetFile.path)) { + setDragOverTarget(targetFile); + e.dataTransfer.dropEffect = 'move'; + } + }; + + const handleFileDragLeave = (e: React.DragEvent, targetFile: FileItem) => { + e.preventDefault(); + e.stopPropagation(); + + // 清除拖拽目标高亮 + if (dragOverTarget?.path === targetFile.path) { + setDragOverTarget(null); + } + }; + + const handleFileDrop = (e: React.DragEvent, targetFile: FileItem) => { + e.preventDefault(); + e.stopPropagation(); + + setDragOverTarget(null); + + if (draggedFiles.length === 0) return; + + // 判断拖拽行为: + // 1. 文件/文件夹 拖拽到 文件夹 = 移动操作 + // 2. 单个文件 拖拽到 单个文件 = diff对比 + // 3. 其他情况 = 无效操作 + + if (targetFile.type === 'directory') { + // 移动操作 + console.log('Moving files to directory:', draggedFiles.map(f => f.name), 'to', targetFile.name); + onFileDrop?.(draggedFiles, targetFile); + } else if (targetFile.type === 'file' && draggedFiles.length === 1 && draggedFiles[0].type === 'file') { + // diff对比操作 + console.log('Comparing files:', draggedFiles[0].name, 'vs', targetFile.name); + onFileDiff?.(draggedFiles[0], targetFile); + } else { + // 无效操作,给用户提示 + console.log('Invalid drag operation'); + } + + setDraggedFiles([]); + }; + + const handleFileDragEnd = () => { + setDraggedFiles([]); + setDragOverTarget(null); + }; + 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); @@ -265,29 +344,51 @@ export function FileManagerGrid({ } }; - // 拖放处理 + // 拖放处理 - 区分内部文件拖拽和外部文件上传 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 isInternalDrag = draggedFiles.length > 0; // 如果有内部拖拽的文件,说明是内部拖拽 + + if (!isInternalDrag) { + // 只有外部文件拖拽才显示上传提示 + setDragCounter(prev => prev + 1); + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragging(true); + } } - }, []); + }, [draggedFiles]); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - setDragCounter(prev => prev - 1); - if (dragCounter <= 1) { - setIsDragging(false); + + // 检查是否是内部文件拖拽 + const isInternalDrag = draggedFiles.length > 0; + + if (!isInternalDrag) { + setDragCounter(prev => prev - 1); + if (dragCounter <= 1) { + setIsDragging(false); + } } - }, [dragCounter]); + }, [dragCounter, draggedFiles]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - }, []); + + // 检查是否是内部文件拖拽 + const isInternalDrag = draggedFiles.length > 0; + + if (isInternalDrag) { + e.dataTransfer.dropEffect = 'move'; + } else { + e.dataTransfer.dropEffect = 'copy'; + } + }, [draggedFiles]); // 滚轮事件处理,确保滚动正常工作 const handleWheel = useCallback((e: React.WheelEvent) => { @@ -454,13 +555,22 @@ export function FileManagerGrid({ e.preventDefault(); e.stopPropagation(); - setIsDragging(false); - setDragCounter(0); + // 检查是否是内部文件拖拽 + const isInternalDrag = draggedFiles.length > 0; - if (onUpload && e.dataTransfer.files.length > 0) { - onUpload(e.dataTransfer.files); + if (isInternalDrag) { + // 内部拖拽:不处理,因为已经在 handleFileDrop 中处理了 + console.log('Internal drag detected, ignoring container drop'); + } else { + // 外部拖拽:处理文件上传 + setIsDragging(false); + setDragCounter(0); + + if (onUpload && e.dataTransfer.files.length > 0) { + onUpload(e.dataTransfer.files); + } } - }, [onUpload]); + }, [onUpload, draggedFiles]); // 文件选择处理 const handleFileClick = (file: FileItem, event: React.MouseEvent) => { @@ -747,10 +857,13 @@ export function FileManagerGrid({
f.path === file.path) && "opacity-50" )} title={`${file.name} - Selected: ${isSelected} - SelectedCount: ${selectedFiles.length}`} onClick={(e) => handleFileClick(file, e)} @@ -759,6 +872,11 @@ export function FileManagerGrid({ e.stopPropagation(); onContextMenu?.(e, file); }} + onDragStart={(e) => handleFileDragStart(e, file)} + onDragOver={(e) => handleFileDragOver(e, file)} + onDragLeave={(e) => handleFileDragLeave(e, file)} + onDrop={(e) => handleFileDrop(e, file)} + onDragEnd={handleFileDragEnd} >
{/* 文件图标 */} @@ -824,10 +942,13 @@ export function FileManagerGrid({
f.path === file.path) && "opacity-50" )} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => { @@ -835,6 +956,11 @@ export function FileManagerGrid({ e.stopPropagation(); onContextMenu?.(e, file); }} + onDragStart={(e) => handleFileDragStart(e, file)} + onDragOver={(e) => handleFileDragOver(e, file)} + onDragLeave={(e) => handleFileDragLeave(e, file)} + onDrop={(e) => handleFileDrop(e, file)} + onDragEnd={handleFileDragEnd} > {/* 文件图标 */}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index dfa16de9..31d6b3e9 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -940,6 +940,147 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { return candidateName; } + // 拖拽处理:文件/文件夹拖到文件夹 = 移动操作 + async function handleFileDrop(draggedFiles: FileItem[], targetFolder: FileItem) { + if (!sshSessionId || targetFolder.type !== 'directory') return; + + try { + await ensureSSHConnection(); + + let successCount = 0; + const movedItems: string[] = []; + + for (const file of draggedFiles) { + try { + const targetPath = targetFolder.path.endsWith('/') + ? `${targetFolder.path}${file.name}` + : `${targetFolder.path}/${file.name}`; + + // 只有当目标路径与原路径不同时才移动 + if (file.path !== targetPath) { + await moveSSHItem( + sshSessionId, + file.path, + targetPath, + currentHost?.id, + currentHost?.userId?.toString() + ); + movedItems.push(file.name); + successCount++; + } + } catch (error: any) { + console.error(`Failed to move file ${file.name}:`, error); + toast.error(`移动 ${file.name} 失败: ${error.message}`); + } + } + + if (successCount > 0) { + // 记录撤销历史 + const movedFiles = draggedFiles.slice(0, successCount).map((file, index) => { + const targetPath = targetFolder.path.endsWith('/') + ? `${targetFolder.path}${file.name}` + : `${targetFolder.path}/${file.name}`; + return { + originalPath: file.path, + targetPath: targetPath, + targetName: file.name + }; + }); + + const undoAction: UndoAction = { + type: 'cut', + description: `拖拽移动了 ${successCount} 个项目到 ${targetFolder.name}`, + data: { + operation: 'cut', + copiedFiles: movedFiles, + targetDirectory: targetFolder.path + }, + timestamp: Date.now() + }; + setUndoHistory(prev => [...prev.slice(-9), undoAction]); + + toast.success(`成功移动了 ${successCount} 个项目到 ${targetFolder.name}`); + loadDirectory(currentPath); + clearSelection(); // 清除选中状态 + } + } catch (error: any) { + console.error('Drag move operation failed:', error); + toast.error(`移动操作失败: ${error.message}`); + } + } + + // 拖拽处理:文件拖到文件 = diff对比操作 + function handleFileDiff(file1: FileItem, file2: FileItem) { + if (file1.type !== 'file' || file2.type !== 'file') { + toast.error('只能对比两个文件'); + return; + } + + if (!sshSessionId) { + toast.error(t("fileManager.noSSHConnection")); + return; + } + + // 在新窗口中打开两个文件进行对比 + console.log('Opening diff comparison:', file1.name, 'vs', file2.name); + + // 计算第一个窗口位置 + const offsetX1 = 100; + const offsetY1 = 100; + + // 计算第二个窗口位置(偏移) + const offsetX2 = 450; + const offsetY2 = 120; + + // 创建第一个文件窗口 + const windowId1 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const createWindowComponent1 = (windowId: string) => ( + + ); + + openWindow({ + id: windowId1, + type: 'file', + title: `${file1.name} (对比文件1)`, + isMaximized: false, + component: createWindowComponent1, + zIndex: Date.now() + }); + + // 稍后打开第二个文件窗口 + setTimeout(() => { + const windowId2 = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const createWindowComponent2 = (windowId: string) => ( + + ); + + openWindow({ + id: windowId2, + type: 'file', + title: `${file2.name} (对比文件2)`, + isMaximized: false, + component: createWindowComponent2, + zIndex: Date.now() + 1 + }); + }, 200); + + toast.success(`正在打开文件对比: ${file1.name} 与 ${file2.name}`); + } + // 过滤文件并添加新建的临时项目 let filteredFiles = files.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()) @@ -1088,6 +1229,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { onCut={handleCutFiles} onPaste={handlePasteFiles} onUndo={handleUndo} + onFileDrop={handleFileDrop} + onFileDiff={handleFileDiff} /> {/* 右键菜单 */}