import React, { useState, useCallback } from 'react'; import { Button } from '@/client/components/ui/button'; import { Card, CardContent } from '@/client/components/ui/card'; import { Progress } from '@/client/components/ui/progress'; import { Badge } from '@/client/components/ui/badge'; import { toast } from 'sonner'; import { Upload, X, CheckCircle, Loader2, FileText } from 'lucide-react'; import { uploadMinIOWithPolicy, MinioProgressEvent } from '@/client/utils/minio'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; import type { RcFile } from 'rc-upload/lib/interface'; interface MinioUploaderProps { /** 上传路径 */ uploadPath: string; /** 允许的文件类型,如['image/*', '.pdf'] */ accept?: string; /** 最大文件大小(MB) */ maxSize?: number; /** 是否允许多文件上传 */ multiple?: boolean; /** 上传成功回调 */ onUploadSuccess?: (fileKey: string, fileUrl: string, file: File) => void; /** 上传失败回调 */ onUploadError?: (error: Error, file: File) => void; /** 自定义上传按钮文本 */ buttonText?: string; /** 自定义提示文本 */ tipText?: string; } // 定义上传文件状态 interface UploadFile { uid: string; name: string; size: number; type?: string; status: 'uploading' | 'success' | 'error'; percent: number; error?: string; url?: string; } const MinioUploader: React.FC = ({ uploadPath = '/', accept, maxSize = 500, // 默认最大500MB multiple = false, onUploadSuccess, onUploadError, buttonText = '点击或拖拽上传文件', tipText = '支持单文件或多文件上传,单个文件大小不超过500MB' }) => { const [fileList, setFileList] = useState([]); const [uploadingFiles, setUploadingFiles] = useState>(new Set()); const [dragActive, setDragActive] = useState(false); // 处理上传进度 const handleProgress = useCallback((uid: string, event: MinioProgressEvent) => { setFileList(prev => prev.map(item => { if (item.uid === uid) { return { ...item, status: event.stage === 'error' ? 'error' : 'uploading', percent: event.progress, error: event.stage === 'error' ? event.message : undefined }; } return item; }) ); }, []); // 处理上传成功 const handleComplete = useCallback((uid: string, result: { fileKey: string; fileUrl: string }, file: File) => { setFileList(prev => prev.map(item => { if (item.uid === uid) { return { ...item, status: 'success', percent: 100, url: result.fileUrl, }; } return item; }) ); setUploadingFiles(prev => { const newSet = new Set(prev); newSet.delete(uid); return newSet; }); toast.success(`文件 "${file.name}" 上传成功`); onUploadSuccess?.(result.fileKey, result.fileUrl, file); }, [onUploadSuccess]); // 处理上传失败 const handleError = useCallback((uid: string, error: Error, file: File) => { setFileList(prev => prev.map(item => { if (item.uid === uid) { return { ...item, status: 'error', percent: 0, error: error.message || '上传失败' }; } return item; }) ); setUploadingFiles(prev => { const newSet = new Set(prev); newSet.delete(uid); return newSet; }); toast.error(`文件 "${file.name}" 上传失败: ${error.message}`); onUploadError?.(error, file); }, [onUploadError]); // 自定义上传逻辑 const handleUpload = async (file: File) => { const uid = Date.now().toString() + Math.random().toString(36).substr(2, 9); // 添加到文件列表 setFileList(prev => [ ...prev, { uid, name: file.name, size: file.size, type: file.type, status: 'uploading', percent: 0, } ]); // 添加到上传中集合 setUploadingFiles(prev => new Set(prev).add(uid)); try { // 验证文件大小 const fileSizeMB = file.size / (1024 * 1024); if (fileSizeMB > maxSize) { throw new Error(`文件大小超过 ${maxSize}MB 限制`); } // 调用minio上传方法 const result = await uploadMinIOWithPolicy( uploadPath, file, file.name, { onProgress: (event) => handleProgress(uid, event), signal: new AbortController().signal } ); handleComplete(uid, result, file); } catch (error) { handleError(uid, error instanceof Error ? error : new Error('未知错误'), file); } }; // 处理文件选择 const handleFileSelect = (files: FileList) => { if (!files || files.length === 0) return; const fileArray = Array.from(files); if (!multiple && fileArray.length > 1) { toast.error('不支持多文件上传'); return; } fileArray.forEach(file => handleUpload(file)); }; // 处理拖拽 const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === 'dragenter' || e.type === 'dragover') { setDragActive(true); } else if (e.type === 'dragleave') { setDragActive(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); const files = e.dataTransfer.files; handleFileSelect(files); }; // 处理文件移除 const handleRemove = (uid: string) => { setFileList(prev => prev.filter(item => item.uid !== uid)); }; // 渲染上传状态 const renderUploadStatus = (item: UploadFile) => { switch (item.status) { case 'uploading': return (
{Math.round(item.percent)}%
); case 'success': return (
上传成功
); case 'error': return (
×
{item.error || '上传失败'}
); default: return null; } }; // 渲染文件图标 const renderFileIcon = (type?: string) => { if (type?.startsWith('image/')) { return ; } else if (type?.startsWith('video/')) { return ; } else if (type?.startsWith('audio/')) { return ; } else if (type?.includes('pdf')) { return ; } else if (type?.includes('word')) { return ; } else if (type?.includes('excel') || type?.includes('sheet')) { return ; } else { return ; } }; return (
{/* 拖拽上传区域 */}

{buttonText}

{tipText}

{/* 上传进度列表 */} {fileList.length > 0 && (

上传进度

{fileList.map(item => (
{renderFileIcon(item.type)}

{item.name}

{renderUploadStatus(item)}
{item.status === 'uploading' && (

{Math.round(item.percent)}% - {formatFileSize(item.size * (item.percent / 100))} / {formatFileSize(item.size)}

)}
))}
)}
); }; // 辅助函数:格式化文件大小 const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; export default MinioUploader;