input + React自定义上传组件【可自定义拓展】
import React, { useState, useRef, useCallback, forwardRef, useImperativeHandle, useEffect } from 'react';
import { UploadOutlined, FileOutlined, DeleteOutlined, LoadingOutlined, EyeOutlined, DownloadOutlined } from '@ant-design/icons';
import { Button, message, Spin } from 'antd';
import './index.css';
import { env, i18nValue } from '@/utils';
import { request } from 'ice';
import { CustomUploadRef, CustomUploadProps, CustomUploadFile } from './types/index';
import { RcFile } from 'antd/es/upload';
import { showErrorNotification } from '@/utils/util';const EmptyObject = {};
const EmptyArray = [];
const DEFAULT_FILE_URL = '/file/fileupload/upload';
const CustomizeUpload = forwardRef<CustomUploadRef, CustomUploadProps>((props, ref) => {const {multiple = false,defaultFileList = EmptyArray,accept,disabled = false,maxCount,maxSize = 100,isDragger = false,showUploadList = true,fileListRender,beforeUpload,customRequest,uploadBtnProps,onChange,onRemove,dragContent,} = props;const [fileList, setFileList] = useState<CustomUploadFile[]>([]);const [isDragOver, setIsDragOver] = useState(false);const fileInputRef = useRef<HTMLInputElement>(null);const [loading, setLoading] = useState(false);useEffect(() => {setFileList(defaultFileList)}, [defaultFileList]);const validateFileSize = useCallback((file: File): boolean => {const fileSizeMB = file.size / 1024 / 1024;if (fileSizeMB > maxSize) {message.error(`文件大小不能超过 ${maxSize}MB`);return false;}return true;}, [maxSize]);const validateFileCount = useCallback((newFiles: File[]): boolean => {if (!maxCount) return true;const currentCount = fileList.length;const newCount = newFiles.length;if (currentCount + newCount > maxCount) {message.error(`最多只能上传 ${maxCount} 个文件`);return false;}return true;}, [fileList.length, maxCount]);const handleUploadProgress = useCallback((fileItem: CustomUploadFile, percent: number) => {setFileList(prev => prev.map(item =>(item.fileId === fileItem.fileId? { ...item, percent, status: 'uploading' }: item),));}, []);const handleUploadSuccess = useCallback((fileItem: CustomUploadFile, response: any) => {const updatedFile: CustomUploadFile = {...fileItem,status: 'done',percent: 100,...response,};console.log(fileList, '====================');onChange?.({type:'success',file: updatedFile,fileList: fileList.map(item =>(item.fileId === fileItem.fileId ? updatedFile : item),),});}, [fileList, onChange]);const handleUploadError = useCallback((fileItem: CustomUploadFile, error: Error, updatedFileList: CustomUploadFile[]) => {const updatedFile: CustomUploadFile = {...fileItem,status: 'error',error,};showErrorNotification(error ?? '请求失败请联系管理员', '上传失败')onChange?.({type:'error',file: updatedFile,fileList: updatedFileList.filter(item => item?.fileId !== fileItem.fileId),});}, [fileList, onChange]);const handleRemoveFile = useCallback(async (fileItem: CustomUploadFile) => {if (onRemove) {const result = await onRemove(fileItem);if (result === false) return; }const updatedFileList = fileList.filter(item => item.fileId !== fileItem.fileId);setFileList(updatedFileList);onChange?.({type:'remove',file: { ...fileItem, status: 'removed' },fileList: updatedFileList,});}, [fileList, onRemove, onChange]);const simulateUpload = useCallback(async (fileItem: CustomUploadFile, updatedFileList: CustomUploadFile[]) => {let progress = 0;const formData = new FormData();formData.append('file', fileItem.originFileObj as Blob);setLoading(true);try {const res = await request({url: DEFAULT_FILE_URL,method: 'post',data: formData,baseURL: env.apiFile,onUploadProgress: (progressEvent) => {if (progressEvent.total) {progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);handleUploadProgress(fileItem, progress);}},headers: { menuCode: 'user', userOperation: 'UPLOAD_FILE', oprDocNumber: '888' },});if (res?.code === 'R000') {const { fileName, fileId, url } = res.data;handleUploadSuccess && handleUploadSuccess(res.data, {});message.success(i18nValue('上传成功'));} else if (res?.code) {handleUploadError(fileItem, res?.message, updatedFileList);}} catch (error) {setLoading(false);} finally {setLoading(false);}}, [handleUploadError, handleUploadProgress, handleUploadSuccess]);const triggerFileSelect = useCallback(() => {if (disabled || !fileInputRef.current) return;fileInputRef.current.click();}, [disabled]);const uploadFile = useCallback((fileItem: CustomUploadFile, updatedFileList: CustomUploadFile[]) => {const file = fileItem.originFileObj;if (!file) return;if (customRequest) {customRequest({file,onSuccess: (response, uploadedFile) => {handleUploadSuccess(uploadedFile, response);},onError: (error) => {handleUploadError(fileItem, error, updatedFileList);},onProgress: (percent, uploadingFile) => {handleUploadProgress(uploadingFile, percent);},});} else {simulateUpload(fileItem, updatedFileList);}}, [customRequest, handleUploadError, handleUploadProgress, handleUploadSuccess, simulateUpload]);const handleFileSelect = useCallback(async (files: FileList | File[]) => {if (disabled) return;const fileArray = Array.from(files);if (!validateFileCount(fileArray)) return;const validFiles: File[] = [];for (const file of fileArray) {if (!validateFileSize(file)) continue;let shouldUpload = true;if (beforeUpload) {try {shouldUpload = await beforeUpload(file, fileArray);} catch (error) {console.error('beforeUpload error:', error);shouldUpload = false;}}if (shouldUpload !== false) {validFiles.push(file);}}if (validFiles.length === 0) return;const newFileListItems: CustomUploadFile[] = validFiles.map((file: RcFile, index) => ({fileId: `${Date.now()}-${index}`,fileName: file.name,size: file.size,type: file.type,status: 'uploading',percent: 0,originFileObj: file,}));const updatedFileList = [...fileList, ...newFileListItems];setFileList(updatedFileList);newFileListItems.forEach(fileItem => {uploadFile(fileItem, updatedFileList);});}, [disabled, validateFileCount, fileList, validateFileSize, beforeUpload, onChange, uploadFile]);const handleFileInputChange = useCallback((eFile: React.ChangeEvent<HTMLInputElement>) => {const { files } = eFile.target;if (files && files.length > 0) {handleFileSelect(files);if (fileInputRef.current) {fileInputRef.current.value = '';}}}, [handleFileSelect]);const handleDragEnter = useCallback((eFile: React.DragEvent) => {eFile.preventDefault();if (!disabled) {setIsDragOver(true);}}, [disabled]);const handleDragLeave = useCallback((eFile: React.DragEvent) => {eFile.preventDefault();setIsDragOver(false);}, []);const handleDragOver = useCallback((eFile: React.DragEvent) => {eFile.preventDefault();}, []);const handleDrop = useCallback((eFile: React.DragEvent) => {eFile.preventDefault();setIsDragOver(false);if (disabled) return;const { files } = eFile.dataTransfer;if (files && files.length > 0) {handleFileSelect(files);}}, [disabled, handleFileSelect]);useImperativeHandle(ref, () => ({upload: (file: File) => {handleFileSelect([file]);},clear: () => {setFileList([]);},getFileList: () => [...fileList],}));const defaultDragContent = (<div className="custom-upload-drag-content"><Button icon={<UploadOutlinedstyle={{ fontSize: '18px', color: 'var(--primary-color)' }}/>}type='text'>{i18nValue('点击选择文件或将文件拖拽到此处')}</Button><p className="ant-upload-hint">{i18nValue('支持单个或批量上传')}</p></div>);const renderDefaultFileList = () => {if (!showUploadList || fileList.length === 0) return null;return (<div className="custom-upload-list">{fileList.map(file => (<div key={file.fileName} className="custom-upload-list-item"><FileOutlined style={{ marginRight: 8, color: 'var(--primary-color)' }} /><span className="custom-upload-file-name" title={file.fileName}>{file.fileName}</span>{}<EyeOutlinedsize={16}title={i18nValue('预览')}style={{ marginLeft: 8, cursor: 'pointer', color: 'var(--primary-color)' }}/><DownloadOutlinedsize={16}title={i18nValue('下载')}style={{ marginLeft: 8, cursor: 'pointer', color: 'var(--primary-color)' }}/><DeleteOutlinedclassName="custom-upload-delete"size={16}title={i18nValue('删除')}onClick={() => handleRemoveFile(file)}style={{ marginLeft: 8, cursor: 'pointer', color: '#ff4d4f' }}/></div>))}</div>);};return (<div className="custom-drag-upload">{}<inputref={fileInputRef}type="file"accept={accept}multiple={multiple}disabled={disabled}onChange={handleFileInputChange}style={{ display: 'none' }}/>{}{isDragger && <Spin spinning={loading} tip={i18nValue('附件上传中')} ><divclassName={`custom-upload-drag-area ${isDragOver ? 'drag-over' : ''} ${(disabled || loading) ? 'disabled' : ''}`}onClick={triggerFileSelect}onDragEnter={handleDragEnter}onDragOver={handleDragOver}onDragLeave={handleDragLeave}onDrop={handleDrop}>{dragContent || defaultDragContent}</div></Spin>}{}{!isDragger && <div className="custom-upload-button" style={{ marginTop: 16 }}><Buttontype="primary"icon={<UploadOutlined />}onClick={triggerFileSelect}loading={loading}disabled={disabled || loading}{...uploadBtnProps}>{uploadBtnProps?.uploadTitle || i18nValue('选择文件')}</Button></div>}{}{showUploadList && (!fileListRender) && renderDefaultFileList()}{}{showUploadList && fileListRender && fileListRender()}</div>);
});export default CustomizeUpload;
import { ButtonProps, UploadFile } from 'antd';
import { RcFile } from 'antd/es/upload';
export type UploadFileStatus = 'uploading' | 'done' | 'error' | 'removed';type PropertiesToOmit = 'uid' | 'name';
export interface CustomUploadFile extends Omit<UploadFile, PropertiesToOmit> {status?: UploadFileStatus;percent?: number;originFileObj?: RcFile;fileId: string;fileName: string;
}enum FileSizeLimits {MAX_SIZE = 150,MAX_COUNT = 50,
}
export interface CustomUploadProps {defaultFileList?: any[];multiple?: boolean;accept?: string;disabled?: boolean;maxCount?: FileSizeLimits.MAX_SIZE;maxSize?: FileSizeLimits.MAX_SIZE;showUploadList?: boolean;fileListRender?: () => React.ReactNode;beforeUpload?: (file: File, fileList: File[]) => boolean | Promise<boolean>;customRequest?: (options: {file: File;onSuccess: (response: any, file: CustomUploadFile) => void;onError: (error: Error) => void;onProgress: (percent: number, file: CustomUploadFile) => void;}) => void;onChange: (info: ChangeInfoProps) => void;onRemove?: (file: CustomUploadFile) => void | boolean | Promise<void | boolean>;dragContent?: React.ReactNode;buttonContent?: React.ReactNode;isDragger: boolean;uploadBtnProps?: UploadBtnProps;
}type OtherBtnProps = {uploadTitle: string;
};
export type ChangeInfoProps = { type: uploadStatus;file: CustomUploadFile; fileList: CustomUploadFile[] };
type uploadStatus = 'success' | 'error' | 'remove';type UploadBtnProps = OtherBtnProps & Omit<ButtonProps, 'loading' | 'onClick' | 'icon'>;
export interface CustomUploadRef {upload: (file: File) => void;clear: () => void;getFileList: () => CustomUploadFile[];
}
.custom-drag-upload {width: 100%;
}.custom-upload-drag-area {border: 1px dashed var(--upload-border-color);border-radius: 6px;padding: 20px;text-align: center;cursor: pointer;transition: border-color 0.3s;background-color: var(--upload-bg-color);
}.custom-upload-drag-area:hover:not(.disabled) {border-color: var(--primary-color);
}.custom-upload-drag-area.drag-over {border-color: var(--primary-color);background-color: var(--upload-hover-bg-color);
}.custom-upload-drag-area.disabled {cursor: not-allowed;opacity: 0.5;
}.custom-upload-button .ant-btn {transition: all 0.3s;
}.custom-upload-list {margin-top: 10px;
}.custom-upload-list-item {display: flex;align-items: flex-start;align-items: center;padding: 2px 12px 2px 10px;border: 1px solid var(--upload-border-color);border-radius: 2px;margin-bottom: 2px;color: var(--text-color);background-color: var(--bg-primary);&:hover {color: var(--primary-color);}
}.custom-upload-file-name {flex: 1;text-align: left;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;
}.custom-upload-progress,
.custom-upload-error {margin-left: 8px;font-size: 12px;
}.ant-btn-disabled {cursor: not-allowed;
}