Bootstrap

antd-upload通过ali-oss上传图片(文件)

上传组件封装

/**
 * OSS 上传组件
 *    props = {
 *        stsUrl:      String           sts 接口地址,
 *        accept:      String           接受上传的文件类型,
 *        multiple:     Boolean         是否支持多选
 *        validator:    (nowFile, nowFileList) => boolean | { passed: boolean, msg: String }     自定义校验方法,不同于 form 的 rules, 这个验证发生在上传前
 *        limitedNum:   Num             限制的文件数
 *
 *        value:       Array            受控的 value 属性,值就是当前的文件列表
 *        onChange:    (newFileList) => {}          组件事件监听
 *    }
 */

import React, { Component } from 'react';
import { Upload, Modal, Button, Icon, message } from 'antd';
import ImgCrop from 'antd-img-crop';
import OSS from 'ali-oss';
import fetchUtil from '../../utils/fetch-case';
import formatParams from '../../utils/formatParams';

class OSSUpload extends Component {
  constructor(props) {
    super(props);
    this.state = {
      stsOptions: null, // 当前文件秘钥对象
      value: [], // 文件列表
      previewVisible: false, // 图片预览关闭
      previewImage: '', // 图片预览地址
    };
    this.uploadingClients = [];// 上传的客户端
  }

  // 文件列表
  static getDerivedStateFromProps(props) {
    if ('value' in props) {
      const value = props.value;
      return {
        value,
      };
    }
    return null;
  }

  // 提供当前文件列表给form组件
  triggerChange = (newValue) => {
    const { onChange } = this.props;
    if (onChange) {
      onChange(newValue);
    }
  };

  // uploading done error都会调用这个函数
  onUploadChange = (changeObj) => {
    console.log(changeObj, 'changeObj');
    const { fileList } = changeObj;// fileList
    const tmpFileList = fileList.filter((item) => {
      return item.status;// 状态有:uploading done error removed
    });
    this.setState({
      value: [...tmpFileList],
    });
    this.triggerChange([...tmpFileList]);
  };

  // 文件上传前的钩子
  beforeUpload = (nowFile, nowFileList) => {
    console.log(nowFile, nowFileList, '上传前');// nowFile当前上传文件,nowFileList当前上传列表
    const { limitedNum, MAX_FILE_SIZE_TEXT, MAX_FILE_SIZE, type, accept } = this.props;
    const { value } = this.state;
    const alreadyFileListNum = Array.isArray(value) ? value.length : 0;
    const nowFileListNum = nowFileList.length;

    // 文件数目上限校验(已有个数+当前上传个数)
    if (typeof limitedNum === 'number' && (nowFileListNum + alreadyFileListNum) > limitedNum) {
      message.error(`上传失败, 原因是:超过最大文件数据上限。最大上传个数为:${limitedNum}`);
      return false;
    }

    // 文件类型判断(accept是否包含当前文件type)
    const typeList = accept.split(',').map(m => m = `image/${m.substr(1)}`);
    const typeReg = type === 'picture' ? typeList.indexOf(nowFile.type) === -1 : nowFile.type.indexOf('application/') !== 0;
    if (typeReg) {
      message.error('该文件不是期待的类型');
      return false;
    }

    // 文件大小判断(单位是b)
    if (MAX_FILE_SIZE && nowFile.size > MAX_FILE_SIZE) {
      message.error(`${nowFile.name} 文件大小超过上限,最大文件大小为: ${MAX_FILE_SIZE_TEXT}`);
      return false;
    }

    // // 自定义校验
    // const isPassed = validator(nowFile, nowFileList);
    // if (typeof isPassed === 'boolean') {
    //   if (!isPassed) {
    //     Modal.error({
    //       title: '上传失败',
    //       content: '原因是:不符合文件校验规则。',
    //     });
    //     return false;
    //   }
    // } else if (!isPassed.passed) {
    //   Modal.error({
    //     title: '上传失败',
    //     content: `原因是:不符合文件校验规则 -- ${isPassed.msg}`,
    //   });
    //   return false;
    // }
    return true;
  };

