当前位置: 首页 > news >正文

input + React自定义上传组件【可自定义拓展】

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';/*** 自定义拖拽上传组件* 支持拖拽上传和点击上传,保持与 Ant Design Upload 一致的 API* 包括操作:* fileList 文件列表* multiple 是否一次性上传多个* accept 支持的文件类型* disable 是否禁用上传* maxCount 允许用户上传的最大数量* isDragger 是否可拖拽上传* showUploadList 展示文件列表* fileListRender 自定义渲染文件列表* beforeUpload 上传前的校验方法* customRequest 自定义请求* onChange 文件变化回调事件* onRemove 删除事件* dragContent 拖拽区域文字*/
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, '====================');// setFileList(prev => prev.map(item =>//   (item.fileId === fileItem.fileId ? updatedFile : item),// ));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) => {// 执行 onRemove 钩子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;/** 需要进行转换则增加转换逻辑 */// let fileConfig = { };handleUploadSuccess && handleUploadSuccess(res.data, {});message.success(i18nValue('上传成功'));} else if (res?.code) {handleUploadError(fileItem, res?.message, updatedFileList);// showErrorNotification(i18nValue(res?.message) || i18nValue('上传失败'), i18nValue('提示'));}} 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;// 执行 beforeUpload 钩子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]);/*** @description 处理文件输入变化* @param {React.ChangeEvent<HTMLInputElement>} eFile* @returns {Function} null* @version 1.0.0* @author 2209150234* @date 2025-09-04 20:15:34*/const handleFileInputChange = useCallback((eFile: React.ChangeEvent<HTMLInputElement>) => {const { files } = eFile.target;if (files && files.length > 0) {handleFileSelect(files);// 重置 input 值以便再次选择相同文件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>);/*** @fileoverview 默认文件列表渲染* @author 2209150234* @date 2025-09-04* @version 1.0.0*/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>{/* { ['uploading','error].includes(file.status) && (<span className="custom-upload-progress">{file.percent!== 100 && <LoadingOutlined style={{ marginRight: 8 }} />}{file.percent && file.percent!== 100 ? `${(file.percent)}%` : ''}</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;/** 文件ID */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;/** 文件大小限制(MB) */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;/** 上传按钮PROPS */uploadBtnProps?: UploadBtnProps;
}type OtherBtnProps = {/** 上传按钮名称 */uploadTitle: string;
};/** 上传Change事件回调参数 */
export type ChangeInfoProps = { type: uploadStatus;file: CustomUploadFile; fileList: CustomUploadFile[] };/** 上传Change事件回调参数类型值 */
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;
}
http://www.dtcms.com/a/506692.html

相关文章:

  • 「日拱一码」125 多层特征融合
  • 第六部分:VTK进阶(第164章 复合数据集 vtkMultiBlockDataSet 组织)
  • k8s(十一)HPA部署与使用
  • 【ReaLM】结合错误数据与课程学习 提升垂域效果
  • 通了网站建设宿迁网站定制
  • Git仓库推送到GitHub
  • 本地多语言切换具体操作代码
  • 济南建设主管部门网站短视频网站如何做推广
  • AWS US-East-1 区宕机
  • C语言——关机小程序(有system()和strcmp()函数的知识点)
  • php网站案例购物网页设计图片
  • golang面经7:interface相关
  • [Agent可视化] 配置系统 | 实现AI模型切换 | 热重载机制 | fsnotify库(go)
  • 【第7篇】引入低配大模型
  • 【Linux】Linux 进程信号核心拆解:pending/block/handler 三张表 + signal/alarm 实战
  • Java-154 深入浅出 MongoDB 用Java访问 MongoDB 数据库 从环境搭建到CRUD完整示例
  • 1.云计算与服务器基础
  • 基于Draw.io的实时协作架构设计与性能优化实践
  • 网站右侧固定标题怎么做深圳品牌馆设计装修公司
  • ASP.NET MVC 前置基础:宿主环境 HttpRuntime 管道,从部署到流程拆透(附避坑指南)
  • 北京单位网站建设培训俱乐部网站方案
  • 如何将一加手机的照片传输到笔记本电脑?
  • 手机群控软件如何构建高效稳定的运营环境?
  • 云手机 无限畅玩手游 巨 椰
  • 做男装去哪个网站好网站备案后 如何建设
  • 用C语言实现代理模式
  • 云开发CloudBase AI+实战:快速搭建AI小程序全流程指南
  • ESP32学习笔记(基于IDF):连接手机热点,用TCP协议实现数据双向通信
  • 一个小程序轻量AR体感游戏,开发实现解决方案
  • java整合itext pdf实现固定模版pdf导出