"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" /> {/* 上传权限设置 */} setUploadAccessMode(0)} > 私有 setUploadAccessMode(1)} > 公开 {uploading ? ( ) : ( )} {uploading ? "正在上传..." : "上传文件"} {/* 文件展示区 */} {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)} handleDownload(file)} > setDeleteTarget(file)} > ))} )} {/* 分页 */} {totalPages > 1 && ( setPage((p) => p - 1)} > 上一页 {page} / {totalPages} = totalPages} onClick={() => setPage((p) => p + 1)} > 下一页 )} {/* 删除确认 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 && } 确认删除 ) }
{debouncedKeyword ? "未搜索到匹配的文件" : "上传您的第一个文件,支持图片、视频、文档和压缩包等格式。"}