diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index bbfbbf5..fd2e3be 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -97,6 +97,7 @@ "Home": "Home", "File Manager": "File Manager", "New Folder": "New Folder", + "New File": "New File", "Upload": "Upload", "Name": "Name", "Size": "Size", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 0b80cd7..c87acaf 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -116,6 +116,7 @@ "Home": "主页", "File Manager": "文件管理", "New Folder": "新建目录", + "New File": "新建文件", "Upload": "上传", "Name": "名称", "Size": "大小", diff --git a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx index 1cc70f5..22569d5 100644 --- a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx +++ b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx @@ -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(null); const [sharingEntries, setSharingEntries] = useState([]); const [detailEntry, setDetailEntry] = useState(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 --- */} { doCreateDirInCurrentDir(name); setCreatingDir(false); }} onCancel={() => setCreatingDir(false)} /> + { doCreateFileInCurrentDir(name); setCreatingFile(false); }} onCancel={() => setCreatingFile(false)} /> { doRename(entry, newName); setRenaming(null); }} onCancel={() => setRenaming(null)} /> setDetailEntry(null)} /> setCreatingFile(true)} onCreateDir={() => setCreatingDir(true)} onShare={doShare} onGetDirectLink={doGetDirectLink} diff --git a/web/src/pages/FileExplorerPage/components/ContextMenu.tsx b/web/src/pages/FileExplorerPage/components/ContextMenu.tsx index 51ba558..bc95690 100644 --- a/web/src/pages/FileExplorerPage/components/ContextMenu.tsx +++ b/web/src/pages/FileExplorerPage/components/ContextMenu.tsx @@ -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 = (props) => { { key: 'upload-folder', label: t('Upload Folder'), onClick: actions.onUploadDirectory }, ], }, + { key: 'new-file', label: t('New File'), icon: , onClick: actions.onCreateFile }, { key: 'mkdir', label: t('New Folder'), icon: , onClick: actions.onCreateDir }, ]; } diff --git a/web/src/pages/FileExplorerPage/components/Modals/CreateFileModal.tsx b/web/src/pages/FileExplorerPage/components/Modals/CreateFileModal.tsx new file mode 100644 index 0000000..f4f5a98 --- /dev/null +++ b/web/src/pages/FileExplorerPage/components/Modals/CreateFileModal.tsx @@ -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 = ({ open, onOk, onCancel }) => { + const [name, setName] = useState(''); + const { t } = useI18n(); + + useEffect(() => { + if (open) { + setName(''); + } + }, [open]); + + const handleOk = () => { + onOk(name); + }; + + return ( + + setName(e.target.value)} + onPressEnter={handleOk} + autoFocus + /> + + ); +}; diff --git a/web/src/pages/FileExplorerPage/hooks/useFileActions.tsx b/web/src/pages/FileExplorerPage/hooks/useFileActions.tsx index 93040d8..451fa35 100644 --- a/web/src/pages/FileExplorerPage/hooks/useFileActions.tsx +++ b/web/src/pages/FileExplorerPage/hooks/useFileActions.tsx @@ -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,