406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
import { useEffect, useState, useRef } from 'react';
|
|
import { Upload, message, Progress, List, Button, Space } from 'antd';
|
|
import { InboxOutlined, CloseOutlined, FileOutlined } from '@ant-design/icons';
|
|
import type { UploadProps } from 'antd';
|
|
import { getTenant, getToken } from '../../../../utils';
|
|
import { useTranslation } from 'react-i18next';
|
|
import config from '../../../../js/config';
|
|
|
|
const { Dragger } = Upload;
|
|
|
|
interface IProps {
|
|
typeId?: number; // 1:视频, 2:图片, 3:文档, 4:音频, 5:其他
|
|
currentCount?: number;
|
|
maxCount?: number;
|
|
onUploadSuccess?: (fileInfo: {
|
|
type: string;
|
|
fileExtension: string;
|
|
fileSize: number;
|
|
filePath: string;
|
|
fileName: string;
|
|
}) => void;
|
|
onUploadError?: (error: any) => void;
|
|
}
|
|
|
|
interface UploadFileItem {
|
|
id: string;
|
|
name: string;
|
|
size: number;
|
|
progress: number;
|
|
status: 'uploading' | 'success' | 'error' | 'cancelled';
|
|
file: File;
|
|
xhr?: XMLHttpRequest;
|
|
}
|
|
|
|
// 根据类型ID获取对应的文件类型配置
|
|
// const getFileTypeConfig = (typeId: number) => {
|
|
// const configs = {
|
|
// 1: {
|
|
// // 视频
|
|
// accept: '.mp4,.mov,.avi,.wmv,.flv,.rmvb',
|
|
// },
|
|
// 2: {
|
|
// // 图片
|
|
// accept: '.png,.jpeg,.jpg,.gif',
|
|
// },
|
|
// 3: {
|
|
// // 文档
|
|
// accept: '.pdf,.doc,.docx,.ppt,.pptx',
|
|
// },
|
|
// 4: {
|
|
// // 音频
|
|
// accept: '.mp3,.aac,.wma,.wav',
|
|
// },
|
|
// 5: {
|
|
// // 其他
|
|
// accept: '*',
|
|
// },
|
|
// };
|
|
//
|
|
// return configs[typeId as keyof typeof configs] || configs[5];
|
|
// };
|
|
|
|
// 根据类型ID获取模块类型
|
|
// const getModuleType = (typeId: number): string => {
|
|
// const types = { 1: 'VIDEO', 2: 'IMAGE', 3: 'DOCUMENT', 4: 'AUDIO', 5: 'OTHER' };
|
|
// return types[typeId as keyof typeof types] || 'OTHER';
|
|
// };
|
|
|
|
export const DraggerUpload = (props: IProps) => {
|
|
const { t } = useTranslation();
|
|
const [uploadFiles, setUploadFiles] = useState<UploadFileItem[]>([]);
|
|
const xhrRef = useRef<Map<string, XMLHttpRequest>>(new Map());
|
|
|
|
const { typeId, currentCount = 0, maxCount = 1, onUploadSuccess, onUploadError } = props;
|
|
// const fileTypeConfig = getFileTypeConfig(typeId);
|
|
const isLimitReached = maxCount > 0 && currentCount >= maxCount;
|
|
|
|
// 获取文件扩展名
|
|
const getFileExtension = (fileName: string): string => {
|
|
return fileName.split('.').pop()?.toLowerCase() || '';
|
|
};
|
|
|
|
useEffect(() => {
|
|
setUploadFiles([]);
|
|
// 清理所有正在进行的上传
|
|
xhrRef.current.forEach((xhr) => {
|
|
xhr.abort();
|
|
});
|
|
xhrRef.current.clear();
|
|
}, [typeId]);
|
|
|
|
// 检查文件类型(仅对其他类型做限制)
|
|
const checkFileType = (file: File): boolean => {
|
|
if (typeId && typeId !== 5) return true;
|
|
|
|
const extension = getFileExtension(file.name);
|
|
const restrictedExtensions = [
|
|
'mp4',
|
|
'mov',
|
|
'avi',
|
|
'wmv',
|
|
'flv',
|
|
'rmvb',
|
|
'mp3',
|
|
'aac',
|
|
'wma',
|
|
'wav',
|
|
'png',
|
|
'jpeg',
|
|
'jpg',
|
|
'gif',
|
|
'docx',
|
|
'pptx',
|
|
'pdf',
|
|
'doc',
|
|
];
|
|
|
|
if (extension && restrictedExtensions.includes(extension)) {
|
|
message.error(t('textbook.resource.restrictedFileType'));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// 更新文件上传进度
|
|
const updateFileProgress = (
|
|
fileId: string,
|
|
progress: number,
|
|
status: UploadFileItem['status'] = 'uploading'
|
|
) => {
|
|
setUploadFiles((prev) =>
|
|
prev.map((item) => (item.id === fileId ? { ...item, progress, status } : item))
|
|
);
|
|
};
|
|
|
|
// 移除上传文件
|
|
const removeUploadFile = (fileId: string) => {
|
|
const fileItem = uploadFiles.find((item) => item.id === fileId);
|
|
|
|
// 如果文件正在上传,先取消上传
|
|
if (fileItem?.status === 'uploading') {
|
|
cancelUpload(fileId);
|
|
}
|
|
|
|
setUploadFiles((prev) => prev.filter((item) => item.id !== fileId));
|
|
xhrRef.current.delete(fileId);
|
|
};
|
|
|
|
// 取消上传
|
|
const cancelUpload = (fileId: string) => {
|
|
const xhr = xhrRef.current.get(fileId);
|
|
if (xhr) {
|
|
xhr.abort();
|
|
xhrRef.current.delete(fileId);
|
|
updateFileProgress(fileId, 0, 'cancelled');
|
|
message.info(t('commen.uploadCancelled'));
|
|
}
|
|
};
|
|
|
|
// 处理文件上传
|
|
const handleCustomRequest: UploadProps['customRequest'] = async (options) => {
|
|
const { file, onSuccess, onError, onProgress } = options;
|
|
|
|
const fileId = Math.random().toString(36).substr(2, 9);
|
|
const uploadFile: UploadFileItem = {
|
|
id: fileId,
|
|
name: (file as File).name,
|
|
size: (file as File).size,
|
|
progress: 0,
|
|
status: 'uploading',
|
|
file: file as File,
|
|
};
|
|
|
|
setUploadFiles((prev) => [...prev, uploadFile]);
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file as File);
|
|
// formData.append('module', getModuleType(typeId));
|
|
formData.append('duration', '0');
|
|
|
|
let appUrl = config.app_url.replace(/\/+$/, '');
|
|
if (!appUrl.startsWith('http')) {
|
|
appUrl = `${window.location.protocol}//${window.location.host}${appUrl.startsWith('/') ? appUrl : '/' + appUrl}`;
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhrRef.current.set(fileId, xhr);
|
|
|
|
// 监听上传进度
|
|
xhr.upload.addEventListener('progress', (event) => {
|
|
if (event.lengthComputable) {
|
|
const percent = (event.loaded / event.total) * 100;
|
|
updateFileProgress(fileId, percent);
|
|
onProgress?.({ percent });
|
|
}
|
|
});
|
|
|
|
// 监听完成
|
|
xhr.addEventListener('load', () => {
|
|
xhrRef.current.delete(fileId);
|
|
|
|
if (xhr.status === 200) {
|
|
let response;
|
|
try {
|
|
response = JSON.parse(xhr.responseText);
|
|
} catch {
|
|
response = xhr.responseText;
|
|
}
|
|
|
|
updateFileProgress(fileId, 100, 'success');
|
|
onSuccess?.(response);
|
|
message.success(t('commen.uploadSuccess'));
|
|
|
|
// 构造返回的文件信息
|
|
const fileInfo = {
|
|
type: (file as File).type,
|
|
fileExtension: getFileExtension((file as File).name),
|
|
fileSize: (file as File).size,
|
|
filePath: response?.data?.path || response?.path || '', // 根据实际接口响应调整
|
|
fileName: (file as File).name,
|
|
// 如果需要,还可以返回原始响应
|
|
rawResponse: response,
|
|
};
|
|
|
|
onUploadSuccess?.(fileInfo);
|
|
} else {
|
|
throw new Error(`Upload failed with status ${xhr.status}`);
|
|
}
|
|
});
|
|
|
|
// 监听错误
|
|
xhr.addEventListener('error', () => {
|
|
xhrRef.current.delete(fileId);
|
|
|
|
// 如果是取消操作,不显示错误消息
|
|
if (xhr.status === 0 && xhr.readyState === 0) {
|
|
return; // 取消操作,不处理
|
|
}
|
|
|
|
updateFileProgress(fileId, 0, 'error');
|
|
const error = new Error('Upload failed');
|
|
onError?.(error);
|
|
onUploadError?.(error);
|
|
message.error(t('commen.uploadFailed'));
|
|
});
|
|
|
|
// 监听取消
|
|
xhr.addEventListener('abort', () => {
|
|
xhrRef.current.delete(fileId);
|
|
updateFileProgress(fileId, 0, 'cancelled');
|
|
// message.info(t('commen.uploadCancelled'));
|
|
});
|
|
|
|
xhr.open('POST', `${appUrl}/backend/v1/localUpload/upload`);
|
|
xhr.setRequestHeader('Authorization', 'Bearer ' + getToken());
|
|
xhr.setRequestHeader('tenant-id', getTenant());
|
|
xhr.send(formData);
|
|
} catch (error) {
|
|
xhrRef.current.delete(fileId);
|
|
updateFileProgress(fileId, 0, 'error');
|
|
onError?.(error as Error);
|
|
onUploadError?.(error);
|
|
message.error(t('commen.uploadFailed'));
|
|
}
|
|
};
|
|
|
|
const uploadProps: UploadProps = {
|
|
name: 'file',
|
|
multiple: false,
|
|
// accept: fileTypeConfig.accept,
|
|
customRequest: handleCustomRequest,
|
|
showUploadList: false,
|
|
disabled: isLimitReached || uploadFiles.some((file) => file.status === 'uploading'),
|
|
beforeUpload: (file) => {
|
|
if (isLimitReached) {
|
|
message.error(t('textbook.resource.uploadLimitReached'));
|
|
return false;
|
|
}
|
|
|
|
// 如果已经有文件在上传,不允许上传新文件
|
|
if (uploadFiles.some((f) => f.status === 'uploading')) {
|
|
message.error(t('commen.uploadInProgress'));
|
|
return false;
|
|
}
|
|
|
|
return checkFileType(file);
|
|
},
|
|
};
|
|
|
|
// 格式化文件大小
|
|
const formatFileSize = (bytes: number) => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
// 获取状态对应的文本和颜色
|
|
const getStatusInfo = (status: UploadFileItem['status']) => {
|
|
switch (status) {
|
|
case 'uploading':
|
|
return { text: t('commen.uploading'), color: '#1890ff' };
|
|
case 'success':
|
|
return { text: t('commen.uploadSuccess'), color: '#52c41a' };
|
|
case 'error':
|
|
return { text: t('commen.uploadFailed'), color: '#ff4d4f' };
|
|
case 'cancelled':
|
|
return { text: t('commen.uploadCancelled'), color: '#faad14' };
|
|
default:
|
|
return { text: '', color: '#000' };
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Dragger {...uploadProps} disabled={uploadFiles.length > 0}>
|
|
<p className="ant-upload-drag-icon">
|
|
<InboxOutlined style={{ color: isLimitReached ? '#d9d9d9' : undefined }} />
|
|
</p>
|
|
<p className="ant-upload-text">
|
|
{isLimitReached
|
|
? t('textbook.resource.uploadLimitReached')
|
|
: uploadFiles.some((f) => f.status === 'uploading')
|
|
? t('commen.uploadInProgress')
|
|
: t('textbook.resource.uploadTips')}
|
|
</p>
|
|
<p className="ant-upload-hint" style={{ color: isLimitReached ? '#999' : undefined }}>
|
|
{isLimitReached
|
|
? t('textbook.resource.uploadLimitReachedDesc')
|
|
: uploadFiles.some((f) => f.status === 'uploading')
|
|
? t('commen.waitForUploadComplete')
|
|
: t('textbook.resource.uploadTips2')}
|
|
</p>
|
|
</Dragger>
|
|
|
|
{/* 上传文件列表 */}
|
|
{uploadFiles.length > 0 && (
|
|
<div style={{ marginTop: 16 }}>
|
|
<List
|
|
size="small"
|
|
dataSource={uploadFiles}
|
|
renderItem={(item) => {
|
|
const statusInfo = getStatusInfo(item.status);
|
|
return (
|
|
<List.Item>
|
|
<div style={{ width: '100%' }}>
|
|
<Space
|
|
style={{ width: '100%', justifyContent: 'space-between', marginBottom: 8 }}
|
|
>
|
|
<Space>
|
|
<div className={'d-flex'}>
|
|
<FileOutlined />
|
|
<div className={'w-174px mr-5 '}>{item.name}</div>
|
|
<div className={'mr-5 '} style={{ color: '#999', fontSize: '12px' }}>
|
|
({formatFileSize(item.size)})
|
|
</div>
|
|
<div
|
|
className={'mr-5 '}
|
|
style={{ color: statusInfo.color, fontSize: '12px' }}
|
|
>
|
|
{statusInfo.text}
|
|
</div>
|
|
</div>
|
|
</Space>
|
|
<Space>
|
|
{item.status === 'uploading' && (
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
danger
|
|
onClick={() => cancelUpload(item.id)}
|
|
>
|
|
{t('commen.cancel')}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
type="text"
|
|
icon={<CloseOutlined />}
|
|
size="small"
|
|
onClick={() => removeUploadFile(item.id)}
|
|
disabled={item.status === 'uploading'}
|
|
/>
|
|
</Space>
|
|
</Space>
|
|
{item.status === 'uploading' && (
|
|
<Progress
|
|
percent={item.progress}
|
|
status="active"
|
|
size="small"
|
|
showInfo={true}
|
|
/>
|
|
)}
|
|
</div>
|
|
</List.Item>
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|