import { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react'; import { Upload, Button, Modal, Progress, App } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; import type { UploadProps, UploadFile } from 'antd'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; // import { request } from '@/utils/request'; import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface'; import CloudUploadOutlined from '@/assets/images/CloudUploadOutlined.png' import { useTranslation } from 'react-i18next'; import { cookieUtils } from '@/utils/request' import { fileUpload } from '@/api/fileStorage' const { confirm } = Modal; const { Dragger } = Upload; interface UploadFilesProps extends Omit { /** 上传接口地址 */ action?: string; /** 是否支持多选 */ multiple?: boolean; /** 已上传的文件列表 */ fileList?: UploadFile[]; /** 文件列表变化回调 */ onChange?: (fileList: UploadFile[]) => void; customRequest?: RcUploadProps['customRequest']; /** 自定义上传请求配置 */ requestConfig?: { data?: Record; headers?: Record; }; /** 禁用上传 */ disabled?: boolean; /** 文件大小限制(MB) */ fileSize?: number; /** 文件类型限制 ['doc', 'xls', 'ppt', 'pdf'] */ fileType?: string[]; /** 是否自动上传,默认为true */ isAutoUpload?: boolean; /** 最大上传文件数 */ maxCount?: number; /** 是否支持拖拽上传,默认为false */ isCanDrag?: boolean; /** 自定义移除文件回调 */ onRemove?: (file: UploadFile) => boolean | void | Promise; } const ALL_FILE_TYPE: { [key: string]: string; } = { txt: 'text/plain', pdf: 'application/pdf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', csv: 'text/csv', md: 'text/markdown', htm: 'text/html', html: 'text/html', json: 'application/json', owl: 'application/rdf+xml', ttl: 'text/turtle', rdf: 'application/rdf+xml', xml: 'application/rdf+xml', yaml: 'application/x-yaml', yml: 'application/x-yaml', } export interface UploadFilesRef { fileList: UploadFile[]; clearFiles: () => void; } /** * 公共上传组件,基于Ant Design Upload组件封装 * 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能 */ const UploadFiles = forwardRef(({ action = '/api/upload', multiple = false, fileList: propFileList = [], onChange, // requestConfig = {}, disabled = false, fileSize = 5, fileType = ['doc', 'xls', 'ppt', 'pdf'], isAutoUpload = true, maxCount = 1, isCanDrag = false, onRemove: customOnRemove, ...props }, ref) => { const { t } = useTranslation(); const { message } = App.useApp() const [fileList, setFileList] = useState(propFileList); const [accept, setAccept] = useState(); // 处理文件移除 const handleRemove = (file: UploadFile) => { // 显示确认弹窗 confirm({ title: `${t('common.confirmRemoveFile')}`, okText: `${t('common.confirm')}`, okType: 'danger', cancelText: `${t('common.cancel')}`, onOk: async () => { // 如果有自定义的 onRemove 回调,在确认后执行 if (customOnRemove) { const result = customOnRemove(file); // 等待 Promise 结果 const finalResult = result instanceof Promise ? await result : result; // 如果返回 false,阻止移除 if (finalResult === false) { return; } } // 移除文件 const newFileList = fileList.filter((item) => item.uid !== file.uid); setFileList(newFileList); onChange?.(newFileList); }, }); return false; // 阻止默认删除行为,由confirm控制 }; // 校验文件类型和大小 const beforeUpload: RcUploadProps['beforeUpload'] = (file) => { // 校验文件大小 if (fileSize) { const isLtMaxSize = (file.size / 1024 / 1024) < fileSize; if (!isLtMaxSize) { message.error(t('common.fileSizeTip', { size: fileSize })); return Upload.LIST_IGNORE; } } // 校验文件类型 if (fileType && fileType.length > 0) { // 获取文件扩展名 const fileName = file.name.toLowerCase(); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); // 检查扩展名是否在允许的类型列表中 const isValidExtension = fileType.some(type => type.toLowerCase() === fileExtension); // 如果有 MIME 类型,也检查 MIME 类型(作为备选验证) const isValidMimeType = file.type && accept ? accept.includes(file.type) : true; if (!isValidExtension && !isValidMimeType) { message.error(`${t('common.fileAcceptTip')}${fileExtension || file.type}`); return Upload.LIST_IGNORE; } } if (!isAutoUpload) { const newFileList = [...fileList, file as UploadFile]; setFileList(newFileList); onChange?.(newFileList); return Upload.LIST_IGNORE; // 阻止自动上传 } return isAutoUpload; }; // 处理上传状态变化 const handleChange: UploadProps['onChange'] = ({ fileList: newFileList, event }) => { console.log('event', event) setFileList(newFileList); if (onChange) { onChange(newFileList); } }; // 清空已上传文件 const clearFiles = () => { setFileList([]); if (onChange) { onChange([]); } } useEffect(() => { if (fileType && fileType.length > 0) { // 同时包含 MIME 类型和文件扩展名 const acceptArray: string[] = []; fileType.forEach((type: string) => { const lowerType = type.toLowerCase(); // 添加 MIME 类型(如果存在) const mimeType = ALL_FILE_TYPE[lowerType]; if (mimeType) { acceptArray.push(mimeType); } // 添加文件扩展名(.md, .html 等) acceptArray.push(`.${lowerType}`); }); setAccept(acceptArray.join(',')); } else { setAccept(undefined); } }, [fileType]) // 生成上传组件配置 const uploadProps: UploadProps = { action, multiple: multiple && maxCount > 1, fileList, beforeUpload, headers: { authorization: `Bearer ${cookieUtils.get('authToken') || ''}`, }, onRemove: handleRemove, onChange: handleChange, accept, disabled: disabled || fileList.length >= maxCount, showUploadList: { showPreviewIcon: false, showRemoveIcon: true, showDownloadIcon: false, }, itemRender: (_, file, __, actions) => { return (
{file.name} actions?.remove()}>{t('common.cancel')}
{isAutoUpload && }
); }, ...props, }; // 暴露给父组件的方法 useImperativeHandle(ref, () => ({ fileList, clearFiles })); const hasProgress = isAutoUpload && fileList.some((item) => item.percent !== 100); if (isCanDrag) { return (
{(!isAutoUpload || !hasProgress && (!fileList || !fileList.length)) && <>
{t('common.dragUploadTip')}{t('common.uploadClickTip')}
{fileType &&
{t('common.supportedFileTypes', { types: fileType.join(',') })}
} {(fileSize || fileType || maxCount > 1) && (
{t('common.uploadFileTipMax', { max: fileSize, maxCount: maxCount })}
)} } {hasProgress &&
{t('common.uploading')}
}
); } return (
{(fileSize || fileType || maxCount > 1) && (
请上传 {fileSize && <>大小不超过 { fileSize }MB} {fileType && <>格式为 { fileType.join('、') }} 的文件 {multiple && maxCount > 1 && <>,最多上传 { maxCount } 个 文件}
)}
); }); export default UploadFiles;