二次封装CustomUpload自定义文件上传组件,基于react+antdesign封装

329 阅读4分钟

使用方法

   <>
      <h2>自定义上传图片组件</h2>
      <CustomUpload
        defaultValue={defaultData}  //默认值可用于图片回显config={configImage}         //配置
        onUpdateFileNames={handleFileNamesUpdate}   // 可以拿到上传图片的name用来下载上传的图片
        onRemove={handleRemove} //可以拿到删除图片name字段组成的数组
      />
      <h2>自定义上传文件组件</h2>
      <CustomUpload
        defaultValue={defaultData}
        config={configFile}
        onUpdateFileNames={handleFileNamesUpdateFile}
        onRemove={handleRemoveFile}
      />
    </>

状态数据

  // 图片默认值,回显数据
  const [defaultData, setDefaultData] = useState<UploadFile[]>([
    {
      uid: '0',
      name: 'image.png',
      size: 1234567,
      status: 'done',
      url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
    },
  ])
  // 下载的图片状态
  const [downLoadImage, setDownLoadImage] = useState<FileInfo[]>([])
  console.log('downLoadImage::: ', downLoadImage)
  // 删除的图片状态
  const [removeImage, setRemoveImage] = useState<FileInfo[]>([])

配置所需功能属性

1.图片上传

  const configImage: configProps = {
    fileType: 'image', //配置组件类型
    // maxCount: 3,      //限制最大上传数量
    beforeUploadConfig: {
      // maxSize: 10,  //校验上传图片的大小不能小于该属性
      allowedTypes: ['image/jpeg', 'image/png', 'image/jpg'],//校验上传图片的格式
    },
    //必传
    customRequestConfig: {
      requestApi: UploadApi,   //配置上传请求接口
      openZip: 1, //大于几兆开启压缩
      zip: {
        quality: 70, //压缩的质量
        size: {
          maxWidth: 800,  //压缩后的最大宽度
          maxHeight: 800, //压缩后的最大高度
        },
        outputFormat: 'JPEG', //压缩后的格式
        outPutType: 'file',  //压缩后的文件类型
      },
    },
    preview: true,  //开启图片预览
  }

2.文件上传

 const configFile: configProps = {
  fileType: 'file',     //配置组件类型
  // maxCount: 3,       //限制最大上传数量
  beforeUploadConfig: {
    maxSize: 20, //校验上传图片的大小不能小于该属性
    allowedTypes: [
      'application/pdf',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
      'application/msword', // doc
      'application/zip', // zip,
    ],
  },
  //必传
  customRequestConfig: {
    requestApi: UploadApi,
  },
  preview: true,      //开启图片预览
  showFileList: true,    //开启上传文件list
}

删除和下载图片

  // 删除图片
 const handleRemove = (options: any) => {
   const { name } = options
   console.log('options::: ', options)
   setRemoveImage((prev) => [...prev, name])
 }
  // 下载上传的图片
 const handleFileNamesUpdate = async (options: string[]) => {
   try {
     const result = await regularOrderRequest(options, DownLoadApi) // 调用函数并等待结果
     setDownLoadImage([...result])
   } catch (error) {
     console.error('Error: ', error)
   }
 }

使用util

/**
* 将file转为base64
* @param file 
* @returns 
*/
const getBase64 = (file: any): Promise<string> =>
 new Promise((resolve, reject) => {
   const reader = new FileReader()
   reader.readAsDataURL(file)
   reader.onload = () => resolve(reader.result as string)
   reader.onerror = (error) => reject(error)
 })
/**
* 解决多个参数请求接口按顺序返回数据的问题
* @param params 请求所需要传的参数  string[]
* @param requestApi 请求接口
* @returns  Promise<[]>
*/
const regularOrderRequest = async (params: string[], requestApi: any) => {
 const result = []
 let index = 0
 for (const item of params) {
   try {
     const res = await requestApi({ filename: item }) // 按顺序等待请求完成
     const blob = new Blob([res])
     const base64 = await getBase64(blob) // 将 Blob 转换为 base64
     result.push({
       name: item,
       uid: `${index + 1}`,
       status: 'done',
       url: base64,
     })
     index++ // 每次循环后递增 index
   } catch (error) {
     console.error('Error during request:', error)
   }
 }
 return result
}
export { displayImage, getBase64, regularOrderRequest }

CustomUpload源码展示

组件内主要内置了图片压缩功能,以及可以通过配置config属性实现,可以直接拿来使用,避免页面文件过多的逻辑代码。

import React, { useState, memo } from 'react'
import { Upload, message, Image } from 'antd'
import { UploadFile, UploadProps } from 'antd/es/upload/interface'
import { handleImageResize, filesPermission, textTip, getFileType, changeType, getBase64, downloadFile } from './utils'
import { OnUpdateFileNames, configProps } from './type'
import { DownloadOutlined } from '@ant-design/icons'
import './index.less'

