【阿里云SDK+React】前端实现带进度条的分片上传

1,756 阅读7分钟

概述

超过100MB的情况,阿里云直接上传会报错,调研了一下阿里云的文件上传功能并进行了以下相关实践

上传文件

主要分为:简单上传、分片上传、断点续传

  • 简单上传
    • File对象、Blob数据以及OSS Buffer上传到OSS
    • 弊端:图片不支持20MB以上,文件不支持100MB以上
  • 分片上传
    • 上传大于100 MB且小于48.8 TB
    • 可计算上传进度
  • 断点续传
    • 文件上传过程中因网络异常或程序崩溃导致文件上传失败时,可继续上传未完成的部分

上传流程设计

  • 初始化一个对象存储文件进度

  • 初始化一个数组存储上传的文件数组

  • 每次上传初始化一个文件选择器

流程图.jpeg

效果预览

Jul-15-2022 12-08-59.gif

组件开发

  • 准备工作

    • 使用RAM用户,获取上传凭证
    • 设置跨域资源共享(CORS)
    •   具体操作,请参见设置跨域资源共享
    •   通过浏览器直接访问OSS时,CORS配置规则要求如下:
    • 来源:设置精准域名(例如www.aliyun.com)或带有通配符星号(*)的域名(例如*.aliyun.com)。
    • 允许Methods:请根据实际使用场景,选择不同的Methods。例如分片上传时,设置为PUT;删除文件时,设置为DELETE
    • 允许Headers:设置为*
    • 暴露Headers:设置为ETagx-oss-request-idx-oss-version-id

安装阿里云支持

yarn add ali-oss

具体代码实现

1、基础组件及获取鉴权

import { getBatchImageZoomUrl, getOSSData } from "@/common/dataSource/oss";
import { Button, Progress, Upload } from "antd";
import { useEffect, useState, useRef } from 'react'
import './OssUploadFile.less'
import numeral from 'numeral'
const OSS = require('ali-oss');
const defaultImage = 'https://image.baidu.com/search/index?ct=201326592&z=undefined&tn=baiduimage&ipn=d&word=%E6%96%87%E4%BB%B6&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=undefined&hd=undefined&latest=undefined&copyright=undefined&cs=3382622322,308361337&os=2045226791,2317509516&simid=3382622322,308361337&pn=124&di=7108135681917976577&ln=1948&fr=&fmq=1658197673394_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=5a&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%253A%252F%252Fimg.zcool.cn%252Fcommunity%252F010a005544672f0000019ae9eee026.jpg%25402o.jpg%26refer%3Dhttp%253A%252F%252Fimg.zcool.cn%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1660789581%26t%3Dca4d615c293ec7590ca4bd9dc84792d5&rpstart=0&rpnum=0&adpicid=0&nojc=undefined&tt=1&dyTabStr=MCwzLDYsNSwyLDEsNCw4LDcsOQ%3D%3D';
enum UploadMethodType {
  SECOND = 'second',
  MULTIPART = 'multipart'
}
enum StatusType {
  PENDING = 'pending',
  DONE = 'done',
}
type ShowListType = {
  name: string,
  url: string,
  key: string,
  type: UploadMethodType,
  status: StatusType }
