|
|
@@ -6,8 +6,6 @@ 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 {
|
|
|
/** 上传路径 */
|
|
|
@@ -26,6 +24,8 @@ interface MinioUploaderProps {
|
|
|
buttonText?: string;
|
|
|
/** 自定义提示文本 */
|
|
|
tipText?: string;
|
|
|
+ /** 组件尺寸模式 */
|
|
|
+ size?: 'default' | 'compact' | 'minimal';
|
|
|
}
|
|
|
|
|
|
// 定义上传文件状态
|
|
|
@@ -48,12 +48,55 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
|
|
|
onUploadSuccess,
|
|
|
onUploadError,
|
|
|
buttonText = '点击或拖拽上传文件',
|
|
|
- tipText = '支持单文件或多文件上传,单个文件大小不超过500MB'
|
|
|
+ tipText = '支持单文件或多文件上传,单个文件大小不超过500MB',
|
|
|
+ size = 'default'
|
|
|
}) => {
|
|
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
|
const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
|
|
|
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 =>
|
|
|
@@ -241,47 +284,54 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
|
|
|
};
|
|
|
|
|
|
// 渲染文件图标
|
|
|
- const renderFileIcon = (type?: string) => {
|
|
|
+ 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 <FileText className="h-8 w-8 text-blue-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-blue-500`} />;
|
|
|
} else if (type?.startsWith('video/')) {
|
|
|
- return <FileText className="h-8 w-8 text-red-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-red-500`} />;
|
|
|
} else if (type?.startsWith('audio/')) {
|
|
|
- return <FileText className="h-8 w-8 text-purple-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-purple-500`} />;
|
|
|
} else if (type?.includes('pdf')) {
|
|
|
- return <FileText className="h-8 w-8 text-red-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-red-500`} />;
|
|
|
} else if (type?.includes('word')) {
|
|
|
- return <FileText className="h-8 w-8 text-blue-600" />;
|
|
|
+ return <FileText className={`${sizeClass} text-blue-600`} />;
|
|
|
} else if (type?.includes('excel') || type?.includes('sheet')) {
|
|
|
- return <FileText className="h-8 w-8 text-green-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-green-500`} />;
|
|
|
} else {
|
|
|
- return <FileText className="h-8 w-8 text-gray-500" />;
|
|
|
+ return <FileText className={`${sizeClass} text-gray-500`} />;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const sizeConfig = getSizeConfig();
|
|
|
+
|
|
|
return (
|
|
|
- <div className="space-y-4">
|
|
|
+ <div className={sizeConfig.spacing}>
|
|
|
{/* 拖拽上传区域 */}
|
|
|
<div
|
|
|
- className={`relative border-2 border-dashed rounded-lg p-6 transition-all ${
|
|
|
- dragActive
|
|
|
- ? 'border-primary bg-primary/5'
|
|
|
+ className={`relative border-2 border-dashed rounded-lg transition-all ${
|
|
|
+ dragActive
|
|
|
+ ? 'border-primary bg-primary/5'
|
|
|
: 'border-gray-300 hover:border-primary/50'
|
|
|
- }`}
|
|
|
+ } ${sizeConfig.container}`}
|
|
|
onDragEnter={handleDrag}
|
|
|
onDragLeave={handleDrag}
|
|
|
onDragOver={handleDrag}
|
|
|
onDrop={handleDrop}
|
|
|
>
|
|
|
- <div className="flex flex-col items-center justify-center space-y-4">
|
|
|
- <Upload className={`h-12 w-12 ${dragActive ? 'text-primary' : 'text-gray-400'}`} />
|
|
|
+ <div className={`flex flex-col items-center justify-center ${sizeConfig.spacing}`}>
|
|
|
+ <Upload className={`${sizeConfig.icon} ${dragActive ? 'text-primary' : 'text-gray-400'}`} />
|
|
|
<div className="text-center">
|
|
|
- <p className="text-lg font-medium">{buttonText}</p>
|
|
|
- <p className="text-sm text-gray-500 mt-1">{tipText}</p>
|
|
|
+ <p className={`${sizeConfig.title} font-medium`}>{buttonText}</p>
|
|
|
+ {size !== 'minimal' && (
|
|
|
+ <p className={`${sizeConfig.description} text-gray-500 mt-1`}>{tipText}</p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
<Button
|
|
|
type="button"
|
|
|
variant="outline"
|
|
|
+ size={size === 'minimal' ? 'sm' : size === 'compact' ? 'sm' : 'default'}
|
|
|
onClick={() => {
|
|
|
const input = document.createElement('input');
|
|
|
input.type = 'file';
|
|
|
@@ -303,35 +353,38 @@ const MinioUploader: React.FC<MinioUploaderProps> = ({
|
|
|
{/* 上传进度列表 */}
|
|
|
{fileList.length > 0 && (
|
|
|
<Card>
|
|
|
- <CardContent className="pt-6">
|
|
|
- <h3 className="text-lg font-semibold mb-4">上传进度</h3>
|
|
|
- <div className="space-y-4">
|
|
|
+ <CardContent className={sizeConfig.cardPadding}>
|
|
|
+ <h3 className={`${sizeConfig.title} font-semibold mb-3`}>上传进度</h3>
|
|
|
+ <div className={sizeConfig.fileList}>
|
|
|
{fileList.map(item => (
|
|
|
- <div key={item.uid} className="flex items-center space-x-4 p-4 border rounded-lg">
|
|
|
+ <div key={item.uid} className={`flex items-center space-x-3 p-3 border rounded-lg ${size === 'minimal' ? 'text-sm' : ''}`}>
|
|
|
<div className="flex-shrink-0">
|
|
|
- {renderFileIcon(item.type)}
|
|
|
+ {renderFileIcon(item.type, size === 'minimal' ? 'small' : 'normal')}
|
|
|
</div>
|
|
|
<div className="flex-1 min-w-0">
|
|
|
- <div className="flex justify-between items-center mb-2">
|
|
|
- <p className="text-sm font-medium truncate">{item.name}</p>
|
|
|
- <div className="flex items-center space-x-2">
|
|
|
+ <div className="flex justify-between items-center mb-1">
|
|
|
+ <p className={`${size === 'minimal' ? 'text-xs' : 'text-sm'} font-medium truncate`}>{item.name}</p>
|
|
|
+ <div className="flex items-center space-x-1">
|
|
|
{renderUploadStatus(item)}
|
|
|
<Button
|
|
|
variant="ghost"
|
|
|
- size="sm"
|
|
|
+ size={size === 'minimal' ? 'icon' : 'sm'}
|
|
|
onClick={() => handleRemove(item.uid)}
|
|
|
disabled={item.status === 'uploading'}
|
|
|
+ className={size === 'minimal' ? 'h-6 w-6' : ''}
|
|
|
>
|
|
|
- <X className="h-4 w-4" />
|
|
|
+ <X className={size === 'minimal' ? 'h-3 w-3' : 'h-4 w-4'} />
|
|
|
</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
{item.status === 'uploading' && (
|
|
|
- <div className="space-y-2">
|
|
|
- <Progress value={item.percent} className="h-2" />
|
|
|
- <p className="text-xs text-gray-500">
|
|
|
- {Math.round(item.percent)}% - {formatFileSize(item.size * (item.percent / 100))} / {formatFileSize(item.size)}
|
|
|
- </p>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <Progress value={item.percent} className={sizeConfig.progressHeight} />
|
|
|
+ {size !== 'minimal' && (
|
|
|
+ <p className={`${sizeConfig.description} text-gray-500`}>
|
|
|
+ {Math.round(item.percent)}% - {formatFileSize(item.size * (item.percent / 100))} / {formatFileSize(item.size)}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|