ai-course/app/backend/src/pages/textbook/compenents/Upload/DraggerUpload.tsx

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>
);
};