function OssUploadFile() {
  const fileInputRef = useRef(null);
  const [showList, setShowList] = useState<ShowListType[]>([])
  const [uploadPercentMap, setUploadPercentMap] = useState({})
  const [oSSData, setOSSData] = useState({})

  async function initOssClient() {
    const info = await getOSSData()
    setOSSData(info)
  };

  function commonUpload(file) {
  
  }

  function multipart(file) {
    
  }
  function uploadOss() {
    // status.innerText = 'Uploading';
    // 获取STS Token
    if (!oSSData?.accessId) {
      return
    }
    const { files } = fileDom;
    const fileList = Array.from(files);
    fileList.forEach(file => {
      // 如果文件大学小于分片大小,使用普通上传,否则使用分片上传
      if (file.size / 1024 / 1024 < 100) {
        commonUpload(file);
      } else {
        multipart(file)
      }
    });
  }
  useEffect(() => {
    initOssClient()

  }, [])
  function openFile() {
    fileInputRef.current?.click()
  }
  return (
    <div className="oss-upload-file-container">
      <input type="file" id='fileDom' multiple onChange={uploadOss} ref={fileInputRef} style={{ display: "none" }} />
      <Button onClick={openFile}>上传文件</Button>
      <Upload />
      <div className="show-list">
        {showList?.map(one => {
          const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0')
          return <div key={one.key} className='file-item'>
            {!one.url && one.type === UploadMethodType.MULTIPART ? <Progress percent={percent} width={80} type="circle" /> : <img src={one.url} key={one.url || defaultImage} width={80} height={80} />}
          </div>
        })}
      </div>
    </div>
  )
}

export default OssUploadFile

2、文件格式校验及文件唯一标示生成

// 上传前的检查及file格式化处理
  const beforeUpload = async (file: any) => {
    let limitFormat = { number: limit, size: 'MB', type: '图片' };
    let isLt2M = limit && file.size / 1024 / 1024 < limit;
    // 文件处理
    if (file.type?.split('/')?.[0] !== 'image') {
      isLt2M = fileLimit && file.size / 1024 / 1024 < fileLimit;
      limitFormat = { number: fileLimit, size: 'MB', type: '文件' };
    }
    // 大小转换
    if (limitFormat.number && limitFormat.number > 1024) {
      limitFormat = {
        number: Number(`${limitFormat.number / 1024}`?.split('.')?.[0]),
        size: 'GB',
        type: limitFormat.type,
      };
    }
    // 文件大小判断
    if (!isLt2M) {
      message.error(
        `${file.name} ${limitFormat.type}大小应小于 ${limitFormat?.number}${limitFormat.size}!`,
      );
      return 'LIST_IGNORE';
    }
    
    const expire = Number(oSSData.expire) - 60 || 0; // 减去60 秒 留出缓冲时间
    if (expire * 1000 < Date.now()) {
      await initOssClient();
    }
    // 文件个数判断
    if (fileNum && showList.length + uploadRef.current >= fileNum) {
      message.error(`${file.name}上传失败,超过最大限制${fileNum}个`);
      return 'LIST_IGNORE';
    }
    // 唯一值设置
    uploadRef.current += 1;
    const suffix = file.name?.slice(file.name.lastIndexOf('.'));
    const filename = `${Date.now()}${uploadRef.current}${suffix}`;
    file.key = oSSData.dir + filename; // eslint-disable-line
    // 上传类型判断
    if (file.size / 1024 / 1024 < 10) {
      commonUpload(file);
    } else {
      multipart(file);
    }
  };

3、简单上传

function commonUpload(file) {
    // 建立阿里云连接
    const ossClient = new OSS({
      region: 'oss-cn-beijing',
      accessKeyId: oSSData?.accessId,
      accessKeySecret: oSSData?.accessKey,
      bucket: oSSData?.bucketName,
    });
    // 新增待上传文件
    file.status = StatusTypeEnum.PENDING;
    const fileKey = file.key;
    showOssUploadFileListRef.current = [
      ...showOssUploadFileListRef.current,
      {
        ...file,
        key: fileKey,
        name: file.name,
        size: file.size,
        type: UploadMethodType.SECOND,
      },
    ];
    setShowList(showOssUploadFileListRef.current);
    return ossClient
      .put(fileKey, file)
      .then((result) => {
        // 更新上传文件地址
        getBatchImageZoomUrl({ picNames: result.name, proportion: 10 }).then((urls) => {
          message.success('上传成功');
          uploadEnd && uploadEnd();
          showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
            if (fileItem.key === fileKey) {
              return {
                ...fileItem,
                status: StatusTypeEnum?.DONE,
                url: urls?.[0] || defaultImage,
              };
            } else {
              return fileItem;
            }
          });
          setShowList(showOssUploadFileListRef.current);
          uploadRef.current -= 1;
          props?.onChange?.(showOssUploadFileListRef.current);
        });
      })
      .catch(() => {
        uploadEnd && uploadEnd();
        message.error('上传失败~');
        removeImg(fileKey);
        uploadRef.current -= 1;
      });
  }

