封装react upload上传组件,允许Form与upload联动(组件独立已解耦)

本文展示了如何使用Ant Design的Upload组件实现文件上传,并在上传完成后自动填充表单字段。同时,文章还介绍了如何处理多个上传路径的情况,以及如何实现在上传图片后进行预览和删除功能。此外,还详细讲解了如何添加排序功能,允许用户通过拖拽来调整已上传图片的顺序。代码中涉及到的功能包括文件上传前的重命名、状态检查、错误处理、文件名处理以及自定义预览和删除操作。
摘要由CSDN通过智能技术生成

原有问题

表单填写往往有某个表单项是文件上传,但是我们不得不在异步上传完毕之后拿着响应体给formData的某个属性塞文件路径,但是如果表单中存在若干个上传路径需要赛给不同的表单属性,这时候怎么办?难不成需要定义多个上传方法?

效果

在这里插入图片描述
在这里插入图片描述
支持大图查看删除操作,拿装泥照片举例,表单绑定的属性是moveImgA存的时图片所在服务器上的路径,那么首先在数据回显时,该字段有值则会自动回显,其次在完成上传后会自动填充表单的moveImgA属性为该图片的保存路径,最后点击删除图片操作,会自动将表单中的moveImgA属性置空

代码

js

import React, { useEffect, useState, useRef } from 'react';
import { Upload, message, Button, Image, Space } from 'antd';
import { useModel } from 'umi';
import { PlusOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons'
import ImgCrop from 'antd-img-crop';
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc';
import styles from './index.less';
import md5 from 'md5';

const GlobalUploadOss = ({
  maxCount = 1,
  onChange,
  onRemove,
  title,
  accept = 'image/*',
  value,
  crop,
  data: { type = 'default' },
  listType = 'picture-card',
  supportSort,
  ...props
}) => {
  const { initialState: { uploadAction, ossSuffix } } = useModel('@@initialState');
  
  const [previewSrc, setPreviewSrc] = useState()
  // const [ossSTSInfo, setOssSTSInfo] = useState();
  const [fileList, setFileList] = useState(() => {
    if (accept === '.apk') { //上传apk专用
      return []
    } else {
      return value?.split(',').map((item, index) => ({
        uid: 'img' + index,
        // name: item.split('/').at(-1), //google兼容,safari等其他浏览器不兼容
        name: item.split('/').slice(-1)?.[0],
        status: 'done',
        url: item,
        thumbUrl: accept.indexOf('video') != -1 ? item + ossSuffix : item
      })) || []
    }
  })
  // useEffect(async () => {
  //   const res = await getOSSData()
  //   setOssSTSInfo(res)
  // }, [])

  const beforeUpload = async (file, fileList) => {
    //上传前文件重命名
    // file.ossName = accept == '.apk' ? 'Android/' + type : `${type}/${randomString(10)}${getSuffix(file.name)}`
    return file
  }

  const handleUploadChange = ({ file, fileList }) => {
    console.log(file, fileList)
    if (file.status === 'done') {
      if (file.response.code == 200) {
        message.success(`${file.name} 上传成功`);
        file.url = file.response.data;
      } else {
        message.warn(file?.response?.msg)
      }
      // if (accept.indexOf('video') != -1) {
      //   file.thumbUrl = file.url + fil;
      // }
    } else if (file.status === 'error') {
      message.error(`${file.name} 上传失败`);
    }

    fileList = fileList.filter(item => item.status);
    if (accept === '.apk') { //上传apk专用
      onChange(fileList)
    } else {
      onChange(fileList.map(item => item.url).filter(r => r).join(',') || undefined)
    }
    setFileList(fileList)
  }

  let previewProps = {}
  if (accept?.indexOf('image') != -1) { //目前仅针对图片利用Image组件4.7.0的新特性实现自定义预览
    previewProps = {
      onPreview: file => setPreviewSrc(file.url)
    }
  }
  if (accept?.indexOf('video') != -1) { //自定义的视频预览
    previewProps = {
      onPreview: file => {
        setVideoSrc(file.url)
        setClassNameVisible(true)
      }
    }
  }

  // 处理视频预览
  const [videoSrc, setVideoSrc] = useState(false)
  const [classNameVisible, setClassNameVisible] = useState(false)
  useEffect(() => {
    if (classNameVisible == false) {
      setTimeout(() => {
        setVideoSrc('')
      }, 200)
    }
  }, [classNameVisible]);


  const uploadComponent = (
    <Upload
      name='fileList'
      {...props}
      listType={listType}
      maxCount={maxCount}
      accept={accept}
      headers={function () {
        const timestamp = new Date().getTime()
        const rand = (Math.random() * 100).toFixed(0)
        return {
          'api-version': 1,
          token: sessionStorage.token || '',
          apiSecret: md5(md5(timestamp + 'ccys' + rand)),
          timestamp,
          rand
        }
      }()}
      // data={file => {
      //   return ({
      //     fileList: file
      //   })
      // }}
      action={uploadAction}
      multiple={maxCount > 1}
      fileList={fileList}
      onChange={handleUploadChange}
      beforeUpload={beforeUpload}
      onRemove={file => onRemove && onRemove(file)}
      {...previewProps}
    >
      {
        fileList.length >= maxCount ? null :
          (
            ['text', 'picture'].includes(listType) ?
              <Button icon={<UploadOutlined />}>上传文件</Button> :
              <div>
                <PlusOutlined />
                <div className="ant-upload-text">上传{accept.indexOf('video') != -1 ? '视频' : '图片'}{maxCount ? `(${fileList.length}/${maxCount})` : ''}</div>
                {title ? <div>{title}</div> : null}
              </div>
          )
      }
    </Upload>
  )

  const SortableItem = SortableElement(({ item }) => <Image style={{ cursor: 'move' }} preview={false} width={50} height={50} src={item.url} />);
  const SortableList = SortableContainer(() => {
    return (
      <Space className={styles.SortContainer}>
        {fileList.map((item, index) => (
          <SortableItem key={`item-${index}`} disabled={false} index={index} item={item} />
        ))}
      </Space>
    );
  });
  const uploadWrapRef = useRef()
  const SortableComponent = () => {
    //.uploadImg_sort的样式写在主要是定义zIndex为1000+,因为antd的modal层级为1000.目的是为了解决拖拽时元素看不见的问题
    return (
      <SortableList
        helperContainer={() => uploadWrapRef.current}
        helperClass="uploadImg_sort"
        lockOffset={0}
        transitionDuration={500} //拖拽过度动画时长
        lockToContainerEdges={true}
        axis="xy"
        onSortEnd={({ oldIndex, newIndex }) => {
          console.log(arrayMove(fileList, oldIndex, newIndex))
          const sortFileList = arrayMove(fileList, oldIndex, newIndex)
          onChange(sortFileList.map(item => item.url).filter(r => r).join(','))
          setFileList(sortFileList)
        }}
      />
    )
  }

  return (
    <div ref={uploadWrapRef} style={{ overflow: 'hidden' }} className={styles.uploadWrap}>
      {
        !!crop ?
          <>
            <ImgCrop rotate grid>
              {uploadComponent}
            </ImgCrop>
            {supportSort && value?.length > 1 ? <SortableComponent /> : null}
          </>
          :
          <>
            {uploadComponent}
            {supportSort && value?.length > 1 ? <SortableComponent /> : null}
          </>
      }
      <Image
        width={0}
        height={0}
        src={previewSrc}
        preview={{
          visible: Boolean(previewSrc),
          onVisibleChange: (visible, prevVisible) => setPreviewSrc(undefined)
        }}
      />
      {
        videoSrc &&
        <div className={"ant-image-preview-mask " + (classNameVisible ? styles.videoPreviewMask : styles.videoPreviewMaskHide)} >
          <div className={"ant-image-preview-wrap " + (classNameVisible ? styles.videoPreviewWrap : styles.videoPreviewWrapHide)}>
            <ul className="ant-image-preview-operations">
              <li className="ant-image-preview-operations-operation">
                <CloseOutlined onClick={() => setClassNameVisible(false)} style={{ fontSize: 18, cursor: 'pointer' }} />
              </li>
            </ul>
            <div className="ant-image-preview-img-wrapper" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={() => setClassNameVisible(false)}>
              <video className="ant-image-preview-img" autoPlay controls poster={videoSrc + ossSuffix} src={videoSrc} onClick={e => e.stopPropagation()} />
            </div>
          </div>
        </div>
      }
    </div>
  )
}

export default GlobalUploadOss

less

.SortContainer {
  width: 100%;
  display: flex;
  box-sizing: border-box;
  padding: 5px;
  background-color: #e6f7ff;
}

.uploadWrap {
  :global {
    .uploadImg_sort {
      z-index: 1001;
    }

    .ant-image-img {
      object-fit: contain;
    }
  }
}


@duration: 200ms;

.videoPreviewMask {
  animation-name: fadeIn;
  animation-duration: @duration;
  animation-timing-function: linear;
}


@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.videoPreviewMaskHide {
  animation-name: fadeOut;
  animation-duration: @duration;
  animation-timing-function: linear;
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

.videoPreviewWrap {
  animation-name: scaleIn;
  animation-duration: @duration;
  animation-timing-function: ease-out;
}


@keyframes scaleIn {
  from {
    transform: scale(0);
  }

  to {
    transform: scale(1);
  }
}

.videoPreviewWrapHide {
  animation-name: scaleOut;
  animation-duration: @duration;
  animation-timing-function: ease-out;
}

@keyframes scaleOut {
  from {
    transform: scale(1);
  }

  to {
    transform: scale(0);
  }
}

引入

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

商朝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值