+
+ {`
@@ -138,5 +256,5 @@ export function TableTab() {
- )
+ );
}
diff --git a/frontend/app/(main)/admin/files/components/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx
index d5f7047f..eea4f8a3 100644
--- a/frontend/app/(main)/admin/files/components/file-list.tsx
+++ b/frontend/app/(main)/admin/files/components/file-list.tsx
@@ -1,8 +1,8 @@
-"use client"
+'use client';
-import * as React from "react"
-import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"
-import {AnimatePresence, motion} from "motion/react"
+import * as React from 'react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { AnimatePresence, motion } from 'motion/react';
import {
Download,
Eye,
@@ -16,14 +16,21 @@ import {
Trash2,
Upload,
X,
-} from "lucide-react"
-import {toast} from "sonner"
+} from 'lucide-react';
+import { toast } from 'sonner';
-import {Button} from "@/components/ui/button"
-import {Input} from "@/components/ui/input"
-import {Badge} from "@/components/ui/badge"
-import {Checkbox} from "@/components/ui/checkbox"
-import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
import {
AlertDialog,
AlertDialogAction,
@@ -33,207 +40,236 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle} from "@/components/ui/sheet"
-import {FileImagePreview} from "@/components/common/file-image-preview"
-import services, {formatFileSize} from "@/lib/services"
-import type {Upload as UploadRecord} from "@/lib/services/upload/types"
+} from '@/components/ui/alert-dialog';
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet';
+import { FileImagePreview } from '@/components/common/file-image-preview';
+import services, { formatFileSize } from '@/lib/services';
+import type { Upload as UploadRecord } from '@/lib/services/upload/types';
/* ─── 工具函数 ─────────────────────────────────────────── */
-function getFileIcon(mimeType: string, className = "size-8") {
- if (mimeType.startsWith("image/")) return
- if (mimeType.startsWith("video/")) return
- if (mimeType.startsWith("audio/")) return
- if (mimeType.includes("zip") || mimeType.includes("tar") || mimeType.includes("gzip"))
- return
- return
+function getFileIcon(mimeType: string, className = 'size-8') {
+ if (mimeType.startsWith('image/'))
+ return ;
+ if (mimeType.startsWith('video/'))
+ return ;
+ if (mimeType.startsWith('audio/'))
+ return ;
+ if (
+ mimeType.includes('zip') ||
+ mimeType.includes('tar') ||
+ mimeType.includes('gzip')
+ )
+ return ;
+ return ;
}
function formatDate(dateStr: string) {
- return new Date(dateStr).toLocaleString("zh-CN", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- })
+ return new Date(dateStr).toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
}
export function FileList() {
- const queryClient = useQueryClient()
- const [keyword, setKeyword] = React.useState("")
- const [debouncedKeyword, setDebouncedKeyword] = React.useState("")
- const [selectedIds, setSelectedIds] = React.useState>(new Set())
- const [deleteTarget, setDeleteTarget] = React.useState(null)
- const [detailTarget, setDetailTarget] = React.useState(null)
- const [page, setPage] = React.useState(1)
- const pageSize = 15 // 表格视图下 15 条更紧凑
+ const queryClient = useQueryClient();
+ const [keyword, setKeyword] = React.useState('');
+ const [debouncedKeyword, setDebouncedKeyword] = React.useState('');
+ const [selectedIds, setSelectedIds] = React.useState>(new Set());
+ const [deleteTarget, setDeleteTarget] = React.useState(
+ null,
+ );
+ const [detailTarget, setDetailTarget] = React.useState(
+ null,
+ );
+ const [page, setPage] = React.useState(1);
+ const pageSize = 15; // 表格视图下 15 条更紧凑
// 搜索防抖
React.useEffect(() => {
const timer = setTimeout(() => {
- setDebouncedKeyword(keyword)
- setPage(1)
- }, 400)
- return () => clearTimeout(timer)
- }, [keyword])
+ setDebouncedKeyword(keyword);
+ setPage(1);
+ }, 400);
+ return () => clearTimeout(timer);
+ }, [keyword]);
// 文件列表查询
const listQuery = useQuery({
- queryKey: ["files", "all", page, pageSize, debouncedKeyword],
- queryFn: () => services.adminUpload.listUploads(page, pageSize, debouncedKeyword || undefined),
- })
+ queryKey: ['files', 'all', page, pageSize, debouncedKeyword],
+ queryFn: () =>
+ services.adminUpload.listUploads(
+ page,
+ pageSize,
+ debouncedKeyword || undefined,
+ ),
+ });
const storageDriverQuery = useQuery({
- queryKey: ["admin", "storage-config", "driver"],
+ queryKey: ['admin', 'storage-config', 'driver'],
queryFn: async () => {
- const record = await services.adminSystemConfig.getSystemConfig("storage_config")
- const cfg = JSON.parse(record.value) as {driver?: string}
- return cfg.driver ?? "local"
+ const record =
+ await services.adminSystemConfig.getSystemConfig('storage_config');
+ const cfg = JSON.parse(record.value) as { driver?: string };
+ return cfg.driver ?? 'local';
},
- })
+ });
- const files = listQuery.data?.items ?? []
- const total = listQuery.data?.total ?? 0
- const totalPages = Math.ceil(total / pageSize)
+ const files = listQuery.data?.items ?? [];
+ const total = listQuery.data?.total ?? 0;
+ const totalPages = Math.ceil(total / pageSize);
- const isAllSelected = files.length > 0 && files.every((f) => selectedIds.has(f.id))
- const isSomeSelected = files.length > 0 && files.some((f) => selectedIds.has(f.id)) && !isAllSelected
+ const isAllSelected =
+ files.length > 0 && files.every((f) => selectedIds.has(f.id));
+ const isSomeSelected =
+ files.length > 0 &&
+ files.some((f) => selectedIds.has(f.id)) &&
+ !isAllSelected;
const handleSelectAll = () => {
if (isAllSelected) {
setSelectedIds((prev) => {
- const next = new Set(prev)
- files.forEach((f) => next.delete(f.id))
- return next
- })
+ const next = new Set(prev);
+ files.forEach((f) => next.delete(f.id));
+ return next;
+ });
} else {
setSelectedIds((prev) => {
- const next = new Set(prev)
- files.forEach((f) => next.add(f.id))
- return next
- })
+ const next = new Set(prev);
+ files.forEach((f) => next.add(f.id));
+ return next;
+ });
}
- }
+ };
// 删除单文件
const deleteMutation = useMutation({
mutationFn: (id: string) => services.adminUpload.deleteFile(id),
onSuccess: () => {
- void queryClient.invalidateQueries({ queryKey: ["files", "my"] })
- void queryClient.invalidateQueries({ queryKey: ["files", "stats"] })
- toast.success("文件已删除")
- setDeleteTarget(null)
+ void queryClient.invalidateQueries({ queryKey: ['files', 'my'] });
+ void queryClient.invalidateQueries({ queryKey: ['files', 'stats'] });
+ toast.success('文件已删除');
+ setDeleteTarget(null);
},
- onError: (err: Error) => toast.error(err.message || "删除失败"),
- })
+ onError: (err: Error) => toast.error(err.message || '删除失败'),
+ });
// 批量 ZIP 下载
const batchDownloadMutation = useMutation({
mutationFn: (ids: string[]) => services.adminUpload.batchDownload(ids),
onSuccess: (blob) => {
- const url = URL.createObjectURL(blob)
- const a = document.createElement("a")
- a.href = url
- a.download = "batch_download.zip"
- a.click()
- URL.revokeObjectURL(url)
- toast.success("批量下载已开始")
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'batch_download.zip';
+ a.click();
+ URL.revokeObjectURL(url);
+ toast.success('批量下载已开始');
},
- onError: () => toast.error("批量下载失败"),
- })
+ onError: () => toast.error('批量下载失败'),
+ });
const toggleSelect = (id: string) => {
setSelectedIds((prev) => {
- const next = new Set(prev)
+ const next = new Set(prev);
if (next.has(id)) {
- next.delete(id)
+ next.delete(id);
} else {
- next.add(id)
+ next.add(id);
}
- return next
- })
- }
+ return next;
+ });
+ };
- const clearSelection = () => setSelectedIds(new Set())
+ const clearSelection = () => setSelectedIds(new Set());
- const selectAll = () => setSelectedIds(new Set(files.map((f) => f.id)))
+ const selectAll = () => setSelectedIds(new Set(files.map((f) => f.id)));
const handleDownload = (file: UploadRecord) => {
- const url = services.adminUpload.getDownloadUrl(file.id)
- const a = document.createElement("a")
- a.href = url
- a.download = file.file_name
- a.click()
- }
+ const url = services.adminUpload.getDownloadUrl(file.id);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = file.file_name;
+ a.click();
+ };
return (
-
+
{/* 搜索栏 */}
-
-
-
+
+
+
setKeyword(e.target.value)}
/>
{keyword && (
)}
{files.length > 0 && (
)}
{/* 打包下载等批量操作 */}
-
+
{selectedIds.size > 0 && (
-
+
已选 {selectedIds.size} 个
)}
@@ -241,131 +277,152 @@ export function FileList() {
{total > 0 && (
-
共 {total} 个文件
+
+ 共 {total} 个文件
+
)}
{/* 文件列表 Table */}
{listQuery.isPending ? (
-
-
+
+
) : files.length === 0 ? (
-
-
-
- {debouncedKeyword ? "没有匹配的文件" : "您还没有上传任何文件"}
+
+
+
+ {debouncedKeyword ? '没有匹配的文件' : '您还没有上传任何文件'}
) : (
-
+
-
-
+
+
- 预览
- ID
- 文件名
- 路径
- 业务类别
- MIME类型
- 大小
- 上传时间
- 操作
+ 预览
+ ID
+ 文件名
+
+ 路径
+
+ 业务类别
+ MIME类型
+ 大小
+ 上传时间
+
+ 操作
+
{files.map((file) => {
- const isSelected = selectedIds.has(file.id)
+ const isSelected = selectedIds.has(file.id);
return (
-
+
toggleSelect(file.id)}
/>
-
-
- {file.mime_type.startsWith("image/") ? (
+
+
+ {file.mime_type.startsWith('image/') ? (
) : (
- getFileIcon(file.mime_type, "size-4.5")
+ getFileIcon(file.mime_type, 'size-4.5')
)}
-
+
{file.id}
-
+
{file.file_name}
-
+
{file.file_path}
-
-
+
+
{file.type}
-
+
{file.mime_type}
-
+
{formatFileSize(file.file_size)}
-
+
{formatDate(file.created_at)}
-
-
+
+
- )
+ );
})}
@@ -374,23 +431,23 @@ export function FileList() {
{/* 分页 */}
{totalPages > 1 && (
-
+
-
+
{page} / {totalPages}