"use client" import * as React from "react" import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import {AnimatePresence, motion} from "motion/react" import { Download, FileArchive, FileAudio, FileImage, FileText, FileVideo, Loader2, Search, Trash2, Upload, X, } 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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import {formatFileSize, UploadService} from "@/lib/services/upload/upload.service" 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 formatDate(dateStr: string) { return new Date(dateStr).toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }) } /* ─── 文件管理主组件 ────────────────────────────────────── */ export function FilesMain() { 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 [page, setPage] = React.useState(1) const pageSize = 24 // 搜索防抖 React.useEffect(() => { const timer = setTimeout(() => { setDebouncedKeyword(keyword) setPage(1) }, 400) return () => clearTimeout(timer) }, [keyword]) // 文件列表查询 const listQuery = useQuery({ queryKey: ["files", "my", page, pageSize, debouncedKeyword], queryFn: () => UploadService.listMyFiles(page, pageSize, debouncedKeyword || undefined), }) const files = listQuery.data?.items ?? [] const total = listQuery.data?.total ?? 0 const totalPages = Math.ceil(total / pageSize) // 删除单文件 const deleteMutation = useMutation({ mutationFn: (id: string) => UploadService.deleteFile(id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["files", "my"] }) toast.success("文件已删除") setDeleteTarget(null) }, onError: (err: Error) => toast.error(err.message || "删除失败"), }) // 批量 ZIP 下载 const batchDownloadMutation = useMutation({ mutationFn: (ids: string[]) => UploadService.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("批量下载已开始") }, onError: () => toast.error("批量下载失败"), }) const toggleSelect = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev) next.has(id) ? next.delete(id) : next.add(id) return next }) } const clearSelection = () => setSelectedIds(new Set()) const selectAll = () => setSelectedIds(new Set(files.map((f) => f.id))) const handleDownload = (file: UploadRecord) => { const url = UploadService.getDownloadUrl(file.id) const a = document.createElement("a") a.href = url a.download = file.file_name a.click() } return ( {/* 头部 */}

文件管理

管理您上传的所有文件,支持下载 and 批量操作

{/* 操作按钮区 */}
{selectedIds.size > 0 && ( 已选 {selectedIds.size} 个 )}
{/* 搜索栏 */}
setKeyword(e.target.value)} /> {keyword && ( )}
{files.length > 0 && ( )} {total > 0 && ( 共 {total} 个文件 )}
{/* 文件网格 */} {listQuery.isPending ? (
) : files.length === 0 ? (

{debouncedKeyword ? "没有匹配的文件" : "您还没有上传任何文件"}

) : (
{files.map((file, idx) => { const isSelected = selectedIds.has(file.id) return ( toggleSelect(file.id)} > {/* 选中指示 */} {isSelected && (
)} {/* 文件图标 / 图片预览 */}
{file.mime_type.startsWith("image/") ? ( // eslint-disable-next-line @next/next/no-img-element {file.file_name} { ;(e.currentTarget as HTMLImageElement).style.display = "none" }} /> ) : ( getFileIcon(file.mime_type) )}
{/* 文件名 */}

{file.file_name}

{/* 元数据 */}

{formatFileSize(file.file_size)}

{formatDate(file.created_at)}

{/* Hover 操作按钮 */}
e.stopPropagation()} >
) })}
)} {/* 分页 */} {totalPages > 1 && (
{page} / {totalPages}
)} {/* 删除确认 Dialog */} !open && setDeleteTarget(null)}> 确认删除文件 确定要删除文件{" "} 「{deleteTarget?.file_name}」{" "} 吗?此操作不可撤销。 取消 deleteTarget && deleteMutation.mutate(deleteTarget.id)} disabled={deleteMutation.isPending} className="bg-destructive hover:bg-destructive/90 text-destructive-foreground" > {deleteMutation.isPending && } 确认删除
) }