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'; 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; /** 上传模式:拖放模式或传统模式 */ uploadMode?: 'dragdrop' | 'traditional'; /** 是否显示已上传文件列表 */ showUploadList?: boolean; /** 已上传文件列表标题 */ uploadListTitle?: string; /** 组件尺寸模式 */ size?: 'default' | 'compact' | 'minimal'; /** 显示模式:卡片模式或完整模式 */ displayMode?: 'full' | 'card'; } // 定义上传文件状态 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', uploadMode = 'dragdrop', showUploadList = true, uploadListTitle = '上传进度', size = 'default', displayMode = 'full' }) => { const [fileList, setFileList] = useState([]); const [dragActive, setDragActive] = useState(false); // 根据尺寸模式获取样式配置 const getSizeConfig = () => { switch (size) { case 'minimal': return { container: 'p-3', icon: 'h-8 w-8', title: 'text-sm', description: 'text-xs', button: 'h-8 px-3 text-xs', spacing: 'space-y-2', fileList: 'space-y-2', cardPadding: 'p-3', progressHeight: 'h-1' }; case 'compact': return { container: 'p-4', icon: 'h-10 w-10', title: 'text-base', description: 'text-sm', button: 'h-9 px-4 text-sm', spacing: 'space-y-3', fileList: 'space-y-3', cardPadding: 'p-4', progressHeight: 'h-2' }; default: return { container: 'p-6', icon: 'h-12 w-12', title: 'text-lg', description: 'text-sm', button: 'h-10 px-4', spacing: 'space-y-4', fileList: 'space-y-4', cardPadding: 'p-6', progressHeight: 'h-2' }; } }; // 处理上传进度 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; }) ); // 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; }) ); // 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, } ]); 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, iconSize: 'small' | 'normal' = 'normal') => { const sizeClass = iconSize === 'small' ? 'h-4 w-4' : 'h-8 w-8'; 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 ; } }; const sizeConfig = getSizeConfig(); // 卡片模式渲染 if (displayMode === 'card') { return (
); } return (
{/* 上传区域 - 根据模式显示不同界面 */} {uploadMode === 'dragdrop' ? (

{buttonText}

{size !== 'minimal' && (

{tipText}

)}
) : (

{buttonText}

{size !== 'minimal' && (

{tipText}

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

{uploadListTitle}

{fileList.map(item => (
{renderFileIcon(item.type, size === 'minimal' ? 'small' : 'normal')}

{item.name}

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

{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;