4、分片上传

// 分片上传
 function multipart(file) {
    // 建立阿里云连接
    const ossClient = new OSS({
      region: 'oss-cn-beijing',
      accessKeyId: oSSData?.accessId,
      accessKeySecret: oSSData?.accessKey,
      bucket: oSSData?.bucketName,
    });
    const fileKey = file.key;
    // 新增待上传文件
    file.status = StatusTypeEnum.PENDING;
    showOssUploadFileListRef.current = [
      ...showOssUploadFileListRef.current,
      {
        ...file,
        key: fileKey,
        name: file.name,
        size: file.size,
        type: UploadMethodType.MULTIPART,
      },
    ];
    setShowList(showOssUploadFileListRef.current);
    return ossClient
      .multipartUpload(fileKey, file, {
        // 获取分片上传进度、断点和返回值。
        progress: (p, cpt, res) => {
          uploadPercentRef.current = { ...uploadPercentRef.current, [fileKey]: p };
          setUploadPercentMap(uploadPercentRef.current);
        },
        // 设置并发上传的分片数量。
        parallel: 4,
        // 设置分片大小。默认值为1 MB,最小值为100 KB。
        partSize: 1024 * 1024 * 5,
        // headers,
        // 自定义元数据,通过HeadObject接口可以获取Object的元数据。
        meta: { year: 2020, people: 'test' },
        mime: 'text/plain',
      })
      .then((result) => {
        // 更新上传文件地址
        getBatchImageZoomUrl({ picNames: result.name, proportion: 30 }).then((urls) => {
          showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
            if (fileItem.key === fileKey) {
              return {
                ...fileItem,
                status: StatusTypeEnum?.DONE,
                url: urls?.[0] || defaultImage,
              };
            } else {
              return fileItem;
            }
          });
          setShowList(showOssUploadFileListRef.current);
          uploadRef.current -= 1;
          props?.onChange?.(showOssUploadFileListRef.current);
        });
      })
      .catch((err) => {
        message.error('上传失败~');
        removeImg(fileKey);
        uploadRef.current -= 1;
      });
  }

5、自定义上传

  • 隐藏原始file,新增自定义按钮控制原始file的dom 行为
      <div id="fileDomList" style={{ display: 'none' }}>
        {fileDomList?.map((one) => (
          <input
            type="file"
            accept={accept}
            disabled={uploadDisabled}
            id={one}
            key={one}
            onChange={uploadOss}
            multiple
          />
        ))}
      </div>
      <Space>
        <span onClick={openFile}>{uploadDom}</span>
        {addonAfter && <Typography.Text type="secondary">
        {addonAfter}</Typography.Text>}
      </Space>   
  • 避免选择文件重复处理,每次点击dom按钮,新增一个file的选择器
 function openFile() {
    const fileDom = fileDomList?.[fileDomList.length - 1];
    document.getElementById(fileDom)?.click();
    setOpenFileDom(fileDom);
    setFileDomList([...fileDomList, `fileDom_${fileDomList.length}`]);
  }

拓展一:Form支持

根据form可以直接获取上传文件的数据

  • 支持外部onChang操作
  • 支持 value值变化更新
<Form.Item name="workMaterialInfoVOList" label="回执文件" required>
          <OssUploadFile             addonAfter={`图片大小不超过15MB文件大小不超过4GB支持格式:${accept}`}
            accept={accept}
          />
</Form.Item>

