在react中手搓一个antd的Upload文件上传组件

在react中手搓一个antd的Upload文件上传组件

前言

  1. 下面所说的照片列表是模仿antd的Upload来进行实现的
  2. 本来是直接用antd的Upload的,但是我发现需要对里面的icon进行更改的时候,很困难,迫不得已,自己实现了一个
  3. 但是功能没有Upload那么强大,并且到最后也没有实现的是“Upload在增删照片之后,相邻照片框的移动是渐进的,就是那种移动过去的感觉”,也没有做到上传两张相同的照片

实现过程

input文件上传

  1. 最基本的,首先我们要有一个input用来上传照片
  2. type:指定为文件上传
  3. accept:指定接受的文件类型
  4. onChange:当上传的文件不一样时触发,参数是一个File类型的对象,也就是你上传的文件
<input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>

onChange事件触发

  1. 我们要接受上传的文件
  2. 因为我们要实现的是一个文件列表,所以我们需要一个数组去存放这些文件
  3. 再考虑到动态生成元素,所以我们选择用状态来存放这个数组
  4. 而代码中的图库文件类型,是根据后面的需要列出来的:name、用来识别另外一个图库中的照片是否相同、thumbUrl用来赋值给img展示出来、lastModified用来找出本图库中的自己
  5. 因为要将上传的照片显示出来,所以要有一个dataUrl用来给img的src进行赋值,然而onChange接受的参数是一个File类型的Blob,所以将Blob转换成dataUrl,这里的getBase64是antd给的,我只是顺手拿来用了
  6. 因为要动态生成列表,所以我们需要借助galleryFileList这个状态,状态改变监听的是引用,所以我们这里用的是深拷贝,将新的数组添加元素后再重新赋给状态,状态就监听到改变了
  7. 我们这里还有一个存入到本地的,是想要再打开但是记录还在的效果
// index.tsx
type galleryFile = { // 图库文件类型
    name: string,
    thumbUrl: string,
    lastModified: number
}

const [galleryFileList, setGalleryFileList] = useState<galleryFile[]>(local_galleryFileList) // 图库文件列表

const handleGalleryChange = (e: any) => { // 文件选择
    getBase64(e.target.files[0]).then((res)=>{
    const fileList = galleryFileList ? [...galleryFileList] : []; // 状态监听的是引用 所以想要监听数组的变化得深拷贝
    fileList.push({
      thumbUrl: res,
      name: e.target.files[0].name,
      lastModified: e.target.files[0].lastModified
    })
    setGalleryFileList(fileList)
    localStorage.setItem('AICase_gallery_fileList', JSON.stringify(fileList))
  })
}
// getBase64
const getBase64 = (file: RcFile): Promise<string> => // 获取base64
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
  });

样式实现

  1. 下面实现的是一个还没有文件上传时的上传框,那个uploadButton单独放出来是为了方便配置框里面的内容以及样式
  2. 主要的样式可以参考一下antd的Upload,就一个正正方方的虚线框,里面有一天“上传”的文字以及一个代表添加的“+”,鼠标放进去之后会有过渡的虚线框变量效果
  3. 值得注意的是,这个框并不是用input改变样式形成的,input的样式本身想要改变比较麻烦,我这里将input铺满整个框,并将opacity调成0,并放在最上面,这样的话我们既可以点到input,触发相应的事件,又可以弄个好看的样式
// HTML结构
<div className="upload_container">
    <div className="upload">
      <input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
      {uploadButton}
    </div>
  </div>
</div>
// uploadButton
const uploadButton = ( // 上传按钮
  <div className='uploadBtn'>
    <PlusOutlined />
    <div style={{ marginTop: 8 }}>上传</div>
  </div>
);
// css样式
.upload_container {
    width: 102px;
    height: 102px;
    margin-right: 8px;
    margin-bottom: 8px;
    box-sizing: border-box;

    .upload {
        position: relative;
        width: 102px;
        height: 102px;
        padding: 8px;
        border-radius: 8px;
        border: 1px dashed #d9d9d9;
        background-color: rgba(0, 0, 0, 0.02);
        box-sizing: border-box;
        cursor: pointer;
        transition: border-color 0.3s;

        input {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
            opacity: 0;
            z-index: 1;
        }

        .uploadBtn {
            position: absolute;
            top: 25%;
            left: 0;
            text-align: center;
            width: 100%;
            height: 100%;
            color: rgba(0, 0, 0, 0.88);
            font-size: 14px;
        }

        img {
            width: 100%;
            height: 100%;
            object-fit: contain;
        }

        &:hover {
            border: 1px dashed rgba(250, 173, 20); 
        }
    }
}

动态生成

  1. 下面这一段代码就是动态生成的
  2. 用react来进行动态生成是挺方便的,我们这里直接利用刚才的galleryFileList状态来进行生成新的照片框,而用来上传的照片框是固定不动的,这也为我们后面“不能连续上传两张相同的照片埋下了隐患”,因为那个文件input一直都在那里,只是我们看不见罢了,后面可以考虑清理一下input的value之类的,解决这个问题
// index.tsx
{galleryFileList ? galleryFileList.map((value: galleryFile, index: number) => {
  return (              
    <div className="upload_container" onClick={(e)=>handleGalleryClick(e)} key={`upload-container-${index}`}>
      <div className="upload upload_active">
        <img src={value.thumbUrl} alt="" width='102px' height='102px' className='uploadImg' data-name={value.name} data-lastmodified={value.lastModified} />
        <div className="mask">
          <CheckOutlined className='icon checkOutIcon' />
          <DeleteOutlined className='icon deleteIcon'/>
        </div>
      </div>
    </div>)
}) : ''}
<div className="upload_container">
    <div className="upload">
      <input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
      {uploadButton}
    </div>