  // 自定义上传操作,上传到ali-oss(获取秘钥)
  uploadFile = (customObj) => {
    console.log(customObj);
    const { file } = customObj;
    const self = this;
    const { stsUrl, stsUrlType } = self.props;
    if (!stsUrl) {
      return false;
    }
    fetchUtil.get(stsUrl, formatParams(stsUrlType), (data) => {
      self.setState({
        stsOptions: { ...data }, // 图片秘钥
      });
      self.doOSSUpload(file, { ...data });
    }, (msg) => {
      self.setState({
        stsOptions: null,
      });
      Modal.error({
        title: 'Sorry',
        content: `获取上传鉴权失败,原因是:${msg}`,
      });
      self.onError(msg, null, file);
    });
  };

  // 获取etag
  doOSSUpload = (file, stsOptions) => {
    const self = this;
    const index1 = file.name.lastIndexOf('.');
    const index2 = file.name.length;
    const realName = `${file.name.substring(0, index1)}${Date.parse(new Date())}${file.name.substring(index1, index2)}`;
    console.log(realName, 'realName');// 包含时间戳的文件名
    const objectName = `${stsOptions.dir}/${realName}`;
    self.uploadingClients.push({
      client: new OSS({
        region: 'oss-cnnnn-beijing',
        accessKeyId: stsOptions.AccessKeyId,
        accessKeySecret: stsOptions.AccessKeySecret,
        stsToken: stsOptions.SecurityToken,
        endpoint: stsOptions.endpoint,
        expireTime: stsOptions.Expiration,
        bucket: stsOptions.bucket,
        cdnDomain: stsOptions.cdnDomain,
      }),
      // 阿里云 object-name
      objectName,
      // 阿里云 bucket 下的子目录
      dir: stsOptions.dir,
      // 枚举类型: -2 -- 初始化, -1 -- 上传中, 0 -- 上传成功, 1 -- 上传失败
      status: -1,
      // 进度 0 ~ 1
      progress: 0,
      // 展示信息
      msg: '',
      // 保存的 url
      url: '',
    });

    // 上传
    const uploadingIndex = self.uploadingClients.length - 1;
    const uploadingObj = self.uploadingClients[uploadingIndex];
    uploadingObj.client.multipartUpload(objectName, file).then((result) => {
      console.log(result, 'result');// etag
      self.onSuccess({ ...result, dir: stsOptions.dir, realname: realName }, file);
    }).catch((e) => {
      self.onError(e, null, file);
    });
  };

  // 成功(更新 fileList中当前文件uid/status/path/name)
  onSuccess = (response, fileObj) => {
    const { stsOptions, value } = this.state;
    const path = `https://${stsOptions.cdnDomain}${response.dir}/${encodeURIComponent(response.realname)}`;
    const targetUid = fileObj.uid;
    const tmpFileList = [...value];
    const len = tmpFileList.length;
    for (let i = 0; i < len; i += 1) {
      if (tmpFileList[i].uid === targetUid) {
        tmpFileList[i].status = 'done';
        tmpFileList[i].path = path;
        tmpFileList[i].objectName = response.name;
        break;
      }
    }
    this.setState({
      value: tmpFileList,
    });
    this.triggerChange([...tmpFileList]);
  };

  /**
   * 失败(更新 fileList的status = 'error')
   * @param e                         Error 对象
   * @param response                  具体响应
   * @param fileObj                   当前事件对应的文件对象
  */
  onError = (e, response, fileObj) => {
    const { value } = this.state;
    const targetUid = fileObj.uid;
    const tmpFileList = [...value];
    const len = tmpFileList.length;
    for (let i = 0; i < len; i += 1) {
      if (tmpFileList[i].uid === targetUid) {
        tmpFileList[i].status = 'error';
        break;
      }
    }
    this.setState({
      value: tmpFileList,
    });
    this.triggerChange([...tmpFileList]);
    // 提示
    Modal.error({
      title: 'Sorry',
      content: `${fileObj.name}上传失败,原因是:${e.message ? e.message : e}`,
    });
  };

