mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-29 03:01:30 +08:00
feat: Add file drag-and-drop functionality #25
This commit is contained in:
@@ -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<ViewMode>('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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -91,6 +126,10 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={closeContextMenus}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Header
|
||||
navKey={navKey}
|
||||
@@ -212,6 +251,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
||||
onStartUpload={uploader.startUpload}
|
||||
/>
|
||||
<AppWindowsLayer windows={appWindows} onClose={closeWindow} onToggleMax={toggleMax} onBringToFront={bringToFront} onUpdateWindow={updateWindow} />
|
||||
<DropzoneOverlay visible={isDragging} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 100,
|
||||
borderColor: token.colorPrimary,
|
||||
borderStyle: 'dashed',
|
||||
borderWidth: 4,
|
||||
borderRadius: token.borderRadius,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: 'white', fontSize: 24, fontWeight: 'bold' }}>
|
||||
将文件拖放到此处以上传
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user