feat: add LoadingSkeleton component

This commit is contained in:
ShiYu
2025-10-19 17:17:03 +08:00
parent 77a4749fec
commit 4d6e0b86ad
4 changed files with 82 additions and 14 deletions

View File

@@ -25,6 +25,7 @@ import { FileDetailModal } from './components/FileDetailModal';
import { MoveCopyModal } from './components/Modals/MoveCopyModal'; import { MoveCopyModal } from './components/Modals/MoveCopyModal';
import type { ViewMode } from './types'; import type { ViewMode } from './types';
import { vfsApi, type VfsEntry } from '../../api/client'; import { vfsApi, type VfsEntry } from '../../api/client';
import { LoadingSkeleton } from './components/LoadingSkeleton';
const FileExplorerPage = memo(function FileExplorerPage() { const FileExplorerPage = memo(function FileExplorerPage() {
const { navKey = 'files', '*': restPath = '' } = useParams(); const { navKey = 'files', '*': restPath = '' } = useParams();
@@ -56,10 +57,11 @@ const FileExplorerPage = memo(function FileExplorerPage() {
const [copyingEntries, setCopyingEntries] = useState<VfsEntry[]>([]); const [copyingEntries, setCopyingEntries] = useState<VfsEntry[]>([]);
// --- Effects --- // --- Effects ---
const routePath = '/' + (restPath || '').replace(/^\/+/, '');
useEffect(() => { useEffect(() => {
const routeP = '/' + (restPath || '').replace(/^\/+/, ''); load(routePath, 1, pagination.pageSize, sortBy, sortOrder);
load(routeP, 1, pagination.pageSize, sortBy, sortOrder); }, [routePath, navKey, load, pagination.pageSize, sortBy, sortOrder]);
}, [restPath, navKey, load, pagination.pageSize, sortBy, sortOrder]);
// --- Handlers --- // --- Handlers ---
const handleOpenEntry = (entry: VfsEntry) => { const handleOpenEntry = (entry: VfsEntry) => {
@@ -167,14 +169,15 @@ const FileExplorerPage = memo(function FileExplorerPage() {
<input ref={uploader.fileInputRef} type="file" style={{ display: 'none' }} multiple onChange={uploader.handleFileChange} /> <input ref={uploader.fileInputRef} type="file" style={{ display: 'none' }} multiple onChange={uploader.handleFileChange} />
<div style={{ flex: 1, overflow: 'auto', paddingBottom: pagination.total > 0 ? '80px' : '0' }} onContextMenu={openBlankContextMenu}> <div style={{ flex: 1, overflow: 'auto', paddingBottom: pagination.total > 0 ? '80px' : '0' }} onContextMenu={openBlankContextMenu}>
{loading && entries.length === 0 ? ( {loading && (entries.length === 0 || path !== routePath) ? (
<LoadingSkeleton mode={viewMode} />
) : !loading && entries.length === 0 ? (
<div style={{ textAlign: 'center', padding: 40 }}><EmptyState isRoot={path === '/'} /></div> <div style={{ textAlign: 'center', padding: 40 }}><EmptyState isRoot={path === '/'} /></div>
) : viewMode === 'grid' ? ( ) : viewMode === 'grid' ? (
<GridView <GridView
entries={entries} entries={entries}
thumbs={thumbs} thumbs={thumbs}
selectedEntries={selectedEntries} selectedEntries={selectedEntries}
loading={loading}
path={path} path={path}
onSelect={handleSelect} onSelect={handleSelect}
onSelectRange={handleSelectRange} onSelectRange={handleSelectRange}
@@ -184,7 +187,6 @@ const FileExplorerPage = memo(function FileExplorerPage() {
) : ( ) : (
<FileListView <FileListView
entries={entries} entries={entries}
loading={loading}
selectedEntries={selectedEntries} selectedEntries={selectedEntries}
onRowClick={(r, e) => handleSelect(r, e.ctrlKey || e.metaKey)} onRowClick={(r, e) => handleSelect(r, e.ctrlKey || e.metaKey)}
onSelectionChange={setSelectedEntries} onSelectionChange={setSelectedEntries}

View File

@@ -9,7 +9,6 @@ import { useI18n } from '../../../i18n';
interface FileListViewProps { interface FileListViewProps {
entries: VfsEntry[]; entries: VfsEntry[];
loading: boolean;
selectedEntries: string[]; selectedEntries: string[];
onRowClick: (entry: VfsEntry, e: React.MouseEvent) => void; onRowClick: (entry: VfsEntry, e: React.MouseEvent) => void;
onSelectionChange: (selectedKeys: string[]) => void; onSelectionChange: (selectedKeys: string[]) => void;
@@ -22,7 +21,6 @@ interface FileListViewProps {
export const FileListView: React.FC<FileListViewProps> = ({ export const FileListView: React.FC<FileListViewProps> = ({
entries, entries,
loading,
selectedEntries, selectedEntries,
onRowClick, onRowClick,
onSelectionChange, onSelectionChange,
@@ -107,7 +105,6 @@ export const FileListView: React.FC<FileListViewProps> = ({
rowKey={r => r.name} rowKey={r => r.name}
dataSource={entries} dataSource={entries}
columns={columns as any} columns={columns as any}
loading={loading}
pagination={false} pagination={false}
onRow={(r) => ({ onRow={(r) => ({
onClick: (e: any) => onRowClick(r, e), onClick: (e: any) => onRowClick(r, e),

View File

@@ -1,5 +1,5 @@
import React, { useRef, useState, useEffect } from 'react'; 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 { FolderFilled, PictureOutlined } from '@ant-design/icons';
import type { VfsEntry } from '../../../api/client'; import type { VfsEntry } from '../../../api/client';
import { getFileIcon } from './FileIcons'; import { getFileIcon } from './FileIcons';
@@ -10,7 +10,6 @@ interface Props {
entries: VfsEntry[]; entries: VfsEntry[];
thumbs: Record<string, string>; thumbs: Record<string, string>;
selectedEntries: string[]; selectedEntries: string[];
loading: boolean;
path: string; path: string;
onSelect: (e: VfsEntry, additive?: boolean) => void; onSelect: (e: VfsEntry, additive?: boolean) => void;
onSelectRange: (names: string[]) => void; onSelectRange: (names: string[]) => void;
@@ -25,7 +24,7 @@ const formatSize = (size: number) => {
return (size / 1024 / 1024 / 1024).toFixed(1) + ' GB'; return (size / 1024 / 1024 / 1024).toFixed(1) + ' GB';
}; };
export const GridView: React.FC<Props> = ({ entries, thumbs, selectedEntries, loading, path, onSelect, onSelectRange, onOpen, onContextMenu }) => { export const GridView: React.FC<Props> = ({ entries, thumbs, selectedEntries, path, onSelect, onSelectRange, onOpen, onContextMenu }) => {
const { token } = theme.useToken(); const { token } = theme.useToken();
const { resolvedMode } = useTheme(); const { resolvedMode } = useTheme();
const lightenColor = (hex: string, amount: number) => { const lightenColor = (hex: string, amount: number) => {
@@ -185,8 +184,7 @@ export const GridView: React.FC<Props> = ({ entries, thumbs, selectedEntries, lo
}} }}
/> />
)} )}
{loading && <div style={{ width: '100%', textAlign: 'center', padding: 40 }}><Spin /></div>} {entries.length === 0 && <EmptyState isRoot={path === '/'} />}
{!loading && entries.length === 0 && <EmptyState isRoot={path === '/'} />}
</div> </div>
); );
}; };

View File

@@ -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<LoadingSkeletonProps> = ({ mode, count }) => {
const { token } = theme.useToken();
const fallbackCount = mode === 'grid' ? 50 : 30;
const items = createArray(count ?? fallbackCount);
if (mode === 'grid') {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))',
gap: 16,
padding: 16,
}}
>
{items.map((key) => (
<div
key={key}
style={{
background: token.colorBgElevated,
borderRadius: token.borderRadius,
padding: 16,
display: 'flex',
flexDirection: 'column',
gap: 12,
}}
>
<Skeleton.Button active block style={{ height: 96, borderRadius: token.borderRadiusLG }} />
<Skeleton active title={false} paragraph={{ rows: 2, width: ['80%', '60%'] }} />
</div>
))}
</div>
);
}
return (
<div style={{ padding: '0 16px' }}>
{items.map((key) => (
<div
key={key}
style={{
display: 'grid',
gridTemplateColumns: '48px 1fr',
alignItems: 'center',
padding: '12px 16px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
}}
>
<Skeleton.Avatar active shape="square" size={32} />
<div style={{ paddingLeft: 16 }}>
<Skeleton active title={false} paragraph={{ rows: 1, width: '60%' }} />
<Skeleton active title={false} paragraph={{ rows: 1, width: '40%' }} />
</div>
</div>
))}
</div>
);
};