  // 移除文件
  onRemove = (fileObj) => {
    const { handleRemove } = this.props;
    this.removeTargetFileFromFileList(fileObj);
    if (handleRemove) handleRemove();
  };

  // 从 state 中的 fileList 中移除指定的 file
  removeTargetFileFromFileList = (fileObj) => { // filter当前id
    const targetUid = fileObj.uid;
    const { value } = this.state;
    const tmpFileList = value.filter((item) => {
      return item.uid !== targetUid;
    });
    this.setState({
      value: tmpFileList,
    });
    this.triggerChange([...tmpFileList]);
  };

  // 文件预览
  handlePreview = (file) => {
    console.log(file, '预览file');
    this.setState({
      previewImage: file.url || file.thumbUrl,
      previewVisible: true,
    });
  }

  // 预览关闭
  handleCancel = () => this.setState({ previewVisible: false })

  render() {
    const { accept, multiple, type, isEnd, limitedNum, cropSettings } = this.props;
    const { value, previewVisible, previewImage } = this.state;
    const uploadButton = (
      <div>
        <Icon type="plus" />
        <div className="ant-upload-text">Upload</div>
      </div>
    );
    const optionIcon = isEnd ? { showRemoveIcon: false, showDownloadIcon: false } : { showRemoveIcon: true, showDownloadIcon: false };

    return (
      <div
        style={{ marginBottom: '4px' }}
        className="upload-compoment"
      >
        {type === 'file' ? (// 文件上传
          <Upload
            accept={accept}
            multiple={multiple}
            fileList={value}
            beforeUpload={this.beforeUpload}
            customRequest={this.uploadFile}
            onRemove={this.onRemove}
            onChange={this.onUploadChange}
          >
            <Button>
              <Icon type="upload" /> 点击浏览文件
            </Button>
          </Upload>
        ) : (// 图片裁剪
          <div>
            {cropSettings ? (
              <ImgCrop
                {...cropSettings}
                resize={false}
                beforeCrop={this.beforeUpload}
              >
                <Upload
                  listType="picture-card"
                  accept={accept}
                  multiple={multiple}
                  fileList={value}
                  beforeUpload={this.beforeUpload}
                  customRequest={this.uploadFile}
                  onRemove={this.onRemove}
                  onChange={this.onUploadChange}
                  onPreview={this.handlePreview}
                  showUploadList={optionIcon}
                >
                  {(value.length >= limitedNum || isEnd) ? null : uploadButton}
                </Upload>
              </ImgCrop>
            ) : (// 图片列表
              <Upload
                listType="picture-card"// 列表样式
                accept={accept}// 文件类型:.jpg,.jpeg,.png
                multiple={multiple}// 是否支持多选文件
                fileList={value}// 已经上传的文件列表
                beforeUpload={this.beforeUpload}// 上传文件之前的钩子
                customRequest={this.uploadFile}// 覆盖默认的上传行为
                onRemove={this.onRemove}// 点击移除文件时的回调
                onChange={this.onUploadChange}// 上传文件改变时的状态
                onPreview={this.handlePreview}// 点击文件链接或预览图标时的回调
                showUploadList={optionIcon}
              >
                {(value.length >= limitedNum || isEnd) ? null : uploadButton}
              </Upload>
            )}
            <Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
              <img alt="example" style={{ width: '100%' }} src={previewImage} />
            </Modal>
          </div>
        )}
      </div>
    );
  }
}

OSSUpload.defaultProps = {
  stsUrl: '',
  accept: '*/*',
  multiple: false,
  validator: () => true,
  limitedNum: undefined,
  onChange: null,
};

export default OSSUpload;

上传组件调用

import { Form, Modal, message } from 'antd';
import React from 'react';
import OSSUpload from '../../../../components/oss-upload';
import { Interfaces } from '../../../../service/serverConfig';
import FetchUtils from '../../../../utils/fetch-case';

