feat: add create file functionality with modal and context menu integration

This commit is contained in:
shiyu
2026-01-18 21:31:01 +08:00
parent c441d8776f
commit 52bac11760
6 changed files with 68 additions and 2 deletions
+1
View File
@@ -97,6 +97,7 @@
"Home": "Home", "Home": "Home",
"File Manager": "File Manager", "File Manager": "File Manager",
"New Folder": "New Folder", "New Folder": "New Folder",
"New File": "New File",
"Upload": "Upload", "Upload": "Upload",
"Name": "Name", "Name": "Name",
"Size": "Size", "Size": "Size",
+1
View File
@@ -116,6 +116,7 @@
"Home": "主页", "Home": "主页",
"File Manager": "文件管理", "File Manager": "文件管理",
"New Folder": "新建目录", "New Folder": "新建目录",
"New File": "新建文件",
"Upload": "上传", "Upload": "上传",
"Name": "名称", "Name": "名称",
"Size": "大小", "Size": "大小",
@@ -17,6 +17,7 @@ import { EmptyState } from './components/EmptyState';
import { ContextMenu } from './components/ContextMenu'; import { ContextMenu } from './components/ContextMenu';
import { DropzoneOverlay } from './components/DropzoneOverlay'; import { DropzoneOverlay } from './components/DropzoneOverlay';
import { CreateDirModal } from './components/Modals/CreateDirModal'; import { CreateDirModal } from './components/Modals/CreateDirModal';
import { CreateFileModal } from './components/Modals/CreateFileModal';
import { RenameModal } from './components/Modals/RenameModal'; import { RenameModal } from './components/Modals/RenameModal';
import { ProcessorModal } from './components/Modals/ProcessorModal'; import { ProcessorModal } from './components/Modals/ProcessorModal';
import UploadModal from './components/Modals/UploadModal'; import UploadModal from './components/Modals/UploadModal';
@@ -49,6 +50,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
// --- State for Modals --- // --- State for Modals ---
const [creatingDir, setCreatingDir] = useState(false); const [creatingDir, setCreatingDir] = useState(false);
const [creatingFile, setCreatingFile] = useState(false);
const [renaming, setRenaming] = useState<VfsEntry | null>(null); const [renaming, setRenaming] = useState<VfsEntry | null>(null);
const [sharingEntries, setSharingEntries] = useState<VfsEntry[]>([]); const [sharingEntries, setSharingEntries] = useState<VfsEntry[]>([]);
const [detailEntry, setDetailEntry] = useState<VfsEntry | null>(null); const [detailEntry, setDetailEntry] = useState<VfsEntry | null>(null);
@@ -138,7 +140,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
clearSearchSelection(); clearSearchSelection();
}, [clearSearchSelection, clearSelection]); }, [clearSearchSelection, clearSelection]);
const { doCreateDir: doCreateDirInCurrentDir } = useFileActions({ const { doCreateDir: doCreateDirInCurrentDir, doCreateFile: doCreateFileInCurrentDir } = useFileActions({
path, path,
refresh, refresh,
clearSelection, clearSelection,
@@ -343,6 +345,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
{/* --- Modals & Context Menus --- */} {/* --- Modals & Context Menus --- */}
<CreateDirModal open={creatingDir} onOk={(name) => { doCreateDirInCurrentDir(name); setCreatingDir(false); }} onCancel={() => setCreatingDir(false)} /> <CreateDirModal open={creatingDir} onOk={(name) => { doCreateDirInCurrentDir(name); setCreatingDir(false); }} onCancel={() => setCreatingDir(false)} />
<CreateFileModal open={creatingFile} onOk={(name) => { doCreateFileInCurrentDir(name); setCreatingFile(false); }} onCancel={() => setCreatingFile(false)} />
<RenameModal entry={renaming} onOk={(entry, newName) => { doRename(entry, newName); setRenaming(null); }} onCancel={() => setRenaming(null)} /> <RenameModal entry={renaming} onOk={(entry, newName) => { doRename(entry, newName); setRenaming(null); }} onCancel={() => setRenaming(null)} />
<FileDetailModal entry={detailEntry} loading={detailLoading} data={detailData} onClose={() => setDetailEntry(null)} /> <FileDetailModal entry={detailEntry} loading={detailLoading} data={detailData} onClose={() => setDetailEntry(null)} />
<MoveCopyModal <MoveCopyModal
@@ -422,6 +425,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
}} }}
onUploadFile={openFilePicker} onUploadFile={openFilePicker}
onUploadDirectory={openDirectoryPicker} onUploadDirectory={openDirectoryPicker}
onCreateFile={() => setCreatingFile(true)}
onCreateDir={() => setCreatingDir(true)} onCreateDir={() => setCreatingDir(true)}
onShare={doShare} onShare={doShare}
onGetDirectLink={doGetDirectLink} onGetDirectLink={doGetDirectLink}
@@ -8,7 +8,7 @@ import { useI18n } from '../../../i18n';
import { import {
FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined, FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined,
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined,
ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined, FileAddOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
interface ContextMenuProps { interface ContextMenuProps {
@@ -28,6 +28,7 @@ interface ContextMenuProps {
onProcess: (entry: VfsEntry, processorType: string) => void; onProcess: (entry: VfsEntry, processorType: string) => void;
onUploadFile: () => void; onUploadFile: () => void;
onUploadDirectory: () => void; onUploadDirectory: () => void;
onCreateFile: () => void;
onCreateDir: () => void; onCreateDir: () => void;
onShare: (entries: VfsEntry[]) => void; onShare: (entries: VfsEntry[]) => void;
onGetDirectLink: (entry: VfsEntry) => void; onGetDirectLink: (entry: VfsEntry) => void;
@@ -70,6 +71,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
{ key: 'upload-folder', label: t('Upload Folder'), onClick: actions.onUploadDirectory }, { key: 'upload-folder', label: t('Upload Folder'), onClick: actions.onUploadDirectory },
], ],
}, },
{ key: 'new-file', label: t('New File'), icon: <FileAddOutlined />, onClick: actions.onCreateFile },
{ key: 'mkdir', label: t('New Folder'), icon: <PlusOutlined />, onClick: actions.onCreateDir }, { key: 'mkdir', label: t('New Folder'), icon: <PlusOutlined />, onClick: actions.onCreateDir },
]; ];
} }
@@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react';
import { Input, Modal } from 'antd';
import { useI18n } from '../../../../i18n';
interface CreateFileModalProps {
open: boolean;
onOk: (name: string) => void;
onCancel: () => void;
}
export const CreateFileModal: React.FC<CreateFileModalProps> = ({ open, onOk, onCancel }) => {
const [name, setName] = useState('');
const { t } = useI18n();
useEffect(() => {
if (open) {
setName('');
}
}, [open]);
const handleOk = () => {
onOk(name);
};
return (
<Modal
title={t('New File')}
open={open}
onOk={handleOk}
onCancel={onCancel}
okButtonProps={{ disabled: !name.trim() }}
destroyOnHidden
>
<Input
placeholder={t('Filename')}
value={name}
onChange={(e) => setName(e.target.value)}
onPressEnter={handleOk}
autoFocus
/>
</Modal>
);
};
@@ -37,6 +37,20 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
} }
}, [path, refresh, t]); }, [path, refresh, t]);
const doCreateFile = useCallback(async (name: string) => {
if (!name.trim()) {
message.warning(t('Please input name'));
return;
}
try {
const fullPath = (path === '/' ? '' : path) + '/' + name.trim();
await vfsApi.uploadFile(fullPath, new Blob([]));
refresh();
} catch (e: any) {
message.error(e.message);
}
}, [path, refresh, t]);
const doDelete = useCallback(async (entries: VfsEntry[]) => { const doDelete = useCallback(async (entries: VfsEntry[]) => {
Modal.confirm({ Modal.confirm({
title: t('Confirm delete {name}?', { name: entries.length > 1 ? `${entries.length} ${t('items')}` : entries[0].name }), title: t('Confirm delete {name}?', { name: entries.length > 1 ? `${entries.length} ${t('items')}` : entries[0].name }),
@@ -193,6 +207,7 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
return { return {
doCreateDir, doCreateDir,
doCreateFile,
doDelete, doDelete,
doRename, doRename,
doDownload, doDownload,