import React, { useState, useEffect, useRef } from "react"; import { FileManagerGrid } from "./FileManagerGrid"; import { FileManagerContextMenu } from "./FileManagerContextMenu"; import { useFileSelection } from "./hooks/useFileSelection"; import { useDragAndDrop } from "./hooks/useDragAndDrop"; import { WindowManager, useWindowManager } from "./components/WindowManager"; import { FileWindow } from "./components/FileWindow"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { Upload, FolderPlus, FilePlus, RefreshCw, Search, Grid3X3, List, Eye, Settings } from "lucide-react"; import type { SSHHost, FileItem } from "../../../types/index.js"; import { listSSHFiles, uploadSSHFile, downloadSSHFile, createSSHFile, createSSHFolder, deleteSSHItem, renameSSHItem, connectSSH, getSSHStatus, identifySSHSymlink } from "@/ui/main-axios.ts"; interface FileManagerModernProps { initialHost?: SSHHost | null; onClose?: () => void; } // 内部组件,使用窗口管理器 function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { const { openWindow } = useWindowManager(); const { t } = useTranslation(); // State const [currentHost, setCurrentHost] = useState(initialHost || null); const [currentPath, setCurrentPath] = useState("/"); const [files, setFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [sshSessionId, setSshSessionId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); // Context menu state const [contextMenu, setContextMenu] = useState<{ x: number; y: number; isVisible: boolean; files: FileItem[]; }>({ x: 0, y: 0, isVisible: false, files: [] }); // 操作状态 const [clipboard, setClipboard] = useState<{ files: FileItem[]; operation: 'copy' | 'cut'; } | null>(null); // 编辑状态 const [editingFile, setEditingFile] = useState(null); const [isCreatingNewFile, setIsCreatingNewFile] = useState(false); // Hooks const { selectedFiles, selectFile, selectAll, clearSelection, setSelection } = useFileSelection(); const { isDragging, dragHandlers } = useDragAndDrop({ onFilesDropped: handleFilesDropped, onError: (error) => toast.error(error), maxFileSize: 100 // 100MB }); // 初始化SSH连接 useEffect(() => { if (currentHost) { initializeSSHConnection(); } }, [currentHost]); // 文件列表更新 useEffect(() => { if (sshSessionId) { loadDirectory(currentPath); } }, [sshSessionId, currentPath]); async function initializeSSHConnection() { if (!currentHost) return; try { setIsLoading(true); console.log("Initializing SSH connection for host:", currentHost.name, "ID:", currentHost.id); // 使用主机ID作为会话ID const sessionId = currentHost.id.toString(); console.log("Using session ID:", sessionId); // 调用connectSSH建立连接 console.log("Connecting to SSH with config:", { hostId: currentHost.id, ip: currentHost.ip, port: currentHost.port, username: currentHost.username, authType: currentHost.authType, credentialId: currentHost.credentialId, userId: currentHost.userId }); const result = await connectSSH(sessionId, { hostId: currentHost.id, ip: currentHost.ip, port: currentHost.port, username: currentHost.username, password: currentHost.password, sshKey: currentHost.key, keyPassword: currentHost.keyPassword, authType: currentHost.authType, credentialId: currentHost.credentialId, userId: currentHost.userId }); console.log("SSH connection result:", result); setSshSessionId(sessionId); console.log("SSH session ID set to:", sessionId); } catch (error: any) { console.error("SSH connection failed:", error); toast.error(t("fileManager.failedToConnect") + ": " + (error.message || error)); } finally { setIsLoading(false); } } async function loadDirectory(path: string) { if (!sshSessionId) { console.error("Cannot load directory: no SSH session ID"); return; } try { setIsLoading(true); console.log("Loading directory:", path, "with session ID:", sshSessionId); // 首先检查SSH连接状态 try { const status = await getSSHStatus(sshSessionId); console.log("SSH connection status:", status); if (!status.connected) { console.log("SSH not connected, attempting to reconnect..."); await initializeSSHConnection(); return; // 重连后会触发useEffect重新加载目录 } } catch (statusError) { console.log("Failed to get SSH status, attempting to reconnect..."); await initializeSSHConnection(); return; } const response = await listSSHFiles(sshSessionId, path); console.log("Directory response from backend:", response); // 处理新的返回格式 { files: FileItem[], path: string } const files = Array.isArray(response) ? response : response?.files || []; console.log("Directory contents loaded:", files.length, "items"); console.log("Files with sizes:", files.map(f => ({ name: f.name, size: f.size, type: f.type }))); setFiles(files); clearSelection(); } catch (error: any) { console.error("Failed to load directory:", error); // 如果是连接错误,尝试重连 if (error.message?.includes("connection") || error.message?.includes("established")) { console.log("Connection error detected, attempting to reconnect..."); await initializeSSHConnection(); } else { toast.error(t("fileManager.failedToLoadDirectory") + ": " + (error.message || error)); } } finally { setIsLoading(false); } } function handleFilesDropped(fileList: FileList) { if (!sshSessionId) { toast.error(t("fileManager.noSSHConnection")); return; } Array.from(fileList).forEach(file => { handleUploadFile(file); }); } async function handleUploadFile(file: File) { if (!sshSessionId) return; try { // 确保SSH连接有效 await ensureSSHConnection(); const targetPath = currentPath.endsWith('/') ? `${currentPath}${file.name}` : `${currentPath}/${file.name}`; await uploadSSHFile(sshSessionId, targetPath, file); toast.success(t("fileManager.fileUploadedSuccessfully", { name: file.name })); loadDirectory(currentPath); } catch (error: any) { if (error.message?.includes('connection') || error.message?.includes('established')) { toast.error(`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`); } else { toast.error(t("fileManager.failedToUploadFile")); } console.error("Upload failed:", error); } } async function handleDownloadFile(file: FileItem) { if (!sshSessionId) return; try { // 确保SSH连接有效 await ensureSSHConnection(); const response = await downloadSSHFile(sshSessionId, file.path); if (response?.content) { // 转换为blob并触发下载 const byteCharacters = atob(response.content); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: response.mimeType || 'application/octet-stream' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = response.fileName || file.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast.success(t("fileManager.fileDownloadedSuccessfully", { name: file.name })); } } catch (error: any) { if (error.message?.includes('connection') || error.message?.includes('established')) { toast.error(`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`); } else { toast.error(t("fileManager.failedToDownloadFile")); } console.error("Download failed:", error); } } async function handleDeleteFiles(files: FileItem[]) { if (!sshSessionId || files.length === 0) return; try { // 确保SSH连接有效 await ensureSSHConnection(); for (const file of files) { await deleteSSHItem( sshSessionId, file.path, file.type === 'directory', // isDirectory currentHost?.id, currentHost?.userId?.toString() ); } toast.success(t("fileManager.itemsDeletedSuccessfully", { count: files.length })); loadDirectory(currentPath); clearSelection(); } catch (error: any) { if (error.message?.includes('connection') || error.message?.includes('established')) { toast.error(`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`); } else { toast.error(t("fileManager.failedToDeleteItems")); } console.error("Delete failed:", error); } } function handleCreateNewFolder() { const baseName = "NewFolder"; const uniqueName = generateUniqueName(baseName, 'directory'); const folderPath = currentPath.endsWith('/') ? `${currentPath}${uniqueName}` : `${currentPath}/${uniqueName}`; // 直接进入编辑模式,使用唯一名字 const newFolder: FileItem = { name: uniqueName, type: 'directory', path: folderPath }; console.log('Starting edit for new folder with unique name:', newFolder); setEditingFile(newFolder); setIsCreatingNewFile(true); } function handleCreateNewFile() { const baseName = "NewFile.txt"; const uniqueName = generateUniqueName(baseName, 'file'); const filePath = currentPath.endsWith('/') ? `${currentPath}${uniqueName}` : `${currentPath}/${uniqueName}`; // 直接进入编辑模式,使用唯一名字 const newFile: FileItem = { name: uniqueName, type: 'file', path: filePath, size: 0 }; console.log('Starting edit for new file with unique name:', newFile); setEditingFile(newFile); setIsCreatingNewFile(true); } // Handle symlink resolution const handleSymlinkClick = async (file: FileItem) => { if (!currentHost || !sshSessionId) { toast.error(t("fileManager.noSSHConnection")); return; } try { // 确保SSH连接有效 let currentSessionId = sshSessionId; try { const status = await getSSHStatus(currentSessionId); if (!status.connected) { const result = await connectSSH(currentSessionId, { hostId: currentHost.id, host: currentHost.ip, port: currentHost.port, username: currentHost.username, authType: currentHost.authType, password: currentHost.password, key: currentHost.key, keyPassword: currentHost.keyPassword, credentialId: currentHost.credentialId }); if (!result.success) { throw new Error(t("fileManager.failedToReconnectSSH")); } } } catch (sessionErr) { throw sessionErr; } const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path); if (symlinkInfo.type === "directory") { // 如果软链接指向目录,导航到它 setCurrentPath(symlinkInfo.target); } else if (symlinkInfo.type === "file") { // 如果软链接指向文件,打开文件 // 计算窗口位置(稍微错开) const windowCount = Date.now() % 10; const offsetX = 120 + (windowCount * 30); const offsetY = 120 + (windowCount * 30); // 创建目标文件对象 const targetFile: FileItem = { ...file, path: symlinkInfo.target }; // 创建窗口组件工厂函数 const createWindowComponent = (windowId: string) => ( ); openWindow({ title: file.name, x: offsetX, y: offsetY, width: 800, height: 600, isMaximized: false, isMinimized: false, component: createWindowComponent }); } } catch (error: any) { toast.error( error?.response?.data?.error || error?.message || t("fileManager.failedToResolveSymlink") ); } }; async function handleFileOpen(file: FileItem) { if (file.type === 'directory') { setCurrentPath(file.path); } else if (file.type === 'link') { // 处理软链接 await handleSymlinkClick(file); } else { // 在新窗口中打开文件 if (!sshSessionId) { toast.error(t("fileManager.noSSHConnection")); return; } // 计算窗口位置(稍微错开) const windowCount = Date.now() % 10; // 简单的偏移计算 const offsetX = 120 + (windowCount * 30); const offsetY = 120 + (windowCount * 30); // 创建窗口组件工厂函数 const createWindowComponent = (windowId: string) => ( ); openWindow({ title: file.name, x: offsetX, y: offsetY, width: 800, height: 600, isMaximized: false, isMinimized: false, component: createWindowComponent }); } } function handleContextMenu(event: React.MouseEvent, file?: FileItem) { event.preventDefault(); const files = file ? [file] : selectedFiles; setContextMenu({ x: event.clientX, y: event.clientY, isVisible: true, files }); } function handleCopyFiles(files: FileItem[]) { setClipboard({ files, operation: 'copy' }); toast.success(t("fileManager.filesCopiedToClipboard", { count: files.length })); } function handleCutFiles(files: FileItem[]) { setClipboard({ files, operation: 'cut' }); toast.success(t("fileManager.filesCutToClipboard", { count: files.length })); } function handlePasteFiles() { if (!clipboard || !sshSessionId) return; // TODO: 实现粘贴功能 // 这里需要根据剪贴板操作类型(copy/cut)来执行相应的操作 toast.info("粘贴功能正在开发中..."); } function handleRenameFile(file: FileItem) { setEditingFile(file); } // 确保SSH连接有效 async function ensureSSHConnection() { if (!sshSessionId || !currentHost) return; try { const status = await getSSHStatus(sshSessionId); console.log('SSH connection status:', status); if (!status.connected) { console.log('SSH not connected, attempting to reconnect...'); await connectSSH(sshSessionId, { hostId: currentHost.id, ip: currentHost.ip, port: currentHost.port, username: currentHost.username, password: currentHost.password, sshKey: currentHost.key, keyPassword: currentHost.keyPassword, authType: currentHost.authType, credentialId: currentHost.credentialId, userId: currentHost.userId }); console.log('SSH reconnection successful'); } } catch (error) { console.log('SSH connection check/reconnect failed:', error); throw error; } } // 处理重命名/创建确认 async function handleRenameConfirm(file: FileItem, newName: string) { if (!sshSessionId) return; try { // 确保SSH连接有效 await ensureSSHConnection(); if (isCreatingNewFile) { // 新建项目:直接创建最终名字 console.log('Creating new item:', { type: file.type, name: newName, path: currentPath, hostId: currentHost?.id, userId: currentHost?.userId }); if (file.type === 'file') { await createSSHFile( sshSessionId, currentPath, newName, "", currentHost?.id, currentHost?.userId?.toString() ); toast.success(t("fileManager.fileCreatedSuccessfully", { name: newName })); } else if (file.type === 'directory') { await createSSHFolder( sshSessionId, currentPath, newName, currentHost?.id, currentHost?.userId?.toString() ); toast.success(t("fileManager.folderCreatedSuccessfully", { name: newName })); } setIsCreatingNewFile(false); } else { // 现有项目:重命名 console.log('Renaming existing item:', { from: file.path, to: newName, hostId: currentHost?.id, userId: currentHost?.userId }); await renameSSHItem( sshSessionId, file.path, newName, currentHost?.id, currentHost?.userId?.toString() ); toast.success(t("fileManager.itemRenamedSuccessfully", { name: newName })); } // 清除编辑状态 setEditingFile(null); loadDirectory(currentPath); } catch (error: any) { console.error("Rename failed with error:", { error, oldPath, newName, message: error.message }); if (error.message?.includes('connection') || error.message?.includes('established')) { toast.error(`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`); } else { toast.error(t("fileManager.failedToRenameItem")); } } } // 开始编辑文件名 function handleStartEdit(file: FileItem) { setEditingFile(file); } // 取消编辑(现在也会保留项目) async function handleCancelEdit() { if (isCreatingNewFile && editingFile) { // 取消时也使用默认名字创建项目 console.log('Creating item with default name on cancel:', editingFile.name); await handleRenameConfirm(editingFile, editingFile.name); } else { setEditingFile(null); } } // 生成唯一名字(处理重名冲突) function generateUniqueName(baseName: string, type: 'file' | 'directory'): string { const existingNames = files.map(f => f.name.toLowerCase()); let candidateName = baseName; let counter = 1; // 如果名字已存在,尝试添加数字后缀 while (existingNames.includes(candidateName.toLowerCase())) { if (type === 'file' && baseName.includes('.')) { // 对于文件,在文件名和扩展名之间添加数字 const lastDotIndex = baseName.lastIndexOf('.'); const nameWithoutExt = baseName.substring(0, lastDotIndex); const extension = baseName.substring(lastDotIndex); candidateName = `${nameWithoutExt}${counter}${extension}`; } else { // 对于文件夹或没有扩展名的文件,直接添加数字 candidateName = `${baseName}${counter}`; } counter++; } console.log(`Generated unique name: ${baseName} -> ${candidateName}`); return candidateName; } // 过滤文件并添加新建的临时项目 let filteredFiles = files.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()) ); // 如果正在创建新项目,将其添加到列表中 if (isCreatingNewFile && editingFile) { // 检查是否已经存在同名项目,避免重复 const exists = filteredFiles.some(f => f.path === editingFile.path); if (!exists && editingFile.name.toLowerCase().includes(searchQuery.toLowerCase())) { filteredFiles = [editingFile, ...filteredFiles]; // 将新项目放在前面 } } if (!currentHost) { return (

