Files
MemoryBear/web/src/components/Upload/UploadFiles.tsx
2026-02-06 21:11:51 +08:00

293 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<UploadProps, 'onChange'> {
/** 上传接口地址 */
action?: string;
/** 是否支持多选 */
multiple?: boolean;
/** 已上传的文件列表 */
fileList?: UploadFile[];
/** 文件列表变化回调 */
onChange?: (fileList: UploadFile[]) => void;
customRequest?: RcUploadProps['customRequest'];
/** 自定义上传请求配置 */
requestConfig?: {
data?: Record<string, string | number | boolean>;
headers?: Record<string, string>;
};
/** 禁用上传 */
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<boolean | void>;
}
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<UploadFilesRef, UploadFilesProps>(({
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<UploadFile[]>(propFileList);
const [accept, setAccept] = useState<string | undefined>();
// 处理文件移除
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 (
<div key={file.uid} className="rb:relative rb:w-full rb:pt-2 rb:pl-2.5 rb:pr-2.5 rb-pb-[10px] rb:border rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-0.5">
{file.name}
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>{t('common.cancel')}</span>
</div>
{isAutoUpload && <Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />}
</div>
);
},
...props,
};
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
fileList,
clearFiles
}));
const hasProgress = isAutoUpload && fileList.some((item) => item.percent !== 100);
if (isCanDrag) {
return (
<div className="rb:mb-6 rb:w-full">
<Dragger {...uploadProps} style={{ height: '270px' }}>
<div className="rb:flex rb:justify-center rb:flex-col rb:items-center">
<img className="rb:w-12 rb:h-12" src={CloudUploadOutlined} />
{(!isAutoUpload || !hasProgress && (!fileList || !fileList.length)) &&
<>
<div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:leading-5">
{t('common.dragUploadTip')}<span className="rb:ml-1 rb:text-[#155EEF]">{t('common.uploadClickTip')}</span>
</div>
{fileType && <div className="rb:text-[12px] rb:text-[#A8A9AA] rb:leading-3.5 rb:mt-2 rb:cursor-pointer">{t('common.supportedFileTypes', { types: fileType.join(',') })}</div>}
{(fileSize || fileType || maxCount > 1) && (
<div className='rb:text-xs rb:mt-2 rb:text-[#A8A9AA]'>
{t('common.uploadFileTipMax', { max: fileSize, maxCount: maxCount })}
</div>
)}
</>
}
{hasProgress && <div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:mb-6 rb:leading-5">{t('common.uploading')}</div>}
</div>
</Dragger>
</div>
);
}
return (
<Upload
{...uploadProps}
>
<div>
<Button
type="default"
icon={<UploadOutlined />}
className="rb:w-full"
disabled={fileList.length >= maxCount}
>
</Button>
{(fileSize || fileType || maxCount > 1) && (
<div>
{fileSize && <> <b style={{color: '#f56c6c'}}>{ fileSize }MB</b></>}
{fileType && <> <b style={{color: '#f56c6c'}}>{ fileType.join('、') }</b></>}
{multiple && maxCount > 1 && <> <b style={{color: '#f56c6c'}}>{ maxCount } </b> </>}
</div>
)}
</div>
</Upload>
);
});
export default UploadFiles;