Files
NapCatQQ/napcat.webui/src/components/file_manage/file_table.tsx
时瑾 06f6a542f5 refactor: 优化eslint配置,提升代码质量 (#1341)
* feat: 统一并标准化eslint

* lint: napcat.webui

* lint: napcat.webui

* lint: napcat.core

* build: fix

* lint: napcat.webui

* refactor: 重构eslint

* Update README.md
2025-11-03 16:30:45 +08:00

248 lines
7.6 KiB
TypeScript

import { Button, ButtonGroup } from '@heroui/button';
import { Pagination } from '@heroui/pagination';
import { Spinner } from '@heroui/spinner';
import {
type Selection,
type SortDescriptor,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from '@heroui/table';
import path from 'path-browserify';
import { useCallback, useEffect, useState } from 'react';
import { BiRename } from 'react-icons/bi';
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi';
import { PhotoSlider } from 'react-photo-view';
import FileIcon from '@/components/file_icon';
import type { FileInfo } from '@/controllers/file_manager';
import { supportedPreviewExts } from './file_preview_modal';
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
export interface FileTableProps {
files: FileInfo[]
currentPath: string
loading: boolean
sortDescriptor: SortDescriptor
onSortChange: (descriptor: SortDescriptor) => void
selectedFiles: Selection
onSelectionChange: (selected: Selection) => void
onDirectoryClick: (dirPath: string) => void
onEdit: (filePath: string) => void
onPreview: (filePath: string) => void
onRenameRequest: (name: string) => void
onMoveRequest: (name: string) => void
onCopyPath: (fileName: string) => void
onDelete: (filePath: string) => void
onDownload: (filePath: string) => void
}
const PAGE_SIZE = 20;
export default function FileTable ({
files,
currentPath,
loading,
sortDescriptor,
onSortChange,
selectedFiles,
onSelectionChange,
onDirectoryClick,
onEdit,
onPreview,
onRenameRequest,
onMoveRequest,
onCopyPath,
onDelete,
onDownload,
}: FileTableProps) {
const [page, setPage] = useState(1);
const pages = Math.ceil(files.length / PAGE_SIZE) || 1;
const start = (page - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
const displayFiles = files.slice(start, end);
const [showImage, setShowImage] = useState(false);
const [previewIndex, setPreviewIndex] = useState(0);
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([]);
const addPreviewImage = useCallback((image: PreviewImage) => {
setPreviewImages((prev) => {
const exists = prev.some((p) => p.key === image.key);
if (exists) return prev;
return [...prev, image];
});
}, []);
useEffect(() => {
setPreviewImages([]);
setPreviewIndex(0);
setShowImage(false);
setPage(1);
}, [currentPath]);
const onPreviewImage = (name: string, images: PreviewImage[]) => {
const index = images.findIndex((image) => image.key === name);
if (index === -1) {
return;
}
setPreviewIndex(index);
setShowImage(true);
};
return (
<>
<PhotoSlider
images={previewImages}
visible={showImage}
onClose={() => setShowImage(false)}
index={previewIndex}
onIndexChange={setPreviewIndex}
/>
<Table
aria-label='文件列表'
sortDescriptor={sortDescriptor}
onSortChange={onSortChange}
onSelectionChange={onSelectionChange}
defaultSelectedKeys={[]}
selectedKeys={selectedFiles}
selectionMode='multiple'
bottomContent={
<div className='flex w-full justify-center'>
<Pagination
isCompact
showControls
showShadow
color='primary'
page={page}
total={pages}
onChange={(page) => setPage(page)}
/>
</div>
}
>
<TableHeader>
<TableColumn key='name' allowsSorting>
</TableColumn>
<TableColumn key='type' allowsSorting>
</TableColumn>
<TableColumn key='size' allowsSorting>
</TableColumn>
<TableColumn key='mtime' allowsSorting>
</TableColumn>
<TableColumn key='actions'></TableColumn>
</TableHeader>
<TableBody
isLoading={loading}
loadingContent={
<div className='flex justify-center items-center h-full'>
<Spinner />
</div>
}
>
{displayFiles.map((file: FileInfo) => {
const filePath = path.join(currentPath, file.name);
const ext = path.extname(file.name).toLowerCase();
const previewable = supportedPreviewExts.includes(ext);
const images = previewImages;
return (
<TableRow key={file.name}>
<TableCell>
{imageExts.includes(ext)
? (
<ImageNameButton
name={file.name}
filePath={filePath}
onPreview={() => onPreviewImage(file.name, images)}
onAddPreview={addPreviewImage}
/>
)
: (
<Button
variant='light'
onPress={() =>
file.isDirectory
? onDirectoryClick(file.name)
: previewable
? onPreview(filePath)
: onEdit(filePath)}
className='text-left justify-start'
startContent={
<FileIcon
name={file.name}
isDirectory={file.isDirectory}
/>
}
>
{file.name}
</Button>
)}
</TableCell>
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
<TableCell>
{isNaN(file.size) || file.isDirectory
? '-'
: `${file.size} 字节`}
</TableCell>
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
<TableCell>
<ButtonGroup size='sm'>
<Button
isIconOnly
color='primary'
variant='flat'
onPress={() => onRenameRequest(file.name)}
>
<BiRename />
</Button>
<Button
isIconOnly
color='primary'
variant='flat'
onPress={() => onMoveRequest(file.name)}
>
<FiMove />
</Button>
<Button
isIconOnly
color='primary'
variant='flat'
onPress={() => onCopyPath(file.name)}
>
<FiCopy />
</Button>
<Button
isIconOnly
color='primary'
variant='flat'
onPress={() => onDownload(filePath)}
>
<FiDownload />
</Button>
<Button
isIconOnly
color='primary'
variant='flat'
onPress={() => onDelete(filePath)}
>
<FiTrash2 />
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
);
}