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

View File

@@ -97,6 +97,7 @@
"Home": "Home",
"File Manager": "File Manager",
"New Folder": "New Folder",
"New File": "New File",
"Upload": "Upload",
"Name": "Name",
"Size": "Size",

View File

@@ -116,6 +116,7 @@
"Home": "主页",
"File Manager": "文件管理",
"New Folder": "新建目录",
"New File": "新建文件",
"Upload": "上传",
"Name": "名称",
"Size": "大小",

View File

@@ -17,6 +17,7 @@ import { EmptyState } from './components/EmptyState';
import { ContextMenu } from './components/ContextMenu';
import { DropzoneOverlay } from './components/DropzoneOverlay';
import { CreateDirModal } from './components/Modals/CreateDirModal';
import { CreateFileModal } from './components/Modals/CreateFileModal';
import { RenameModal } from './components/Modals/RenameModal';
import { ProcessorModal } from './components/Modals/ProcessorModal';
import UploadModal from './components/Modals/UploadModal';
@@ -49,6 +50,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
// --- State for Modals ---
const [creatingDir, setCreatingDir] = useState(false);
const [creatingFile, setCreatingFile] = useState(false);
const [renaming, setRenaming] = useState<VfsEntry | null>(null);
const [sharingEntries, setSharingEntries] = useState<VfsEntry[]>([]);
const [detailEntry, setDetailEntry] = useState<VfsEntry | null>(null);
@@ -138,7 +140,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
clearSearchSelection();
}, [clearSearchSelection, clearSelection]);
const { doCreateDir: doCreateDirInCurrentDir } = useFileActions({
const { doCreateDir: doCreateDirInCurrentDir, doCreateFile: doCreateFileInCurrentDir } = useFileActions({
path,
refresh,
clearSelection,
@@ -343,6 +345,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
{/* --- Modals & Context Menus --- */}
<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)} />
<FileDetailModal entry={detailEntry} loading={detailLoading} data={detailData} onClose={() => setDetailEntry(null)} />
<MoveCopyModal
@@ -422,6 +425,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
}}
onUploadFile={openFilePicker}
onUploadDirectory={openDirectoryPicker}
onCreateFile={() => setCreatingFile(true)}
onCreateDir={() => setCreatingDir(true)}
onShare={doShare}
onGetDirectLink={doGetDirectLink}

View File

@@ -8,7 +8,7 @@ import { useI18n } from '../../../i18n';
import {
FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined,
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined,
ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined
ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined, FileAddOutlined
} from '@ant-design/icons';
interface ContextMenuProps {
@@ -28,6 +28,7 @@ interface ContextMenuProps {
onProcess: (entry: VfsEntry, processorType: string) => void;
onUploadFile: () => void;
onUploadDirectory: () => void;
onCreateFile: () => void;
onCreateDir: () => void;
onShare: (entries: 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: 'new-file', label: t('New File'), icon: <FileAddOutlined />, onClick: actions.onCreateFile },
{ key: 'mkdir', label: t('New Folder'), icon: <PlusOutlined />, onClick: actions.onCreateDir },
];
}

View File

@@ -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>
);
};

View File

@@ -37,6 +37,20 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
}
}, [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[]) => {
Modal.confirm({
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 {
doCreateDir,
doCreateFile,
doDelete,
doRename,
doDownload,