{t("fileManager.selectHostToStart")}

); } return (
{/* 工具栏 */}

{currentHost.name}

{currentHost.ip}:{currentHost.port}
{/* 搜索 */}
setSearchQuery(e.target.value)} className="pl-8 w-48 h-9 bg-dark-bg-button border-dark-border" />
{/* 视图切换 */}
{/* 操作按钮 */}
{/* 主内容区域 */}
{}} // 不再需要这个回调,使用onSelectionChange onFileOpen={handleFileOpen} onSelectionChange={setSelection} currentPath={currentPath} isLoading={isLoading} onPathChange={setCurrentPath} onRefresh={() => loadDirectory(currentPath)} onUpload={handleFilesDropped} onContextMenu={handleContextMenu} viewMode={viewMode} onRename={handleRenameConfirm} editingFile={editingFile} onStartEdit={handleStartEdit} onCancelEdit={handleCancelEdit} /> {/* 右键菜单 */} setContextMenu(prev => ({ ...prev, isVisible: false }))} onDownload={(files) => files.forEach(handleDownloadFile)} onRename={handleRenameFile} onCopy={handleCopyFiles} onCut={handleCutFiles} onPaste={handlePasteFiles} onDelete={handleDeleteFiles} onUpload={() => { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.onchange = (e) => { const files = (e.target as HTMLInputElement).files; if (files) handleFilesDropped(files); }; input.click(); }} onNewFolder={handleCreateNewFolder} onNewFile={handleCreateNewFile} onRefresh={() => loadDirectory(currentPath)} hasClipboard={!!clipboard} />
); } // 主要的导出组件,包装了 WindowManager export function FileManagerModern({ initialHost, onClose }: FileManagerModernProps) { return ( ); }