拓展二:自定义文件展示

  • 根据函数listRenderFunc对外暴露 文件列表showList和进度对象uploadPercentMap,可根据具体业务需求处理不用的样式
      {listRenderFunc ? (
        listRenderFunc(showList, uploadPercentMap)
      ) : (
        <div className="show-list">
          {showList?.map((one) => {
            const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0');
            return (
              <div key={one.key} className="file-item-box">
                <div className="file-item">
                  <div className="operate-action">
                    {one.status === StatusTypeEnum.DONE ? (
                      <DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
                    ) : null}
                  </div>
                  {!one.url && one.type === UploadMethodType.MULTIPART ? (
                    <span>
                      <Progress percent={percent} width={80} type="circle" />
                    </span>
                  ) : (
                    <span className="img-container">
                      <img
                        id={`js-img-${one.key}`}
                        src={one.url}
                        key={one.url || defaultImage}
                        width={80}
                        height={80}
                      />
                    </span>
                  )}
                </div>
                <div>
                  <Typography.Text ellipsis={{ tooltip: one.name }} style={{ width: 100 }}>
                    {one.name}
                  </Typography.Text>
                </div>
              </div>
            );
          })}
        </div>
      )}

拓展三:手动删除

可手动删除已上传的文件

                <div className="file-item">
                  <div className="operate-action">
                    {one.status === StatusTypeEnum.DONE ? (
                      <DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
                    ) : null}
                  </div>
                  {!one.url && one.type === UploadMethodType.MULTIPART ? (
                    <span>
                      <Progress percent={percent} width={80} type="circle" />
                    </span>
                  ) : (
                    <span className="img-container">
                      <img
                        id={`js-img-${one.key}`}
                        src={one.url}
                        key={one.url || defaultImage}
                        width={80}
                        height={80}
                      />
                    </span>
                  )}
                </div>
  • Js
  // 删除
  function removeImg(fileKey: string) {
    showOssUploadFileListRef.current = showOssUploadFileListRef.current.filter(
      (fileItem) => fileItem.key !== fileKey,
    );
    setShowList(showOssUploadFileListRef.current);
    props?.onChange?.(showOssUploadFileListRef.current);
  }

优化鉴权加密

  • 获取鉴权的时候增加AES解密,安装解密依赖
yarn add crypto-js
  • 解密
 // 解密AES
  function decrypted(encryptedBase64Str: string) {
    const encryptedkey = CryptoJS.enc.Utf8.parse(ACCESS_KEY_PASS);
    const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, encryptedkey, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return decryptedData.toString(CryptoJS.enc.Utf8);
  }
 // 获取权限
  async function initOssClient() {
    const info = await getOSSData();
    setOSSData({
      ...info,
      accessKey: decrypted(info.accessKey),
    });
  }

API

参数说明类型默认值
accept文件上传格式string'.png, .jpg, .jpeg'
limit图片上传大小number15
fileLimit文件上传大小number1024 * 4
fileNum上传最大个数number99
listRenderFunc自定义文件列表(showList: ShowListType[], uploadPercentMap: object) => React.ReactNode;
onChange外部回调(value: any, name?: any) => void;
value外部回调ShowListType[];
uploadDom自定义上传按钮React.ReactNode<Button>上传文件</Button>
addonAfter说明信息any图片大小不超过15MB, 文件大小不超过4GB
showName是否展示文件名称booleanfalse
fileDomId上传文件dom的idstringfileDom

踩坑

  • 在多个文件上传时会存在数据和渲染不一致的行为,体现在当前操作的文件不能获取到最新的数据

    • 解决方案:新增了一个全部变量,保证数据的及时性,根据setState去更新dom渲染

参考文献

[ OSS Browser.js SDK ]:用于阿里云服务的上传文档,包含简单上传、分片上传、断点上传等

[ crypto-js ]:CryptoJS (crypto.js) 为 JavaScript 提供了各种各样的加密算法在线尝试

完整代码

import { getBatchImageZoomUrl, getOSSData } from '@/common/dataSource/oss';
import { Button, message, Progress, Space, Typography } from 'antd';
import React, { useEffect, useState, useRef } from 'react';
import './OssUploadFile.less';
import numeral from 'numeral';
import { DeleteOutlined } from '@ant-design/icons';
import CryptoJS from 'crypto-js';
import { isNodeEnv } from '@sentry/utils';

