import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs' import { Button } from '@heroui/button' import { Input } from '@heroui/input' import type { Selection, SortDescriptor } from '@react-types/shared' import clsx from 'clsx' import { motion } from 'motion/react' import path from 'path-browserify' import { useEffect, useState } from 'react' import { useDropzone } from 'react-dropzone' import toast from 'react-hot-toast' import { FiDownload, FiMove, FiPlus, FiUpload } from 'react-icons/fi' import { MdRefresh } from 'react-icons/md' import { TbTrash } from 'react-icons/tb' import { TiArrowBack } from 'react-icons/ti' import { useLocation, useNavigate } from 'react-router-dom' import CreateFileModal from '@/components/file_manage/create_file_modal' import FileEditModal from '@/components/file_manage/file_edit_modal' import FilePreviewModal from '@/components/file_manage/file_preview_modal' import FileTable from '@/components/file_manage/file_table' import MoveModal from '@/components/file_manage/move_modal' import RenameModal from '@/components/file_manage/rename_modal' import useDialog from '@/hooks/use-dialog' import FileManager, { FileInfo } from '@/controllers/file_manager' export default function FileManagerPage() { const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) const [sortDescriptor, setSortDescriptor] = useState({ column: 'name', direction: 'ascending' }) const dialog = useDialog() const location = useLocation() const navigate = useNavigate() // 修改 currentPath 初始化逻辑,去掉可能的前导斜杠 let currentPath = decodeURIComponent(location.hash.slice(1) || '/') if (/^\/[A-Z]:$/i.test(currentPath)) { currentPath = currentPath.slice(1) } const [editingFile, setEditingFile] = useState<{ path: string content: string } | null>(null) const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) const [newFileName, setNewFileName] = useState('') const [fileType, setFileType] = useState<'file' | 'directory'>('file') const [selectedFiles, setSelectedFiles] = useState(new Set()) const [isRenameModalOpen, setIsRenameModalOpen] = useState(false) const [isMoveModalOpen, setIsMoveModalOpen] = useState(false) const [renamingFile, setRenamingFile] = useState('') const [moveTargetPath, setMoveTargetPath] = useState('') const [jumpPath, setJumpPath] = useState('') const [previewFile, setPreviewFile] = useState('') const [showUpload, setShowUpload] = useState(false) const sortFiles = (files: FileInfo[], descriptor: typeof sortDescriptor) => { return [...files].sort((a, b) => { if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1 const direction = descriptor.direction === 'ascending' ? 1 : -1 switch (descriptor.column) { case 'name': return direction * a.name.localeCompare(b.name) case 'type': { const aType = a.isDirectory ? '目录' : '文件' const bType = a.isDirectory ? '目录' : '文件' return direction * aType.localeCompare(bType) } case 'size': return direction * ((a.size || 0) - (b.size || 0)) case 'mtime': return ( direction * (new Date(a.mtime).getTime() - new Date(b.mtime).getTime()) ) default: return 0 } }) } const loadFiles = async () => { setLoading(true) try { const fileList = await FileManager.listFiles(currentPath) setFiles(sortFiles(fileList, sortDescriptor)) } catch (error) { toast.error('加载文件列表失败') setFiles([]) } setLoading(false) } useEffect(() => { loadFiles() }, [currentPath]) const handleSortChange = (descriptor: typeof sortDescriptor) => { setSortDescriptor(descriptor) setFiles((prev) => sortFiles(prev, descriptor)) } const handleDirectoryClick = (dirPath: string) => { if (dirPath === '..') { if (/^[A-Z]:$/i.test(currentPath)) { navigate('/file_manager#/') return } const parentPath = path.dirname(currentPath) navigate( `/file_manager#${encodeURIComponent(parentPath === currentPath ? '/' : parentPath)}` ) return } navigate( `/file_manager#${encodeURIComponent(path.join(currentPath, dirPath))}` ) } const handleEdit = async (filePath: string) => { try { const content = await FileManager.readFile(filePath) setEditingFile({ path: filePath, content }) } catch (error) { toast.error('打开文件失败') } } const handleSave = async () => { if (!editingFile) return try { await FileManager.writeFile(editingFile.path, editingFile.content) toast.success('保存成功') setEditingFile(null) loadFiles() } catch (error) { toast.error('保存失败') } } const handleDelete = async (filePath: string) => { dialog.confirm({ title: '删除文件', content:
确定要删除文件 {filePath} 吗?
, onConfirm: async () => { try { await FileManager.delete(filePath) toast.success('删除成功') loadFiles() } catch (error) { toast.error('删除失败') } } }) } const handleCreate = async () => { if (!newFileName) return const newPath = path.join(currentPath, newFileName) try { if (fileType === 'directory') { if (!(await FileManager.createDirectory(newPath))) { toast.error('目录已存在') return } } else { if (!(await FileManager.createFile(newPath))) { toast.error('文件已存在') return } } toast.success('创建成功') setIsCreateModalOpen(false) setNewFileName('') loadFiles() } catch (error) { toast.error((error as Error)?.message || '创建失败') } } const handleBatchDelete = async () => { const selectedArray = selectedFiles instanceof Set ? Array.from(selectedFiles) : files.map((f) => f.name) if (selectedArray.length === 0) return dialog.confirm({ title: '批量删除', content:
确定要删除选中的 {selectedArray.length} 个项目吗?
, onConfirm: async () => { try { const paths = selectedArray.map((key) => path.join(currentPath, key.toString()) ) await FileManager.batchDelete(paths) toast.success('批量删除成功') setSelectedFiles(new Set()) loadFiles() } catch (error) { toast.error('批量删除失败') } } }) } const handleRename = async () => { if (!renamingFile || !newFileName) return try { await FileManager.rename( path.join(currentPath, renamingFile), path.join(currentPath, newFileName) ) toast.success('重命名成功') setIsRenameModalOpen(false) setRenamingFile('') setNewFileName('') loadFiles() } catch (error) { toast.error('重命名失败') } } const handleMove = async (sourceName: string) => { if (!moveTargetPath) return try { await FileManager.move( path.join(currentPath, sourceName), path.join(moveTargetPath, sourceName) ) toast.success('移动成功') setIsMoveModalOpen(false) setMoveTargetPath('') loadFiles() } catch (error) { toast.error('移动失败') } } const handleBatchMove = async () => { if (!moveTargetPath) return const selectedArray = selectedFiles instanceof Set ? Array.from(selectedFiles) : files.map((f) => f.name) if (selectedArray.length === 0) return try { const items = selectedArray.map((name) => ({ sourcePath: path.join(currentPath, name.toString()), targetPath: path.join(moveTargetPath, name.toString()) })) await FileManager.batchMove(items) toast.success('批量移动成功') setIsMoveModalOpen(false) setMoveTargetPath('') setSelectedFiles(new Set()) loadFiles() } catch (error) { toast.error('批量移动失败') } } const handleCopyPath = (fileName: string) => { navigator.clipboard.writeText(path.join(currentPath, fileName)) toast.success('路径已复制') } const handleMoveClick = (fileName: string) => { setRenamingFile(fileName) setMoveTargetPath('') setIsMoveModalOpen(true) } const handleDownload = (filePath: string) => { FileManager.download(filePath) } const handleBatchDownload = async () => { const selectedArray = selectedFiles instanceof Set ? Array.from(selectedFiles) : files.map((f) => f.name) if (selectedArray.length === 0) return const paths = selectedArray.map((key) => path.join(currentPath, key.toString()) ) await FileManager.batchDownload(paths) } const handlePreview = (filePath: string) => { setPreviewFile(filePath) } const onDrop = async (acceptedFiles: File[]) => { try { // 遍历处理文件,保持文件夹结构 const processedFiles = acceptedFiles.map((file) => { const relativePath = file.webkitRelativePath || file.name // 不需要额外的编码处理,浏览器会自动处理 return new File([file], relativePath, { type: file.type, lastModified: file.lastModified }) }) toast .promise(FileManager.upload(currentPath, processedFiles), { loading: '正在上传文件...', success: '上传成功', error: '上传失败' }) .then(() => { loadFiles() }) } catch (error) { toast.error('上传失败') } } const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, noClick: true, onDragOver: (e) => { e.preventDefault() e.stopPropagation() }, useFsAccessApi: false // 添加此选项以避免某些浏览器的文件系统API问题 }) return (
{((selectedFiles instanceof Set && selectedFiles.size > 0) || selectedFiles === 'all') && ( <> )} {currentPath.split('/').map((part, index, parts) => ( { const newPath = parts.slice(0, index + 1).join('/') navigate(`/file_manager#${encodeURIComponent(newPath)}`) }} > {part} ))} setJumpPath(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && jumpPath.trim() !== '') { navigate(`/file_manager#${encodeURIComponent(jumpPath.trim())}`) } }} className="ml-auto w-64" />
{ e.preventDefault() e.stopPropagation() }} >

拖拽文件或文件夹到此处上传,或点击选择文件

{ setRenamingFile(name) setNewFileName(name) setIsRenameModalOpen(true) }} onMoveRequest={handleMoveClick} onCopyPath={handleCopyPath} onDelete={handleDelete} onDownload={handleDownload} /> setEditingFile(null)} onSave={handleSave} onContentChange={(newContent) => setEditingFile((prev) => prev ? { ...prev, content: newContent ?? '' } : null ) } /> setPreviewFile('')} /> setNewFileName(e.target.value)} onClose={() => setIsCreateModalOpen(false)} onCreate={handleCreate} /> setNewFileName(e.target.value)} onClose={() => setIsRenameModalOpen(false)} onRename={handleRename} /> 0 ? `${selectedFiles.size} 个项目` : renamingFile } onClose={() => setIsMoveModalOpen(false)} onMove={() => selectedFiles instanceof Set && selectedFiles.size > 0 ? handleBatchMove() : handleMove(renamingFile) } onSelect={(dir) => setMoveTargetPath(dir)} // 替换原有 onTargetChange />
) }