Skip to content

Commit

Permalink
feat: 文件下载/上传
Browse files Browse the repository at this point in the history
  • Loading branch information
bietiaop committed Feb 3, 2025
1 parent 8059373 commit b32f9fa
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 72 deletions.
2 changes: 2 additions & 0 deletions napcat.webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@heroui/listbox": "2.3.10",
"@heroui/modal": "2.2.8",
"@heroui/navbar": "2.2.9",
"@heroui/pagination": "^2.2.9",
"@heroui/popover": "2.3.10",
"@heroui/select": "2.4.10",
"@heroui/slider": "2.4.8",
Expand Down Expand Up @@ -63,6 +64,7 @@
"quill": "^2.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.5",
"react-error-boundary": "^5.0.0",
"react-hook-form": "^7.54.2",
"react-hot-toast": "^2.4.1",
Expand Down
85 changes: 85 additions & 0 deletions napcat.webui/src/components/file_manage/file_preview_modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Button } from '@heroui/button'
import {
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader
} from '@heroui/modal'
import { Spinner } from '@heroui/spinner'
import { useRequest } from 'ahooks'
import path from 'path-browserify'

import FileManager from '@/controllers/file_manager'

interface FilePreviewModalProps {
isOpen: boolean
filePath: string
onClose: () => void
}

const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
const videoExts = ['.mp4', '.webm']
const audioExts = ['.mp3', '.wav']

const supportedPreviewExts = [...imageExts, ...videoExts, ...audioExts]

export default function FilePreviewModal({
isOpen,
filePath,
onClose
}: FilePreviewModalProps) {
const ext = path.extname(filePath).toLowerCase()
const { data, loading, error, run } = useRequest(
async (path: string) => FileManager.downloadToURL(path),
{
refreshDeps: [filePath],
refreshDepsAction: () => {
const ext = path.extname(filePath).toLowerCase()
if (!filePath || !supportedPreviewExts.includes(ext)) {
return
}
run(filePath)
}
}
)

let contentElement = null
if (!supportedPreviewExts.includes(ext)) {
contentElement = <div>暂不支持预览此文件类型</div>
} else if (error) {
contentElement = <div>读取文件失败</div>
} else if (loading || !data) {
contentElement = (
<div className="flex justify-center items-center h-full">
<Spinner />
</div>
)
} else if (imageExts.includes(ext)) {
contentElement = (
<img src={data} alt="预览" className="max-w-full max-h-96" />
)
} else if (videoExts.includes(ext)) {
contentElement = (
<video src={data} controls className="max-w-full max-h-96" />
)
} else if (audioExts.includes(ext)) {
contentElement = <audio src={data} controls className="w-full" />
}

return (
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside">
<ModalContent>
<ModalHeader>文件预览</ModalHeader>
<ModalBody className="flex justify-center items-center">
{contentElement}
</ModalBody>
<ModalFooter>
<Button color="danger" variant="flat" onPress={onClose}>
关闭
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
129 changes: 87 additions & 42 deletions napcat.webui/src/components/file_manage/file_table.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Button, ButtonGroup } from '@heroui/button'
import { Pagination } from '@heroui/pagination'
import { Spinner } from '@heroui/spinner'
import {
type Selection,
Expand All @@ -10,10 +11,10 @@ import {
TableHeader,
TableRow
} from '@heroui/table'
import { Tooltip } from '@heroui/tooltip'
import path from 'path-browserify'
import { useState } from 'react'
import { BiRename } from 'react-icons/bi'
import { FiCopy, FiMove, FiTrash2 } from 'react-icons/fi'
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi'

import FileIcon from '@/components/file_icon'

Expand All @@ -29,12 +30,16 @@ interface FileTableProps {
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,
Expand All @@ -45,11 +50,18 @@ export default function FileTable({
onSelectionChange,
onDirectoryClick,
onEdit,
onPreview,
onRenameRequest,
onMoveRequest,
onCopyPath,
onDelete
onDelete,
onDownload
}: FileTableProps) {
const [page, setPage] = useState(1)
const pages = Math.ceil(files.length / PAGE_SIZE)
const start = (page - 1) * PAGE_SIZE
const end = start + PAGE_SIZE
const displayFiles = files.slice(start, end)
return (
<Table
aria-label="文件列表"
Expand All @@ -59,6 +71,19 @@ export default function FileTable({
defaultSelectedKeys={[]}
selectedKeys={selectedFiles}
selectionMode="multiple"
bottomContent={
<div className="flex w-full justify-center">
<Pagination
isCompact
showControls
showShadow
color="danger"
page={page}
total={pages}
onChange={(page) => setPage(page)}
/>
</div>
}
>
<TableHeader>
<TableColumn key="name" allowsSorting>
Expand All @@ -82,34 +107,52 @@ export default function FileTable({
<Spinner />
</div>
}
items={files}
items={displayFiles}
>
{(file: FileInfo) => (
<TableRow key={file.name}>
<TableCell>
<Button
variant="light"
onPress={() =>
file.isDirectory
? onDirectoryClick(file.name)
: onEdit(path.join(currentPath, file.name))
}
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">
<Tooltip content="重命名">
{(file: FileInfo) => {
const filePath = path.join(currentPath, file.name)
// 判断预览类型
const ext = path.extname(file.name).toLowerCase()
const previewable = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.bmp',
'.mp4',
'.webm',
'.mp3',
'.wav'
].includes(ext)
return (
<TableRow key={file.name}>
<TableCell>
<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="danger"
Expand All @@ -118,8 +161,6 @@ export default function FileTable({
>
<BiRename />
</Button>
</Tooltip>
<Tooltip content="移动">
<Button
isIconOnly
color="danger"
Expand All @@ -128,8 +169,6 @@ export default function FileTable({
>
<FiMove />
</Button>
</Tooltip>
<Tooltip content="复制路径">
<Button
isIconOnly
color="danger"
Expand All @@ -138,21 +177,27 @@ export default function FileTable({
>
<FiCopy />
</Button>
</Tooltip>
<Tooltip content="删除">
<Button
isIconOnly
color="danger"
variant="flat"
onPress={() => onDelete(path.join(currentPath, file.name))}
onPress={() => onDownload(filePath)}
>
<FiDownload />
</Button>
<Button
isIconOnly
color="danger"
variant="flat"
onPress={() => onDelete(filePath)}
>
<FiTrash2 />
</Button>
</Tooltip>
</ButtonGroup>
</TableCell>
</TableRow>
)}
</ButtonGroup>
</TableCell>
</TableRow>
)
}}
</TableBody>
</Table>
)
Expand Down
Loading

0 comments on commit b32f9fa

Please sign in to comment.