const OSS = require('ali-oss');
const defaultImage = 'https://image.baidu.com/search/index?ct=201326592&z=undefined&tn=baiduimage&ipn=d&word=%E6%96%87%E4%BB%B6&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=undefined&hd=undefined&latest=undefined&copyright=undefined&cs=3382622322,308361337&os=2045226791,2317509516&simid=3382622322,308361337&pn=124&di=7108135681917976577&ln=1948&fr=&fmq=1658197673394_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=5a&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%253A%252F%252Fimg.zcool.cn%252Fcommunity%252F010a005544672f0000019ae9eee026.jpg%25402o.jpg%26refer%3Dhttp%253A%252F%252Fimg.zcool.cn%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1660789581%26t%3Dca4d615c293ec7590ca4bd9dc84792d5&rpstart=0&rpnum=0&adpicid=0&nojc=undefined&tt=1&dyTabStr=MCwzLDYsNSwyLDEsNCw4LDcsOQ%3D%3D';
enum UploadMethodType {
  SECOND = 'second',
  MULTIPART = 'multipart',
}
enum StatusTypeEnum {
  PENDING = 'pending',
  DONE = 'done',
}
type ShowListType = {
  name: string;
  url: string;
  key: string;
  type: UploadMethodType;
  status: StatusTypeEnum;
};

type OssUploadFileType = {
  accept?: string;
  limit?: number; // 上传图片大小
  fileLimit?: number; // 上传文件大小
  fileNum?: number; // 上传文件个数
  listRenderFunc?: (showList: ShowListType[], uploadPercentMap: object) => React.ReactNode; // 列表的数据
  onChange?: (value: any, name?: any) => void;
  value?: ShowListType[];
  uploadDom?: React.ReactNode;
  addonAfter?: any;
  showName: boolean;
  fileDomId?: string;
  uploadStart?: () => void;
  uploadEnd?: () => void;
  uploadDisabled?: boolean;
};
type OSSDataType = {
  accessId: string;
  accessKey: string;
  bucketName: string;
  dir: string;
  expire: string;
  host: string;
  policy: string;
  signature: string;
};

