diff --git a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx index b33f142..1de9877 100644 --- a/web/src/pages/FileExplorerPage/FileExplorerPage.tsx +++ b/web/src/pages/FileExplorerPage/FileExplorerPage.tsx @@ -25,6 +25,7 @@ import { FileDetailModal } from './components/FileDetailModal'; import { MoveCopyModal } from './components/Modals/MoveCopyModal'; import type { ViewMode } from './types'; import { vfsApi, type VfsEntry } from '../../api/client'; +import { LoadingSkeleton } from './components/LoadingSkeleton'; const FileExplorerPage = memo(function FileExplorerPage() { const { navKey = 'files', '*': restPath = '' } = useParams(); @@ -56,10 +57,11 @@ const FileExplorerPage = memo(function FileExplorerPage() { const [copyingEntries, setCopyingEntries] = useState([]); // --- Effects --- + const routePath = '/' + (restPath || '').replace(/^\/+/, ''); + useEffect(() => { - const routeP = '/' + (restPath || '').replace(/^\/+/, ''); - load(routeP, 1, pagination.pageSize, sortBy, sortOrder); - }, [restPath, navKey, load, pagination.pageSize, sortBy, sortOrder]); + load(routePath, 1, pagination.pageSize, sortBy, sortOrder); + }, [routePath, navKey, load, pagination.pageSize, sortBy, sortOrder]); // --- Handlers --- const handleOpenEntry = (entry: VfsEntry) => { @@ -167,14 +169,15 @@ const FileExplorerPage = memo(function FileExplorerPage() {
0 ? '80px' : '0' }} onContextMenu={openBlankContextMenu}> - {loading && entries.length === 0 ? ( + {loading && (entries.length === 0 || path !== routePath) ? ( + + ) : !loading && entries.length === 0 ? (
) : viewMode === 'grid' ? ( handleSelect(r, e.ctrlKey || e.metaKey)} onSelectionChange={setSelectedEntries} diff --git a/web/src/pages/FileExplorerPage/components/FileListView.tsx b/web/src/pages/FileExplorerPage/components/FileListView.tsx index ff5f14c..d48c3e6 100644 --- a/web/src/pages/FileExplorerPage/components/FileListView.tsx +++ b/web/src/pages/FileExplorerPage/components/FileListView.tsx @@ -9,7 +9,6 @@ import { useI18n } from '../../../i18n'; interface FileListViewProps { entries: VfsEntry[]; - loading: boolean; selectedEntries: string[]; onRowClick: (entry: VfsEntry, e: React.MouseEvent) => void; onSelectionChange: (selectedKeys: string[]) => void; @@ -22,7 +21,6 @@ interface FileListViewProps { export const FileListView: React.FC = ({ entries, - loading, selectedEntries, onRowClick, onSelectionChange, @@ -107,7 +105,6 @@ export const FileListView: React.FC = ({ rowKey={r => r.name} dataSource={entries} columns={columns as any} - loading={loading} pagination={false} onRow={(r) => ({ onClick: (e: any) => onRowClick(r, e), diff --git a/web/src/pages/FileExplorerPage/components/GridView.tsx b/web/src/pages/FileExplorerPage/components/GridView.tsx index 60443b0..2968b40 100644 --- a/web/src/pages/FileExplorerPage/components/GridView.tsx +++ b/web/src/pages/FileExplorerPage/components/GridView.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState, useEffect } from 'react'; -import { Tooltip, Spin, theme } from 'antd'; +import { Tooltip, theme } from 'antd'; import { FolderFilled, PictureOutlined } from '@ant-design/icons'; import type { VfsEntry } from '../../../api/client'; import { getFileIcon } from './FileIcons'; @@ -10,7 +10,6 @@ interface Props { entries: VfsEntry[]; thumbs: Record; selectedEntries: string[]; - loading: boolean; path: string; onSelect: (e: VfsEntry, additive?: boolean) => void; onSelectRange: (names: string[]) => void; @@ -25,7 +24,7 @@ const formatSize = (size: number) => { return (size / 1024 / 1024 / 1024).toFixed(1) + ' GB'; }; -export const GridView: React.FC = ({ entries, thumbs, selectedEntries, loading, path, onSelect, onSelectRange, onOpen, onContextMenu }) => { +export const GridView: React.FC = ({ entries, thumbs, selectedEntries, path, onSelect, onSelectRange, onOpen, onContextMenu }) => { const { token } = theme.useToken(); const { resolvedMode } = useTheme(); const lightenColor = (hex: string, amount: number) => { @@ -185,8 +184,7 @@ export const GridView: React.FC = ({ entries, thumbs, selectedEntries, lo }} /> )} - {loading &&
} - {!loading && entries.length === 0 && } + {entries.length === 0 && }
); }; diff --git a/web/src/pages/FileExplorerPage/components/LoadingSkeleton.tsx b/web/src/pages/FileExplorerPage/components/LoadingSkeleton.tsx new file mode 100644 index 0000000..f80d895 --- /dev/null +++ b/web/src/pages/FileExplorerPage/components/LoadingSkeleton.tsx @@ -0,0 +1,71 @@ +import type { FC } from 'react'; +import { Skeleton, theme } from 'antd'; + +type LoadingMode = 'grid' | 'list'; + +interface LoadingSkeletonProps { + mode: LoadingMode; + count?: number; +} + +const createArray = (length: number) => Array.from({ length }, (_, index) => index); + +export const LoadingSkeleton: FC = ({ mode, count }) => { + const { token } = theme.useToken(); + const fallbackCount = mode === 'grid' ? 50 : 30; + const items = createArray(count ?? fallbackCount); + + if (mode === 'grid') { + return ( +
+ {items.map((key) => ( +
+ + +
+ ))} +
+ ); + } + + return ( +
+ {items.map((key) => ( +
+ +
+ + +
+
+ ))} +
+ ); +}; +