| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import React, { useState, useCallback } from 'react';
- import { Upload, Progress, message, Tag, Space, Typography, Button } from 'antd';
- import { UploadOutlined, CloseOutlined, CheckCircleOutlined, SyncOutlined } from '@ant-design/icons';
- import { App } from 'antd';
- import type { UploadFile, UploadProps } from 'antd';
- import type { RcFile } from 'rc-upload/lib/interface';
- import type { UploadFileStatus } from 'antd/es/upload/interface';
- import type { UploadRequestOption } from 'rc-upload/lib/interface';
- 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;
- }
- const MinioUploader: React.FC<MinioUploaderProps> = ({
- uploadPath = '/',
- accept,
- maxSize = 500, // 默认最大500MB
- multiple = false,
- onUploadSuccess,
- onUploadError,
- buttonText = '点击或拖拽上传文件',
- tipText = '支持单文件或多文件上传,单个文件大小不超过500MB'
- }) => {
- const { message: antdMessage } = App.useApp();
- const [fileList, setFileList] = useState<UploadFile[]>([]);
- const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
- // 处理上传进度
- const handleProgress = useCallback((uid: string, event: MinioProgressEvent) => {
- setFileList(prev =>
- prev.map(item => {
- if (item.uid === uid) {
- return {
- ...item,
- status: event.stage === 'error' ? ('error' as UploadFileStatus) : ('uploading' as UploadFileStatus),
- 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' as UploadFileStatus,
- percent: 100,
- response: { fileKey: result.fileKey },
- url: result.fileUrl,
- };
- }
- return item;
- })
- );
-
- setUploadingFiles(prev => {
- const newSet = new Set(prev);
- newSet.delete(uid);
- return newSet;
- });
-
- antdMessage.success(`文件 "${file.name}" 上传成功`);
- onUploadSuccess?.(result.fileKey, result.fileUrl, file);
- }, [antdMessage, onUploadSuccess]);
- // 处理上传失败
- const handleError = useCallback((uid: string, error: Error, file: File) => {
- setFileList(prev =>
- prev.map(item => {
- if (item.uid === uid) {
- return {
- ...item,
- status: 'error' as UploadFileStatus,
- percent: 0,
- error: error.message || '上传失败'
- };
- }
- return item;
- })
- );
-
- setUploadingFiles(prev => {
- const newSet = new Set(prev);
- newSet.delete(uid);
- return newSet;
- });
-
- antdMessage.error(`文件 "${file.name}" 上传失败: ${error.message}`);
- onUploadError?.(error, file);
- }, [antdMessage, onUploadError]);
- // 自定义上传逻辑
- const customRequest = async (options: UploadRequestOption) => {
- const { file, onSuccess, onError } = options;
- const rcFile = file as RcFile;
- const uid = rcFile.uid;
-
- // 添加到文件列表
- setFileList(prev => [
- ...prev.filter(item => item.uid !== uid),
- {
- uid,
- name: rcFile.name,
- size: rcFile.size,
- type: rcFile.type,
- lastModified: rcFile.lastModified,
- lastModifiedDate: new Date(rcFile.lastModified),
- status: 'uploading' as UploadFileStatus,
- percent: 0,
- }
- ]);
-
- // 添加到上传中集合
- setUploadingFiles(prev => new Set(prev).add(uid));
-
- try {
- // 调用minio上传方法
- const result = await uploadMinIOWithPolicy(
- uploadPath,
- options.file as unknown as File,
- rcFile.name,
- {
- onProgress: (event) => handleProgress(uid, event),
- signal: new AbortController().signal
- }
- );
-
- handleComplete(uid, result, rcFile as unknown as File);
- onSuccess?.({}, rcFile);
- } catch (error) {
- handleError(uid, error instanceof Error ? error : new Error('未知错误'), rcFile as unknown as File);
- onError?.(error instanceof Error ? error : new Error('未知错误'));
- }
- };
- // 处理文件移除
- const handleRemove = (uid: string) => {
- setFileList(prev => prev.filter(item => item.uid !== uid));
- };
- // 验证文件大小
- const beforeUpload = (file: File) => {
- const fileSizeMB = file.size / (1024 * 1024);
- if (fileSizeMB > maxSize!) {
- message.error(`文件 "${file.name}" 大小超过 ${maxSize}MB 限制`);
- return false;
- }
- return true;
- };
- // 渲染上传状态
- const renderUploadStatus = (item: UploadFile) => {
- switch (item.status) {
- case 'uploading':
- return (
- <Space>
- <SyncOutlined spin size={12} />
- <span>{item.percent}%</span>
- </Space>
- );
- case 'done':
- return (
- <Space>
- <CheckCircleOutlined style={{ color: '#52c41a' }} size={12} />
- <Tag color="success">上传成功</Tag>
- </Space>
- );
- case 'error':
- return (
- <Space>
- <CloseOutlined style={{ color: '#ff4d4f' }} size={12} />
- <Tag color="error">{item.error || '上传失败'}</Tag>
- </Space>
- );
- default:
- return null;
- }
- };
- return (
- <div className="minio-uploader">
- <Upload.Dragger
- name="files"
- accept={accept}
- multiple={multiple}
- customRequest={customRequest}
- beforeUpload={beforeUpload}
- showUploadList={false}
- disabled={uploadingFiles.size > 0 && !multiple}
- >
- <div className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-gray-300 rounded-md transition-all hover:border-primary">
- <UploadOutlined style={{ fontSize: 24, color: '#1890ff' }} />
- <Typography.Text className="mt-2">{buttonText}</Typography.Text>
- <Typography.Text type="secondary" className="mt-1">
- {tipText}
- </Typography.Text>
- </div>
- </Upload.Dragger>
- {/* 上传进度列表 */}
- {fileList.length > 0 && (
- <div className="mt-4 space-y-3">
- {fileList.map(item => (
- <div key={item.uid} className="flex items-center p-3 border rounded-md">
- <div className="flex-1 min-w-0">
- <div className="flex justify-between items-center mb-1">
- <Typography.Text ellipsis className="max-w-xs">
- {item.name}
- </Typography.Text>
- <div className="flex items-center space-x-2">
- {renderUploadStatus(item)}
- <Button
- type="text"
- size="small"
- icon={<CloseOutlined />}
- onClick={() => handleRemove(item.uid)}
- disabled={item.status === 'uploading'}
- />
- </div>
- </div>
- {item.status === 'uploading' && (
- <Progress
- percent={item.percent}
- size="small"
- status={item.percent === 100 ? 'success' : undefined}
- />
- )}
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- );
- };
- export default MinioUploader;
|