diff --git a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx index f4d52e4..e4399d1 100644 --- a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx +++ b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router'; import { theme, Pagination } from 'antd'; import { AppWindowsLayer } from '../../apps/AppWindowsLayer'; @@ -15,6 +15,7 @@ import { GridView } from './components/GridView'; import { FileListView } from './components/FileListView'; import { EmptyState } from './components/EmptyState'; import { ContextMenu } from './components/ContextMenu'; +import { DropzoneOverlay } from './components/DropzoneOverlay'; import { CreateDirModal } from './components/Modals/CreateDirModal'; import { RenameModal } from './components/Modals/RenameModal'; import { ProcessorModal } from './components/Modals/ProcessorModal'; @@ -29,6 +30,8 @@ const FileExplorerPage = memo(function FileExplorerPage() { const { navKey = 'files', '*': restPath = '' } = useParams(); const { token } = theme.useToken(); const [viewMode, setViewMode] = useState('grid'); + const [isDragging, setIsDragging] = useState(false); + const dragCounter = useRef(0); // --- Hooks --- const { path, entries, loading, pagination, processorTypes, load, navigateTo, goUp, handlePaginationChange, refresh } = useFileExplorer(navKey); @@ -37,6 +40,7 @@ const FileExplorerPage = memo(function FileExplorerPage() { const { appWindows, openFileWithDefaultApp, confirmOpenWithApp, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows(path); const { ctxMenu, blankCtxMenu, openContextMenu, openBlankContextMenu, closeContextMenus } = useContextMenu(); const uploader = useUploader(path, refresh); + const { handleFileDrop } = uploader; const processorHook = useProcessor({ path, processorTypes, refresh }); const { thumbs } = useThumbnails(entries, path); @@ -79,6 +83,37 @@ const FileExplorerPage = memo(function FileExplorerPage() { } }; + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current++; + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragging(true); + } + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current--; + if (dragCounter.current === 0) { + setIsDragging(false); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + dragCounter.current = 0; + handleFileDrop(e.dataTransfer.files); + }; + return (
+
); }); diff --git a/web/src/pages/FileExplorerPage/components/DropzoneOverlay.tsx b/web/src/pages/FileExplorerPage/components/DropzoneOverlay.tsx new file mode 100644 index 0000000..1ba3971 --- /dev/null +++ b/web/src/pages/FileExplorerPage/components/DropzoneOverlay.tsx @@ -0,0 +1,39 @@ +import { memo } from 'react'; +import { theme } from 'antd'; + +interface DropzoneOverlayProps { + visible: boolean; +} + +export const DropzoneOverlay = memo(function DropzoneOverlay({ visible }: DropzoneOverlayProps) { + const { token } = theme.useToken(); + + if (!visible) { + return null; + } + + return ( +
+
+ 将文件拖放到此处以上传 +
+
+ ); +}); \ No newline at end of file diff --git a/web/src/pages/FileExplorerPage/hooks/useUploader.ts b/web/src/pages/FileExplorerPage/hooks/useUploader.ts index 51515e5..a8f63da 100644 --- a/web/src/pages/FileExplorerPage/hooks/useUploader.ts +++ b/web/src/pages/FileExplorerPage/hooks/useUploader.ts @@ -39,13 +39,25 @@ export function useUploader(path: string, onUploadComplete: () => void) { })); setFiles(newFiles); setIsModalVisible(true); - // reset file input if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; + const handleFileDrop = (droppedFiles: FileList) => { + if (droppedFiles && droppedFiles.length > 0) { + const newFiles: UploadFile[] = Array.from(droppedFiles).map(file => ({ + id: `${file.name}-${Date.now()}`, + file, + status: 'pending', + progress: 0, + })); + setFiles(newFiles); + setIsModalVisible(true); + } + }; + const startUpload = useCallback(async () => { if (files.length === 0) { return; @@ -66,7 +78,7 @@ export function useUploader(path: string, onUploadComplete: () => void) { setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f)); }); - const link = await vfsApi.getTempLinkToken(dest, 60 * 60 * 24 * 365 * 10); // 10 years + const link = await vfsApi.getTempLinkToken(dest, 60 * 60 * 24 * 365 * 10); const permanentLink = vfsApi.getTempPublicUrl(link.token); setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'success', progress: 100, permanentLink } : f)); @@ -86,6 +98,7 @@ export function useUploader(path: string, onUploadComplete: () => void) { openModal, closeModal, handleFileChange, + handleFileDrop, startUpload, }; } \ No newline at end of file