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={
<UploadOutlined
style={{ 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>
{}
<EyeOutlined
size={16}
title={i18nValue('预览')}
style={{ marginLeft: 8, cursor: 'pointer', color: 'var(--primary-color)' }}
/>
<DownloadOutlined
size={16}
title={i18nValue('下载')}
style={{ marginLeft: 8, cursor: 'pointer', color: 'var(--primary-color)' }}
/>
<DeleteOutlined
className="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">
{}
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple={multiple}
disabled={disabled}
onChange={handleFileInputChange}
style={{ display: 'none' }}
/>
{}
{isDragger && <Spin spinning={loading} tip={i18nValue('附件上传中')} >
<div
className={`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 }}>
<Button
type="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;
}