"use client" import * as React from "react" import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import { ChevronLeft, ChevronRight, Download, FileArchive, FileAudio, FileImage, FileText, FileVideo, Globe, Loader2, Lock, Plus, Search, Trash2, Upload } 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 {Card, CardContent} from "@/components/ui/card" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" 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-10") { 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 UserFileManager() { const queryClient = useQueryClient() const [keyword, setKeyword] = React.useState("") const [debouncedKeyword, setDebouncedKeyword] = React.useState("") const [page, setPage] = React.useState(1) const [uploading, setUploading] = React.useState(false) const [deleteTarget, setDeleteTarget] = React.useState(null) const [uploadAccessMode, setUploadAccessMode] = React.useState(0) // 默认私有 const fileInputRef = React.useRef(null) const pageSize = 12 // 每页 12 个卡片更均衡(3x4 或 4x3 布局) // 搜索防抖 React.useEffect(() => { const timer = setTimeout(() => { setDebouncedKeyword(keyword) setPage(1) }, 400) return () => clearTimeout(timer) }, [keyword]) // 我的文件列表查询 const { data, isPending } = useQuery({ queryKey: ["user-files", page, pageSize, debouncedKeyword], queryFn: () => services.upload.listMyUploads(page, pageSize, debouncedKeyword || undefined), }) const files = data?.items ?? [] const total = data?.total ?? 0 const totalPages = Math.ceil(total / pageSize) // 上传文件 Mutation const uploadMutation = useMutation({ mutationFn: async (file: File) => { return services.upload.uploadFile(file, "generic", undefined, uploadAccessMode) }, onMutate: () => { setUploading(true) }, onSuccess: () => { toast.success("文件上传成功") void queryClient.invalidateQueries({ queryKey: ["user-files"] }) }, onError: (err: Error) => { toast.error(err.message || "上传失败") }, onSettled: () => { setUploading(false) if (fileInputRef.current) { fileInputRef.current.value = "" } } }) // 删除文件 Mutation const deleteMutation = useMutation({ mutationFn: (id: string) => services.upload.deleteMyFile(id), onSuccess: () => { toast.success("文件已成功删除") void queryClient.invalidateQueries({ queryKey: ["user-files"] }) setDeleteTarget(null) }, onError: (err: Error) => { toast.error(err.message || "删除失败") } }) const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { uploadMutation.mutate(file) } } const triggerUploadClick = () => { fileInputRef.current?.click() } const handleDownload = (file: UploadRecord) => { const url = services.upload.getDownloadUrl(file.id) const a = document.createElement("a") a.href = url a.download = file.file_name a.click() } return (
{/* 操作栏:搜索、权限选择、上传按钮 */}
setKeyword(e.target.value)} className="pl-9 h-9" />
{/* 上传权限设置 */}
{/* 文件展示区 */} {isPending ? (
) : files.length === 0 ? (

还没有上传文件

{debouncedKeyword ? "未搜索到匹配的文件" : "上传您的第一个文件,支持图片、视频、文档和压缩包等格式。"}

{!debouncedKeyword && ( )}
) : (
{files.map((file) => ( {/* 文件预览/图标区 */}
{file.mime_type.startsWith("image/") ? ( ) : ( getFileIcon(file.mime_type) )} {/* 访问权限 Badge */}
{file.access_mode === 0 ? ( 私有 ) : ( 公开 )}
{/* 文件详情区 */}

{file.file_name}

{formatFileSize(file.file_size)} {file.mime_type}
{formatDate(file.created_at)}
))}
)} {/* 分页 */} {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 && } 确认删除
) }