interface CustomUploadProps extends UploadProps {
  customOnChange?: (fileList: UploadFile[]) => void
  beforeUpload?: (file: File) => boolean | Promise<File>
  defaultValue: UploadFile[]
  onUpdateFileNames: OnUpdateFileNames
  config: configProps
  downFilesApi?: (fileList: UploadFile[]) => void
}

const CustomUpload: React.FC<CustomUploadProps> = ({
  onUpdateFileNames,
  defaultValue = [],
  customOnChange,
  beforeUpload,
  config,
  downFilesApi,
  ...rest
}) => {
  const {
    fileType,
    maxCount = 3,
    beforeUploadConfig = { maxSize: 3, allowedTypes: [] },
    customRequestConfig,
    preview,
    showFileList,
  } = config
  const { maxSize = 3, allowedTypes } = beforeUploadConfig
  const { requestApi, openZip = 0, zip } = customRequestConfig

  const [fileList, setFileList] = useState<UploadFile[]>(defaultValue)
  const [saveImageName, setSaveImageName] = useState<string[]>([])

  const type_message = textTip(fileType)
  // 内置上传方法
  const settingBeforeUpload = (file: File) => {
    if (file.size > 1024 * 1024 * maxSize) {
      message.error(`不能超过文件最大容量${maxSize}MB!`)
      return false
    }

    if (!filesPermission(allowedTypes, file.type)) {
      message.error(`不支持的${type_message}类型!`)
      return false
    }
    return true
  }

  // 内置自定义请求方法
  const handleCustomRequest = async (options: any) => {
    const { onSuccess, onError, file, onProgress } = options
    if (!file || !(file instanceof File)) {
      // console.error('无效的文件:', file) // 确认文件对象是否有效
      onError(`无效的${type_message}!`)
      return
    }
    try {
      const formData = new FormData()
      const fileSizeInMB = file.size / (1024 * 1024)
      if (fileSizeInMB > openZip && zip) {
        const resizedImage = (await handleImageResize(file, zip)) as Blob // 确保类型为 Blob
        formData.append('file', new File([resizedImage], file.name, { type: file.type }))
      } else {
        formData.append('file', file)
      }
      const res = await requestApi(formData)
      if (res.message === 'Success') {
        const newFiles = res.file
        setSaveImageName((prev: string[]) => [...prev, ...newFiles])
        onUpdateFileNames([...saveImageName, ...newFiles])
        onSuccess(`${type_message}上传成功`)
      } else {
        onError(`${type_message}上传失败`)
      }
    } catch (error) {
      onError(`处理${type_message}时出错`)
    }
  }

  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    setFileList(newFileList)
    if (customOnChange) {
      customOnChange(newFileList)
    }
  }

  const handleBeforeUpload = (file: File) => {
    return beforeUpload ? beforeUpload(file) : settingBeforeUpload(file)
  }

  const [previewOpen, setPreviewOpen] = useState(false)
  const [previewImage, setPreviewImage] = useState('')
  // 弹窗查看图片downloadFile
  const handlePreview = async (file: UploadFile) => {
    const allowedExtensions = ['png', 'jpg', 'jpeg', 'gif']
    const fileExtension = file.name.split('.').pop() // 获取文件扩展名并转换为小写
    if (fileExtension && !allowedExtensions.includes(fileExtension)) {
      downloadFile(file.originFileObj, file.name)
    } else {
      if (!file.url && !file.preview) {
        file.preview = await getBase64(file.originFileObj)
      }
      setPreviewImage(file.url || (file.preview as string))
      setPreviewOpen(true)
    }
  }

  const showUploadList = {
    extra: ({ size = 10 }) => {
      return <span style={{ color: '#cccccc' }}>({(size / 1024 / 1024).toFixed(2)}MB)</span>
    },
    showDownloadIcon: true,
    downloadIcon: React.createElement(DownloadOutlined),
    showRemoveIcon: true,
  }

  return (
    <>
      <Upload
        {...rest}
        listType={fileType === 'image' ? 'picture-card' : 'text'}
        fileList={showFileList ? fileList : []}
        onChange={handleChange}
        beforeUpload={(file) => {
          if (!getFileType(fileType, file, allowedTypes)) {
            message.error(`不支持的${type_message}格式!`)
            return Upload.LIST_IGNORE
          }
          return handleBeforeUpload(file)
        }}
        customRequest={customRequestConfig ? handleCustomRequest : undefined}
        onPreview={preview ? handlePreview : undefined}
        showUploadList={fileType === 'file' ? showUploadList : undefined}>
        {changeType(fileType, fileList, maxCount)}
      </Upload>
      {previewImage && (
        <Image
          wrapperStyle={{ display: 'none' }}
          preview={{
            visible: previewOpen,
            onVisibleChange: (visible) => {
              setPreviewOpen(visible)
              if (!visible) {
                setPreviewImage('') // 关闭时清空预览图片
              }
            },
          }}
          src={previewImage}
        />
      )}
    </>
  )
}

