'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 ? '未搜索到匹配的文件' : '上传您的第一个文件,支持图片、视频、文档和压缩包等格式。'}