</div>

源码

// dataChange.ts文件 用于将File转化为Base64

import type { RcFile } from 'antd/es/upload';

export const getBase64 = (file: RcFile): Promise<string> => // 获取base64
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
});
// index.tsx
import { PlusOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons'
import type { UploadFile } from 'antd/es/upload/interface';
import React,{ useState } from 'react'
import { getBase64 } from '../../utils/dataChange'
import './index.scss'

type galleryFile = { // 图库文件类型
    name: string,
    thumbUrl: string,
    lastModified: number
}

const uploadButton = ( // 上传按钮
  <div className='uploadBtn'>
    <PlusOutlined />
    <div style={{ marginTop: 8 }}>上传</div>
  </div>
);

const local_galleryFileList =  JSON.parse(localStorage.getItem('AICase_gallery_fileList') as string) // 本地图库

export default function UploadList(props:{presetFileList: UploadFile[], setSpotResult: any, setIsModalOpen: any}) { // 文件上传列表

    const {presetFileList, setSpotResult, setIsModalOpen} = props // 预设文件列表 人脸识别结果 modal开关设置

    const [galleryFileList, setGalleryFileList] = useState<galleryFile[]>(local_galleryFileList) // 图库文件列表

    const showModal = () => { // 信息展示
        setIsModalOpen(true);
      };

    const handleGalleryChange = (e: any) => { // 文件选择
        getBase64(e.target.files[0]).then((res)=>{
        const fileList = galleryFileList ? [...galleryFileList] : []; // 状态监听的是引用 所以想要监听数组的变化得深拷贝
        fileList.push({
          thumbUrl: res,
          name: e.target.files[0].name,
          lastModified: e.target.files[0].lastModified
        })
        setGalleryFileList(fileList)
        localStorage.setItem('AICase_gallery_fileList', JSON.stringify(fileList))
      })
    }
  
    const handleGalleryClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { // 图库点击处理
      const target = e.target as HTMLDivElement
      const currentTarget = e.currentTarget as HTMLDivElement
      const img = currentTarget.querySelector('img') as HTMLImageElement
  
      if(target.classList.value.includes("deleteIcon")){ // 删除
        const result = galleryFileList.filter((obj:galleryFile)  => obj.lastModified.toString() !== img.getAttribute('data-lastmodified'))
        setGalleryFileList(result)
      }
      else if(target.classList.value.includes("checkOutIcon")){ // 确认
        // 获取自定义属性值并比较
        const isExist = presetFileList.some((obj: UploadFile) => obj.name === img.getAttribute('data-name'))
        setSpotResult(isExist)
        showModal()
     } 
  }
  return (
    <div className="upload_list">
    {galleryFileList ? galleryFileList.map((value: galleryFile, index: number) => {
      return (              
        <div className="upload_container" onClick={(e)=>handleGalleryClick(e)} key={`upload-container-${index}`}>
          <div className="upload upload_active">
            <img src={value.thumbUrl} alt="" width='102px' height='102px' className='uploadImg' data-name={value.name} data-lastmodified={value.lastModified} />
            <div className="mask">
              <CheckOutlined className='icon checkOutIcon' />
              <DeleteOutlined className='icon deleteIcon'/>
            </div>
          </div>
        </div>)
    }) : ''}
    <div className="upload_container">
        <div className="upload">
          <input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
          {uploadButton}
        </div>
      </div>
    </div>
  )
}

// index.scss
.upload_list { // 文件列表
  display: flex;
  justify-content: start;
  align-content: flex-start;
  flex-wrap: wrap;
  width: 100%;

  .upload_container {
    width: 102px;
    height: 102px;
    margin-right: 8px;
    margin-bottom: 8px;
    box-sizing: border-box;

    .upload {
      position: relative;
      width: 102px;
      height: 102px;
      padding: 8px;
      border-radius: 8px;
      border: 1px dashed #d9d9d9;
      background-color: rgba(0, 0, 0, 0.02);
      box-sizing: border-box;
      cursor: pointer;
      transition: border-color 0.3s;

      input {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        cursor: pointer;
        opacity: 0;
        z-index: 1;
      }

      .uploadBtn {
        position: absolute;
        top: 25%;
        left: 0;
        text-align: center;
        width: 100%;
        height: 100%;
        color: rgba(0, 0, 0, 0.88);
        font-size: 14px;
      }

      img {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }

      &:hover {
        border: 1px dashed rgba(250, 173, 20); 
      }
    }

    .upload_active { // 上传后的文件列表
      border: 1px solid #d9d9d9;
      [fill=currentColor] { // antd内置的颜色修改
        color: rgba(255, 255, 255, 0.65);
      }

      .mask { // 遮罩层
        position: absolute;
        top: 8px;
        left: 8px;
        width: 84px;
        height: 84px;
        background-color: rgba(0,0,0,.45);
        text-align: center;
        line-height: 84px;
        opacity: 0;
        transition: opacity .3s;

        .icon { // 操作icon
          position: relative;
          transition: background-color color .3s;
          padding: 3px;
          [fill=currentColor]:hover {
            color: #fff;
          }
          &:hover {
            border-radius: 3px;
            background-color: rgba(0,0,0,.1);
              }

              &::after { // 这个伪类是用来确保点击事件的target指向该icon而不是icon里面的什么
              content: "";
              position: absolute;
              top: 0;
              left: 0;
              width: 22px;
              height: 22px;
              background-color: rgba(255, 255, 255, 0);
              }
              }

              .deleteIcon {
              margin-left: 8px;
              }

              &:hover {
              opacity: 1;
              }
              }

              &:hover {
              border: 1px solid #d9d9d9; 
              }
              }
              }
              }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值