// 设置默认值
CustomUpload.defaultProps = {
  config: {
    fileType: 'image',
    beforeUploadConfig: {
      maxSize: 3,
      allowedTypes: [],
    },
    customRequestConfig: {
      requestApi: (data: any) => {},
      openZip: 1,
      zip: {
        quality: 60,
        size: {
          maxWidth: 800,
          maxHeight: 800,
        },
        outputFormat: 'jpeg',
        outPutType: 'File',
      },
    },
    preview: false,
    showFileList: false,
  },
}

export default memo(CustomUpload)
//暂时解决预览图片不居中的问题
.ant-image-preview-img-wrapper{
  display: flex;
  justify-content: center;
  align-items: center;
  img{
    max-width: 900px;
    max-height:900px;
  }
  }
export type FileInfo = {
  name: string
  url: string
  uid: string
  status: string
}

// 使用 interface 定义函数类型
export interface OnUpdateFileNames {
  (val: string[]): void
}
export type sizeInfo = {
  maxWidth: number
  maxHeight: number
}
export interface zipProps {
  quality: number
  size: sizeInfo
  outputFormat: string
  outPutType: string
}
export interface configProps {
  fileType: 'image' | 'file' | 'both'
  maxCount?: number
  beforeUploadConfig: {
    maxSize?: number
    allowedTypes: string[]
  }
  customRequestConfig: {
    requestApi: any
    openZip?: number
    zip?: zipProps
  }
  preview?: boolean
  showFileList?: boolean
}
import React from 'react'
import Resizer from 'react-image-file-resizer'
import { Button } from 'antd'
import { zipProps } from './type'
import { UploadOutlined } from '@ant-design/icons'

/**
 * 压缩图片处理函数
 * @param file
 * @param zip
 * @returns
 */
const handleImageResize = (file: File, zip: zipProps) => {
  const { quality = 60, size, outputFormat = 'jpeg', outPutType = 'file' } = zip
  return new Promise((resolve, reject) => {
    Resizer.imageFileResizer(
      file,
      size.maxWidth, // 最大宽度
      size.maxHeight, // 最大高度
      outputFormat, // 输出格式
      quality, // 压缩质量
      0, // 图片旋转角度
      (resizedImage) => {
        resolve(resizedImage) // 返回压缩后的图片
      },
      outPutType, // 输出类型,返回文件
    )
  })
}
/**
 * 校验上传的文件是否是支持的格式
 * @param fileType
 * @param file
 * @param allowedTypes
 * @returns
 */
const getFileType = (fileType: string, file: File, allowedTypes: string[]) => {
  if (fileType === 'image') {
    return allowedTypes.includes(file.type)
  }
  if (fileType === 'file') {
    return allowedTypes.includes(file.type)
  }
  return true
}
/**
 * 根据不同的文件类型显示不同的样式
 * @param fileType
 * @param fileList
 * @param maxCount
 * @returns
 */
const changeType = (fileType: any, fileList: any, maxCount: number) => {
  switch (fileType) {
    case 'image':
      return fileList.length < maxCount && '+ Upload'
    case 'file':
      return (
        <Button icon={React.createElement(UploadOutlined)}>
          {fileType === 'image' ? 'Upload Image' : fileType === 'file' ? 'Upload File' : 'Upload'}
        </Button>
      )
    case 'both':
      return (
        <Button icon={React.createElement(UploadOutlined)}>
          {fileType === 'image' ? 'Upload Image' : fileType === 'file' ? 'Upload File' : 'Upload'}
        </Button>
      )
  }
}
function filesPermission(type: any, fileType: any) {
  if (!type.includes(fileType)) {
    return false
  }
  return true
}
const textTip = (type: string) => {
  return type === 'image' ? '图片' : '文件'
}
/**
 * 将file转为base64
 * @param file
 * @returns
 */
const getBase64 = (file: any): Promise<string> =>
  new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => resolve(reader.result as string)
    reader.onerror = (error) => reject(error)
  })
/**
 * 浏览器下载文件方法
 * @param file File
 * @param name string
 */
const downloadFile = (file: any, name: string) => {
  let downloadElement = document.createElement('a')
  // 创建下载的链接
  let href = window.URL.createObjectURL(file)
  downloadElement.href = href
  // 下载后文件名
  downloadElement.download = name
  document.body.appendChild(downloadElement)
  // 点击下载
  downloadElement.click()
  // 下载完成移除元素
  document.body.removeChild(downloadElement)
  // 释放掉blob对象
  window.URL.revokeObjectURL(href)
}

export { handleImageResize, filesPermission, textTip, getFileType, changeType, getBase64, downloadFile }