const FormItem = Form.Item;

class PicturesWall extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      urlInfo: [],//图片的回显值
    };
  }

  // 校验
  validatorFileList = (rule, value, callback) => {
    console.log(value, '校验value');
    if (!value || !value[0]) {
      // 因为 rule 里面定义了 require 规则,因此这里不需要 callback 内容
      callback();
      return true;
    }
    if (value[0].status === 'uploading') {
      callback('当前选择的文件尚未上传成功,请稍候');
      return false;
    }
    if (value[0].status === 'error') {
      callback('当前选择的文件上传失败,请重新选择文件重试');
      return false;
    }
    callback();
    return true;
  };

  // 提交
  subPic = () => {
    const { id, uploadType, form, setUploadVisible } = this.props;
    const { urlInfo } = this.state;
    const picList = form.getFieldValue('picList');
    const path = picList.map(item => item.path);// 所有的图片
    console.log(path);
    // 路径求差集(编辑后新增的图片)
    const urlInfoTransferPath = [...urlInfo].map(item => item.path);
    const differencePath = new Set([...path].filter(x => urlInfoTransferPath.findIndex(y => y === x) === -1));
    const pathArr = [...differencePath];
    console.log(differencePath, 'differencePath');
    // 删除求差集(编辑后删除的图片)
    const urlInfoTransfer = [...urlInfo].map(item => item.uid);
    const picListTransfer = [...picList].map(item => item.uid);
    const difference = new Set([...urlInfoTransfer].filter(x => picListTransfer.findIndex(y => y === x) === -1));
    const delArr = [...difference];
    const params = {
      bc_id: id,
      type: uploadType,
      path: urlInfo.length > 0 ? JSON.stringify(pathArr) : JSON.stringify(path),
      del_ids: delArr.join(),
    };
    FetchUtils.post(Interfaces.businessClassUploadLeave, params, (data) => {
      setUploadVisible(false);
    }, (msg) => {
      message.error(msg);
    });
  }

  // 回显(更新fileList和form的值)
  componentDidMount () {
    const { id, uploadType, form } = this.props;
    const params = {
      bc_id: id,
      type: uploadType,
    };
    FetchUtils.get(Interfaces.businessClassLeaveList, params, (data) => {
      const urlInfoRes = [...data].map((item) => {
        return { uid: item.id, status: 'done', url: item.path, path: item.path };
      });
      form.setFieldsValue({ picList: urlInfoRes });
      this.setState({ urlInfo: urlInfoRes });
    }, (msg) => {
      message.error(msg);
    });
  }

  render () {
    const { form, uploadType, setUploadVisible } = this.props;
    const { getFieldDecorator } = form;
    return (
      <Modal
        title="添加"
        width={610}
        visible
        onCancel={() => setUploadVisible(false)}
        onOk={this.subPic}
        okText="提交"
      >
        <div className="clearfix">
          <Form>
            <FormItem label={uploadType === 1 ? '上传考勤表' : '上传请假表'}>
              {getFieldDecorator('picList', {
                initialValue: [],
                rules: [{
                  validator: this.validatorFileList,
                }],
              })(
                <OSSUpload
                  stsUrl={Interfaces.uploadToken}// 接口
                  accept=".jpg,.jpeg,.png"
                  stsUrlType={{ type: 'image' }}// 接口请求参数
                  limitedNum={2}
                  type="picture"// 组件类型
                  MAX_FILE_SIZE={5 * 1024 * 1024}
                  MAX_FILE_SIZE_TEXT="5M"
                  multiple
                />,
              )}
            </FormItem>
            <FormItem label="">
              <div style={{ color: '#999' }}>图片格式为:png、jpg、jpeg,最多上传50张图片,每张图片最大不超过5MB</div>
            </FormItem>
          </Form>
        </div>
      </Modal>
    );
  }
}
export default Form.create()(PicturesWall);

;