fix: 预览

This commit is contained in:
bietiaop
2025-02-04 14:47:38 +08:00
parent e3c7af3d91
commit bd4b0885a1
5 changed files with 265 additions and 150 deletions

View File

@@ -70,6 +70,7 @@
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"react-photo-view": "^1.2.7",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-responsive": "^10.0.0", "react-responsive": "^10.0.0",
"react-router-dom": "^7.1.4", "react-router-dom": "^7.1.4",

View File

@@ -18,11 +18,10 @@ interface FilePreviewModalProps {
onClose: () => void onClose: () => void
} }
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'] export const videoExts = ['.mp4', '.webm']
const videoExts = ['.mp4', '.webm'] export const audioExts = ['.mp3', '.wav']
const audioExts = ['.mp3', '.wav']
const supportedPreviewExts = [...imageExts, ...videoExts, ...audioExts] export const supportedPreviewExts = [...videoExts, ...audioExts]
export default function FilePreviewModal({ export default function FilePreviewModal({
isOpen, isOpen,
@@ -31,7 +30,7 @@ export default function FilePreviewModal({
}: FilePreviewModalProps) { }: FilePreviewModalProps) {
const ext = path.extname(filePath).toLowerCase() const ext = path.extname(filePath).toLowerCase()
const { data, loading, error, run } = useRequest( const { data, loading, error, run } = useRequest(
async (path: string) => FileManager.downloadToURL(path), async () => FileManager.downloadToURL(filePath),
{ {
refreshDeps: [filePath], refreshDeps: [filePath],
refreshDepsAction: () => { refreshDepsAction: () => {
@@ -39,7 +38,7 @@ export default function FilePreviewModal({
if (!filePath || !supportedPreviewExts.includes(ext)) { if (!filePath || !supportedPreviewExts.includes(ext)) {
return return
} }
run(filePath) run()
} }
} }
) )
@@ -55,20 +54,20 @@ export default function FilePreviewModal({
<Spinner /> <Spinner />
</div> </div>
) )
} else if (imageExts.includes(ext)) {
contentElement = (
<img src={data} alt="预览" className="max-w-full max-h-96" />
)
} else if (videoExts.includes(ext)) { } else if (videoExts.includes(ext)) {
contentElement = ( contentElement = <video src={data} controls className="max-w-full" />
<video src={data} controls className="max-w-full max-h-96" />
)
} else if (audioExts.includes(ext)) { } else if (audioExts.includes(ext)) {
contentElement = <audio src={data} controls className="w-full" /> contentElement = <audio src={data} controls className="w-full" />
} else {
contentElement = (
<div className="flex justify-center items-center h-full">
<Spinner />
</div>
)
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside"> <Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size="3xl">
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody className="flex justify-center items-center"> <ModalBody className="flex justify-center items-center">

View File

@@ -12,15 +12,19 @@ import {
TableRow TableRow
} from '@heroui/table' } from '@heroui/table'
import path from 'path-browserify' import path from 'path-browserify'
import { useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { BiRename } from 'react-icons/bi' import { BiRename } from 'react-icons/bi'
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi' import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi'
import { PhotoSlider } from 'react-photo-view'
import FileIcon from '@/components/file_icon' import FileIcon from '@/components/file_icon'
import type { FileInfo } from '@/controllers/file_manager' import type { FileInfo } from '@/controllers/file_manager'
interface FileTableProps { import { supportedPreviewExts } from './file_preview_modal'
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button'
export interface FileTableProps {
files: FileInfo[] files: FileInfo[]
currentPath: string currentPath: string
loading: boolean loading: boolean
@@ -62,143 +66,180 @@ export default function FileTable({
const start = (page - 1) * PAGE_SIZE const start = (page - 1) * PAGE_SIZE
const end = start + PAGE_SIZE const end = start + PAGE_SIZE
const displayFiles = files.slice(start, end) 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)
}, [files])
const onPreviewImage = (name: string, images: PreviewImage[]) => {
const index = images.findIndex((image) => image.key === name)
if (index === -1) {
return
}
setPreviewIndex(index)
setShowImage(true)
}
return ( return (
<Table <>
aria-label="文件列表" <PhotoSlider
sortDescriptor={sortDescriptor} images={previewImages}
onSortChange={onSortChange} visible={showImage}
onSelectionChange={onSelectionChange} onClose={() => setShowImage(false)}
defaultSelectedKeys={[]} index={previewIndex}
selectedKeys={selectedFiles} onIndexChange={setPreviewIndex}
selectionMode="multiple" />
bottomContent={ <Table
<div className="flex w-full justify-center"> aria-label="文件列表"
<Pagination sortDescriptor={sortDescriptor}
isCompact onSortChange={onSortChange}
showControls onSelectionChange={onSelectionChange}
showShadow defaultSelectedKeys={[]}
color="danger" selectedKeys={selectedFiles}
page={page} selectionMode="multiple"
total={pages} bottomContent={
onChange={(page) => setPage(page)} <div className="flex w-full justify-center">
/> <Pagination
</div> isCompact
} showControls
> showShadow
<TableHeader> color="danger"
<TableColumn key="name" allowsSorting> page={page}
total={pages}
</TableColumn> onChange={(page) => setPage(page)}
<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> </div>
} }
items={displayFiles}
> >
{(file: FileInfo) => { <TableHeader>
const filePath = path.join(currentPath, file.name) <TableColumn key="name" allowsSorting>
// 判断预览类型
const ext = path.extname(file.name).toLowerCase() </TableColumn>
const previewable = [ <TableColumn key="type" allowsSorting>
'.png',
'.jpg', </TableColumn>
'.jpeg', <TableColumn key="size" allowsSorting>
'.gif',
'.bmp', </TableColumn>
'.mp4', <TableColumn key="mtime" allowsSorting>
'.webm',
'.mp3', </TableColumn>
'.wav' <TableColumn key="actions"></TableColumn>
].includes(ext) </TableHeader>
return ( <TableBody
<TableRow key={file.name}> isLoading={loading}
<TableCell> loadingContent={
<Button <div className="flex justify-center items-center h-full">
variant="light" <Spinner />
onPress={() => </div>
file.isDirectory }
? onDirectoryClick(file.name) >
: previewable {displayFiles.map((file: FileInfo) => {
? onPreview(filePath) const filePath = path.join(currentPath, file.name)
: onEdit(filePath) const ext = path.extname(file.name).toLowerCase()
} const previewable = supportedPreviewExts.includes(ext)
className="text-left justify-start" const images = previewImages
startContent={ return (
<FileIcon name={file.name} isDirectory={file.isDirectory} /> <TableRow key={file.name}>
} <TableCell>
> {imageExts.includes(ext) ? (
{file.name} <ImageNameButton
</Button> name={file.name}
</TableCell> filePath={filePath}
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell> onPreview={() => onPreviewImage(file.name, images)}
<TableCell> onAddPreview={addPreviewImage}
{isNaN(file.size) || file.isDirectory />
? '-' ) : (
: `${file.size} 字节`} <Button
</TableCell> variant="light"
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell> onPress={() =>
<TableCell> file.isDirectory
<ButtonGroup size="sm"> ? onDirectoryClick(file.name)
<Button : previewable
isIconOnly ? onPreview(filePath)
color="danger" : onEdit(filePath)
variant="flat" }
onPress={() => onRenameRequest(file.name)} className="text-left justify-start"
> startContent={
<BiRename /> <FileIcon
</Button> name={file.name}
<Button isDirectory={file.isDirectory}
isIconOnly />
color="danger" }
variant="flat" >
onPress={() => onMoveRequest(file.name)} {file.name}
> </Button>
<FiMove /> )}
</Button> </TableCell>
<Button <TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
isIconOnly <TableCell>
color="danger" {isNaN(file.size) || file.isDirectory
variant="flat" ? '-'
onPress={() => onCopyPath(file.name)} : `${file.size} 字节`}
> </TableCell>
<FiCopy /> <TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
</Button> <TableCell>
<Button <ButtonGroup size="sm">
isIconOnly <Button
color="danger" isIconOnly
variant="flat" color="danger"
onPress={() => onDownload(filePath)} variant="flat"
> onPress={() => onRenameRequest(file.name)}
<FiDownload /> >
</Button> <BiRename />
<Button </Button>
isIconOnly <Button
color="danger" isIconOnly
variant="flat" color="danger"
onPress={() => onDelete(filePath)} variant="flat"
> onPress={() => onMoveRequest(file.name)}
<FiTrash2 /> >
</Button> <FiMove />
</ButtonGroup> </Button>
</TableCell> <Button
</TableRow> isIconOnly
) color="danger"
}} variant="flat"
</TableBody> onPress={() => onCopyPath(file.name)}
</Table> >
<FiCopy />
</Button>
<Button
isIconOnly
color="danger"
variant="flat"
onPress={() => onDownload(filePath)}
>
<FiDownload />
</Button>
<Button
isIconOnly
color="danger"
variant="flat"
onPress={() => onDelete(filePath)}
>
<FiTrash2 />
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</>
) )
} }

View File

@@ -0,0 +1,73 @@
import { Button } from '@heroui/button'
import { Image } from '@heroui/image'
import { Spinner } from '@heroui/spinner'
import { useRequest } from 'ahooks'
import path from 'path-browserify'
import { useEffect } from 'react'
import FileManager from '@/controllers/file_manager'
import FileIcon from '../file_icon'
export interface PreviewImage {
key: string
src: string
alt: string
}
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
export interface ImageNameButtonProps {
name: string
filePath: string
onPreview: () => void
onAddPreview: (image: PreviewImage) => void
}
export default function ImageNameButton({
name,
filePath,
onPreview,
onAddPreview
}: ImageNameButtonProps) {
const { data, loading, error, run } = useRequest(
async () => FileManager.downloadToURL(filePath),
{
refreshDeps: [filePath],
refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase()
if (!filePath || !imageExts.includes(ext)) {
return
}
run()
}
}
)
useEffect(() => {
if (data) {
onAddPreview({
key: name,
src: data,
alt: name
})
}
}, [data, name, onAddPreview])
return (
<Button
variant="light"
className="text-left justify-start"
onPress={onPreview}
startContent={
error ? (
<FileIcon name={name} isDirectory={false} />
) : loading || !data ? (
<Spinner size="sm" />
) : (
<Image src={data} alt={name} className="w-8 h-8" radius="sm" />
)
}
>
{name}
</Button>
)
}

View File

@@ -1,4 +1,5 @@
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import 'react-photo-view/dist/react-photo-view.css'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import App from '@/App.tsx' import App from '@/App.tsx'