/* * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-02-11 11:32:48 */ /** * File Upload Component * * A reusable file upload component based on Ant Design Upload. * Supports single/multiple file uploads, drag-and-drop, file validation, and preview. * * Features: * - File type validation (images, documents, etc.) * - File size validation * - Auto-upload or manual upload modes * - Progress tracking * - Custom upload actions and headers * - File list management * * @component */ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { Upload, Progress, App } from 'antd'; import type { UploadProps, UploadFile } from 'antd'; import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface'; import { useTranslation } from 'react-i18next'; import { request } from '@/utils/request' import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' interface UploadFilesProps extends Omit { /** Upload API endpoint */ action?: string; /** Enable multiple file selection */ multiple?: boolean; /** List of uploaded files */ fileList?: UploadFile[]; /** Callback when file list changes */ onChange?: (fileList: UploadFile | UploadFile[]) => void; customRequest?: RcUploadProps['customRequest']; /** Custom upload request configuration */ requestConfig?: { data?: Record; headers?: Record; }; /** Disable upload */ disabled?: boolean; /** File size limit in MB */ fileSize?: number; /** Allowed file types ['doc', 'xls', 'ppt', 'pdf'] */ fileType?: string[]; /** Auto-upload on file selection, default is true */ isAutoUpload?: boolean; /** Maximum number of files allowed */ maxCount?: number; /** Custom file removal callback */ onRemove?: (file: UploadFile) => boolean | void | Promise; } // Mapping of file extensions to MIME types 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', csv: 'text/csv', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // md: 'text/markdown', // htm: 'text/html', // html: 'text/html', // json: 'application/json', jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', bmp: 'image/bmp', webp: 'image/webp', svg: 'image/svg+xml', } export interface UploadFilesRef { /** Current file list */ fileList: UploadFile[]; /** Clear all uploaded files */ clearFiles: () => void; } /** * Common upload component based on Ant Design Upload * Supports single/multiple file uploads, drag-and-drop, file validation, and preview */ const UploadFiles = forwardRef(({ action = fileUploadUrlWithoutApiPrefix, multiple = false, fileList: propFileList = [], onChange, disabled = false, fileSize = 5, fileType = Object.entries(ALL_FILE_TYPE).map(([key]) => key), isAutoUpload = true, maxCount = 1, onRemove: customOnRemove, requestConfig, ...props }, ref) => { const { t } = useTranslation(); const { message } = App.useApp() const [fileList, setFileList] = useState(propFileList); const [accept, setAccept] = useState(); /** * Validates file type and size before upload * @returns Upload.LIST_IGNORE to prevent upload, or true to proceed */ const beforeUpload: RcUploadProps['beforeUpload'] = (file) => { // Validate file size if (fileSize) { const isLtMaxSize = (file.size / 1024 / 1024) < fileSize; if (!isLtMaxSize) { message.error(t('common.fileSizeTip', { size: fileSize })); return Upload.LIST_IGNORE; } } // Validate file type if (fileType && fileType.length > 0) { // Get file extension const fileName = file.name.toLowerCase(); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); // Check if extension is in allowed types list const isValidExtension = fileType.some(type => type.toLowerCase() === fileExtension); // Also check MIME type if available (as fallback validation) 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; // Prevent auto-upload } return isAutoUpload; }; /** * Custom upload request handler */ const handleCustomRequest: RcUploadProps['customRequest'] = async (options) => { const { file, onSuccess, onError } = options; try { const formData = new FormData(); formData.append('file', file); const response = await request.uploadFile(action, formData, requestConfig); onSuccess?.({data: response}); } catch (error) { onError?.(error as Error); } }; /** * Handles upload state changes */ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { setFileList(newFileList); if (onChange) { onChange(maxCount === 1 ? newFileList[newFileList.length - 1] : newFileList); } }; /** * Clears all uploaded files */ const clearFiles = () => { setFileList([]); if (onChange) { onChange([]); } } // Build accept string from file types (includes both MIME types and extensions) useEffect(() => { if (fileType && fileType.length > 0) { // Include both MIME types and file extensions const acceptArray: string[] = []; fileType.forEach((type: string) => { const lowerType = type.toLowerCase(); // Add MIME type (if exists) const mimeType = ALL_FILE_TYPE[lowerType]; if (mimeType) { acceptArray.push(mimeType); } // Add file extension (.md, .html, etc.) acceptArray.push(`.${lowerType}`); }); setAccept(acceptArray.join(',')); } else { setAccept(undefined); } }, [fileType]) // Generate upload component configuration const uploadProps: UploadProps = { customRequest: handleCustomRequest, multiple: multiple && maxCount > 1, fileList, beforeUpload, onChange: handleChange, accept, disabled, showUploadList: false, itemRender: (_, file, __, actions) => { return (
{file.name} actions?.remove()}>Cancel
); }, className: 'rb:-mb-1.5!', ...props, }; // Expose methods to parent component via ref useImperativeHandle(ref, () => ({ fileList, clearFiles })); return ( {t('memoryConversation.uploadFile')} ); }); export default UploadFiles;