'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 { useTranslations } from 'next-intl'; 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 t = useTranslations('files'); 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(t('uploadSuccess')); void queryClient.invalidateQueries({ queryKey: ['user-files'] }); }, onError: (err: Error) => { toast.error(err.message || t('uploadFailed')); }, onSettled: () => { setUploading(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } }, }); // 删除文件 Mutation const deleteMutation = useMutation({ mutationFn: (id: string) => services.upload.deleteMyFile(id), onSuccess: () => { toast.success(t('deleteSuccess')); void queryClient.invalidateQueries({ queryKey: ['user-files'] }); setDeleteTarget(null); }, onError: (err: Error) => { toast.error(err.message || t('deleteFailed')); }, }); 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 ? (

{t('noFiles')}

{debouncedKeyword ? t('noMatchingFiles') : t('uploadFirstFile')}

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

{file.file_name}

{formatFileSize(file.file_size)} {file.mime_type}
{formatDate(file.created_at)}
))}
)} {/* 分页 */} {totalPages > 1 && (
{page} / {totalPages}
)} {/* 删除确认 Dialog */} !open && setDeleteTarget(null)} > {t('confirmDeleteFile')} {t('confirmDeleteFileDesc', { name: deleteTarget?.file_name || '', })} {t('cancel')} deleteTarget && deleteMutation.mutate(deleteTarget.id) } disabled={deleteMutation.isPending} className='bg-destructive hover:bg-destructive/90 text-destructive-foreground' > {deleteMutation.isPending && ( )} {t('confirmDelete')}
); }