|
|
@@ -1,262 +0,0 @@
|
|
|
-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 { UploadFileStatus } from 'antd/es/upload/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: {
|
|
|
- file: UploadFile,
|
|
|
- onSuccess: () => void,
|
|
|
- onError: (error: Error) => void
|
|
|
- }) => {
|
|
|
- const { file, onSuccess, onError } = options;
|
|
|
- const uid = file.uid;
|
|
|
-
|
|
|
- // 添加到文件列表
|
|
|
- setFileList(prev => [
|
|
|
- ...prev.filter(item => item.uid !== uid),
|
|
|
- {
|
|
|
- uid,
|
|
|
- name: file.name,
|
|
|
- size: file.size,
|
|
|
- type: file.type,
|
|
|
- lastModified: file.lastModified,
|
|
|
- lastModifiedDate: file.lastModifiedDate,
|
|
|
- originFileObj: file.originFileObj,
|
|
|
- status: 'uploading' as UploadFileStatus,
|
|
|
- percent: 0,
|
|
|
- }
|
|
|
- ]);
|
|
|
-
|
|
|
- // 添加到上传中集合
|
|
|
- setUploadingFiles(prev => new Set(prev).add(uid));
|
|
|
-
|
|
|
- try {
|
|
|
- // 调用minio上传方法
|
|
|
- const result = await uploadMinIOWithPolicy(
|
|
|
- uploadPath,
|
|
|
- file.originFileObj as File,
|
|
|
- file.name,
|
|
|
- {
|
|
|
- onProgress: (event) => handleProgress(uid, event),
|
|
|
- signal: new AbortController().signal
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- handleComplete(uid, result, file.originFileObj as File);
|
|
|
- onSuccess();
|
|
|
- } catch (error) {
|
|
|
- handleError(uid, error instanceof Error ? error : new Error('未知错误'), file.originFileObj 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;
|