const ACCESS_KEY_PASS = '用作密码转换的key,前后端一致即可';
const OssUploadFile: React.FC<OssUploadFileType> = (props) => {
  const [showList, setShowList] = useState<ShowListType[]>([]);
  const [uploadPercentMap, setUploadPercentMap] = useState({});
  const [oSSData, setOSSData] = useState<Partial<OSSDataType>>({});
  const [fileDomList, setFileDomList] = useState([props?.fileDomId]);
  const uploadRef = useRef(0);
  const openFileDomRef = useRef('');
  const uploadPercentRef = useRef({});
  const showOssUploadFileListRef = useRef<ShowListType[]>([]);

  const {
    limit,
    fileNum,
    fileLimit,
    accept,
    listRenderFunc,
    uploadDom,
    addonAfter,
    uploadStart,
    uploadEnd,
    uploadDisabled,
  } = props;

  // 解密AES
  function decrypted(encryptedBase64Str: string) {
    const encryptedkey = CryptoJS.enc.Utf8.parse(ACCESS_KEY_PASS);
    const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, encryptedkey, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return decryptedData.toString(CryptoJS.enc.Utf8);
  }

  async function initOssClient() {
    const info = await getOSSData();
    setOSSData({
      ...info,
      accessKey: decrypted(info.accessKey),
    });
  }

  useEffect(() => {
    if (props?.value) {
      setShowList(props.value);
      showOssUploadFileListRef.current = props.value;
    }
  }, [props.value]);
  // 删除
  function removeImg(fileKey: string) {
    showOssUploadFileListRef.current = showOssUploadFileListRef.current.filter(
      (fileItem) => fileItem.key !== fileKey,
    );
    setShowList(showOssUploadFileListRef.current);
    props?.onChange?.(showOssUploadFileListRef.current);
  }

  function commonUpload(file) {
    // 建立阿里云连接
    const ossClient = new OSS({
      region: 'oss-cn-beijing',
      accessKeyId: oSSData?.accessId,
      accessKeySecret: oSSData?.accessKey,
      bucket: oSSData?.bucketName,
    });
    // 新增待上传文件
    file.status = StatusTypeEnum.PENDING;
    const fileKey = file.key;
    showOssUploadFileListRef.current = [
      ...showOssUploadFileListRef.current,
      {
        ...file,
        key: fileKey,
        name: file.name,
        size: file.size,
        type: UploadMethodType.SECOND,
      },
    ];
    setShowList(showOssUploadFileListRef.current);
    return ossClient
      .put(fileKey, file)
      .then((result) => {
        // 更新上传文件地址
        getBatchImageZoomUrl({ picNames: result.name, proportion: 10 }).then((urls) => {
          message.success('上传成功');
          uploadEnd && uploadEnd();
          showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
            if (fileItem.key === fileKey) {
              return {
                ...fileItem,
                status: StatusTypeEnum?.DONE,
                url: urls?.[0] || defaultImage,
              };
            } else {
              return fileItem;
            }
          });
          setShowList(showOssUploadFileListRef.current);
          uploadRef.current -= 1;
          props?.onChange?.(showOssUploadFileListRef.current);
        });
      })
      .catch(() => {
        uploadEnd && uploadEnd();
        message.error('上传失败~');
        removeImg(fileKey);
        uploadRef.current -= 1;
      });
  }
  // 分片上传
  function multipart(file) {
    // 建立阿里云连接
    const ossClient = new OSS({
      region: 'oss-cn-beijing',
      accessKeyId: oSSData?.accessId,
      accessKeySecret: oSSData?.accessKey,
      bucket: oSSData?.bucketName,
    });
    const fileKey = file.key;
    // 新增待上传文件
    file.status = StatusTypeEnum.PENDING;
    showOssUploadFileListRef.current = [
      ...showOssUploadFileListRef.current,
      {
        ...file,
        key: fileKey,
        name: file.name,
        size: file.size,
        type: UploadMethodType.MULTIPART,
      },
    ];
    setShowList(showOssUploadFileListRef.current);
    return ossClient
      .multipartUpload(fileKey, file, {
        // 获取分片上传进度、断点和返回值。
        progress: (p, cpt, res) => {
          uploadPercentRef.current = { ...uploadPercentRef.current, [fileKey]: p };
          setUploadPercentMap(uploadPercentRef.current);
        },
        // 设置并发上传的分片数量。
        parallel: 4,
        // 设置分片大小。默认值为1 MB,最小值为100 KB。
        partSize: 1024 * 1024 * 5,
        // headers,
        // 自定义元数据,通过HeadObject接口可以获取Object的元数据。
        meta: { year: 2020, people: 'test' },
        mime: 'text/plain',
      })
      .then((result) => {
        // 更新上传文件地址
        getBatchImageZoomUrl({ picNames: result.name, proportion: 30 }).then((urls) => {
          showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
            if (fileItem.key === fileKey) {
              return {
                ...fileItem,
                status: StatusTypeEnum?.DONE,
                url: urls?.[0] || defaultImage,
              };
            } else {
              return fileItem;
            }
          });
          setShowList(showOssUploadFileListRef.current);
          uploadRef.current -= 1;
          props?.onChange?.(showOssUploadFileListRef.current);
        });
      })
      .catch((err) => {
        message.error('上传失败~');
        removeImg(fileKey);
        uploadRef.current -= 1;
      });
  }
  // 上传前的检查及file格式化处理
  const beforeUpload = async (file: any) => {
    let limitFormat = { number: limit, size: 'MB', type: '图片' };
    let isLt2M = limit && file.size / 1024 / 1024 < limit;
    // 文件处理
    if (file.type?.split('/')?.[0] !== 'image') {
      isLt2M = fileLimit && file.size / 1024 / 1024 < fileLimit;
      limitFormat = { number: fileLimit, size: 'MB', type: '文件' };
    }
    // 大小转换
    if (limitFormat.number && limitFormat.number > 1024) {
      limitFormat = {
        number: Number(`${limitFormat.number / 1024}`?.split('.')?.[0]),
        size: 'GB',
        type: limitFormat.type,
      };
    }
    if (!isLt2M) {
      message.error(
        `${file.name} ${limitFormat.type}大小应小于 ${limitFormat?.number}${limitFormat.size}!`,
      );
      return 'LIST_IGNORE';
    }
    const expire = Number(oSSData.expire) - 60 || 0; // 减去60 秒 留出缓冲时间
    if (expire * 1000 < Date.now()) {
      await initOssClient();
    }
    if (fileNum && showList.length + uploadRef.current >= fileNum) {
      message.error(`${file.name}上传失败,超过最大限制${fileNum}个`);
      return 'LIST_IGNORE';
    }
    uploadRef.current += 1;
    const suffix = file.name?.slice(file.name.lastIndexOf('.'));
    const filename = `${Date.now()}${uploadRef.current}${suffix}`;
    file.key = oSSData.dir + filename; // eslint-disable-line
    if (file.size / 1024 / 1024 < 10) {
      commonUpload(file);
    } else {
      multipart(file);
    }
  };

  function uploadOss() {
    if (!oSSData?.accessId) {
      return;
    }
    const { files } = document.getElementById(openFileDomRef.current);
    const fileList = Array.from(files);
    uploadStart && uploadStart();
    message.success('开始上传...');
    for (let i = 0; i < fileList.length; i++) {
      beforeUpload(fileList[i]);
    }
  }

  useEffect(() => {
    initOssClient();
  }, []);

  function openFile() {
    const fileDom = fileDomList?.[fileDomList.length - 1];
    document.getElementById(fileDom)?.click();
    openFileDomRef.current = fileDom;
    setFileDomList([...fileDomList, `fileDom_${fileDomList.length}`]);
  }
  return (
    <div className="oss-upload-file-container">
      <div id="fileDomList" style={{ display: 'none' }}>
        {fileDomList?.map((one) => (
          <input
            type="file"
            accept={accept}
            disabled={uploadDisabled}
            id={one}
            key={one}
            onChange={uploadOss}
            multiple
          />
        ))}
      </div>
      <Space>
        <span onClick={openFile}>{uploadDom}</span>
        {addonAfter && <Typography.Text type="secondary">{addonAfter}</Typography.Text>}
      </Space>
      {listRenderFunc ? (
        listRenderFunc(showList, uploadPercentMap)
      ) : (
        <div className="show-list">
          {showList?.map((one) => {
            const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0');
            return (
              <div key={one.key} className="file-item-box">
                <div className="file-item">
                  <div className="operate-action">
                    {one.status === StatusTypeEnum.DONE ? (
                      <DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
                    ) : null}
                  </div>
                  {!one.url && one.type === UploadMethodType.MULTIPART ? (
                    <span>
                      <Progress percent={percent} width={80} type="circle" />
                    </span>
                  ) : (
                    <span className="img-container">
                      <img
                        id={`js-img-${one.key}`}
                        src={one.url}
                        key={one.url || defaultImage}
                        width={80}
                        height={80}
                      />
                    </span>
                  )}
                </div>
                <div>
                  <Typography.Text ellipsis={{ tooltip: one.name }} style={{ width: 100 }}>
                    {one.name}
                  </Typography.Text>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};
OssUploadFile.defaultProps = {
  accept: '.png, .jpg, .jpeg',
  limit: 15,
  fileLimit: 1024 * 4,
  fileNum: 99,
  uploadDom: <Button>上传文件</Button>,
  addonAfter: `图片大小不超过15MB, 文件大小不超过4GB`,
  showName: false,
  fileDomId: 'fileDom',
};
export default OssUploadFile;