'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' /> {/* 上传权限设置 */} setUploadAccessMode(0)} > {t('private')} setUploadAccessMode(1)} > {t('public')} {uploading ? ( ) : ( )} {uploading ? t('uploading') : t('uploadFile')} {/* 文件展示区 */} {isPending ? ( ) : files.length === 0 ? ( {t('noFiles')} {debouncedKeyword ? t('noMatchingFiles') : t('uploadFirstFile')} {!debouncedKeyword && ( {t('selectFileUpload')} )} ) : ( {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)} handleDownload(file)} > setDeleteTarget(file)} > ))} )} {/* 分页 */} {totalPages > 1 && ( setPage((p) => p - 1)} > {t('prevPage')} {page} / {totalPages} = totalPages} onClick={() => setPage((p) => p + 1)} > {t('nextPage')} )} {/* 删除确认 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')} ); }
{debouncedKeyword ? t('noMatchingFiles') : t('uploadFirstFile')}