React项目实现任意比例图片裁剪上传功能

1,505 阅读4分钟

背景

  • 使用框架:基于umi 和 antDesign UI 的开发框架
  • 使用插件:react-easy-crop
  • 实现功能:任意尺寸的图片裁剪功能(可放大缩小 移动图片位置)
  • 简单实现场景:点击图片上传logo 在本地选择一张图片 选择后弹出一个model弹窗,里面为待裁剪的图片,对图片进行拖拽放大缩小等操作后 点击确定按钮 将图片进行保存并实现上传功能

实现

项目安装react-easy-crop插件

npm install react-easy-crop --save

项目文件中引入该插件

import Cropper from 'react-easy-crop'

Upload上传组件和Modal弹窗组件

<div className="upload-wrapper">
  <Upload
    accept=".png,.jpg"
    listType="picture-card"
    showUploadList={false}
    beforeUpload={this.beforeUpload} 
    disabled={opType === 'detail' || disable === '1'}
  >
    {
      imgUrl
        ? opType === 'detail' || disable === '1'
          ? <ImageComp src={imgUrl} alt="avatar" style={{maxHeight: 100}}/>
          : <img src={imgUrl} alt="avatar" style={{ maxWidth: '100%', maxHeight: '100%' }} />
        : uploadButton
    }
  </Upload>
  <Modal
    onOk={this.saveImg}
    bodyStyle={bodyStyle}
    visible={modalVisible}
    width={cropperModalWidth}
    onCancel={this.onImgClose}
  >
    <Cropper
      crop={crop}   // 定位
      zoom={zoom}   // 变焦
      ref="cropper"
      image={srcCropper}  // 目标图片信息
      rotation={rotation}  // 旋转
      aspect={cropperAccept}  // 横竖比例
      onCropChange={this.onCropChange}  // 位置修改函数
      onZoomChange={this.onZoomChange}  // 变焦修改函数
      onCropComplete={this.onCropComplete}  // 修改完毕后触发的函数
      cropSize={{ width: width, height: height }}  // 图片需要裁剪的长度和宽度  长款可以自己随意写
    />
  </Modal>
</div>

点击上传 执行beforeUpload方法 实现图片的缓存的功能 为后面model弹窗提供可裁剪的图片信息

/**
 * 在弹窗进行弹出之前 操作电脑中的文件或图片 并进行缓存
 * @param file
 * @returns {boolean}
 */
beforeUpload = (file) => {
  this.setState({
    uploading: true
  }, () => {
    // 使用fileReader 操作目标文件/图片
    let reader = new FileReader()
    // 创建img
    const image = new Image()
    let height
    let width

    //因为读取文件需要时间,所以要在回调函数中使用读取的结果

    //开始读取文件
    reader.readAsDataURL(file)
    reader.onload = (e) => {
      // 实现图片预加载功能
      // 为图片设置属性
      image.src = reader.result
      image.onload = () => {
        height = image.naturalHeight
        width = image.naturalWidth

        // 保存图片信息数据 为modal图片剪切展示时候用
        this.setState({
          srcCropper: e.target.result,
          modalVisible: true
        })
      }
    }
  })

  return false
}

crop:图片的position定位
zoom:图片的变焦
通过onCropChange方法和onZoomChange方法修改图片位置和大小

onCropChange = (crop) => this.setState({ crop })
onZoomChange = (zoom) => this.setState({ zoom })

onCropComplete方法 实现图片裁剪完成后 处理图片信息的功能(需要用到一个相关处理方法getCroppedImg 后面代码贴出)获取处理后的图片数据信息

import getCroppedImg from '@/components/ImgComp/getCroppedImg'
onCropComplete = async (croppedArea, croppedAreaPixels) => {
  try {
    croppedImage = await getCroppedImg(
      this.state.srcCropper,
      croppedAreaPixels
    )
  } catch (e) {
    console.error(e)
  }
}

最后点击确定 将裁剪好的图片以formData的形式传递给后台

saveImg = async () => {
  const {
    urlType,
    photoType,
    imgSubType,
    handleAdditionUpload,
  } = this.props
  const formData = new FormData
  const postData = { ...getDispatchParam(null, this.token), resType: 'document' }

  _.each(postData, (value, key) => formData.append(key, value))

  formData.append('file', croppedImage)

  const res = await request.post(upImgURl, { data: formData })

  if (res && res.code === 0) {
    this.setState({
      modalVisible: false,
      uploading: false
    }, () => {
      message.success('图片上传成功')
      // 如果有在上传后有其他的事情 调用此方法
      handleAdditionUpload(photoType, imgSubType, urlType, res.oss)
    })
  }
}

这样将图片拖拽裁剪上传功能实现了

完整代码

ImgUpload文件

import React, { PureComponent } from 'react'

import {
  Modal,
  Upload,
  message
} from 'antd'

import {Image as ImageComp} from 'antd'

import {
  PlusOutlined,
  LoadingOutlined
} from '@ant-design/icons'

import './index.less'
import _ from 'lodash'
import Cropper from 'react-easy-crop'
import request from '@/utils/request'
import { upImgURl } from '@/config/upload'
import { getDispatchParam } from '@/utils'

import getCroppedImg from '@/components/ImgComp/getCroppedImg'

let croppedImage

class Index extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      logoFList: '',
      modalVisible: false,
      srcCropper: '',
      crop: { x: 0, y: 0 },
      zoom: 1,
      aspect: 4 / 3,
      imgUrl: '',
      uploading: false
    }
  }

  token = localStorage.getItem('token')

  onCropChange = (crop) => this.setState({ crop })
  onZoomChange = (zoom) => this.setState({ zoom })

  /**
   * 在弹窗进行弹出之前 操作电脑中的文件或图片 并进行缓存
   * @param file
   * @returns {boolean}
   */
  beforeUpload = (file) => {
    this.setState({
      uploading: true
    }, () => {
      // 使用fileReader 操作目标文件/图片
      let reader = new FileReader()
      // 创建img
      const image = new Image()
      let height
      let width

      //因为读取文件需要时间,所以要在回调函数中使用读取的结果

      //开始读取文件
      reader.readAsDataURL(file)
      reader.onload = (e) => {
        // 实现图片预加载功能
        // 为图片设置属性
        image.src = reader.result
        image.onload = () => {
          height = image.naturalHeight
          width = image.naturalWidth

          // 保存图片信息数据 为modal图片剪切展示时候用
          this.setState({
            srcCropper: e.target.result,
            modalVisible: true
          })
        }
      }
    })

    return false
  }

  onCropComplete = async (croppedArea, croppedAreaPixels) => {
    try {
      croppedImage = await getCroppedImg(
        this.state.srcCropper,
        croppedAreaPixels
      )
    } catch (e) {
      console.error(e)
    }
  }

  onImgClose = () => {
    this.setState({
      modalVisible: false,
      uploading: false
    })
  }

  saveImg = async () => {
    const {
      urlType,
      photoType,
      imgSubType,
      handleAdditionUpload,
    } = this.props
    const formData = new FormData
    const postData = { ...getDispatchParam(null, this.token), resType: 'document' }

    _.each(postData, (value, key) => formData.append(key, value))

    formData.append('file', croppedImage)

    const res = await request.post(upImgURl, { data: formData })

    if (res && res.code === 0) {
      this.setState({
        modalVisible: false,
        uploading: false
      }, () => {
        message.success('图片上传成功')
        // 如果有在上传后有其他的事情 调用此方法
        handleAdditionUpload(photoType, imgSubType, urlType, res.oss)
      })
    }
  }

  render() {
    const {
      crop,
      zoom,
      rotation,
      uploading,
      srcCropper,
      modalVisible
    } = this.state

    const {
      width,
      height,
      imgUrl,
      opType,
      accept,
      disable,
      uploadName,
      modalWidth,
    } = this.props

    let cropperAccept = accept ?? 4/3
    let cropperModalWidth = modalWidth ?? 800
    let cropperUploadName = uploadName ?? '上传'

    const uploadButton = (
      <div>
        { uploading
          ? <LoadingOutlined />
          : <><PlusOutlined /><div style={{ marginTop: 8 }}>{cropperUploadName}</div></>
        }
      </div>
    )

    const bodyStyle = { width: '100%', height: 600 }

    return (
      <div className="upload-wrapper">
        <Upload
          accept=".png,.jpg"
          listType="picture-card"
          showUploadList={false}
          beforeUpload={this.beforeUpload}
          disabled={opType === 'detail' || disable === '1'}
        >
          {
            imgUrl
              ? opType === 'detail' || disable === '1'
                ? <ImageComp src={imgUrl} alt="avatar" style={{maxHeight: 100}}/>
                : <img src={imgUrl} alt="avatar" style={{ maxWidth: '100%', maxHeight: '100%' }} />
              : uploadButton
          }
        </Upload>
        <Modal
          onOk={this.saveImg}
          bodyStyle={bodyStyle}
          visible={modalVisible}
          width={cropperModalWidth}
          onCancel={this.onImgClose}
        >
          <Cropper
            crop={crop}
            zoom={zoom}
            ref="cropper"
            image={srcCropper}
            rotation={rotation}
            aspect={cropperAccept}
            onCropChange={this.onCropChange}
            onZoomChange={this.onZoomChange}
            onCropComplete={this.onCropComplete}
            cropSize={{ width: width, height: height }}
          />
        </Modal>
      </div>
    )
  }
}

export default Index

getCroppedImg文件

const createImage = url =>
  new Promise((resolve, reject) => {
    const image = new Image()
    image.addEventListener('load', () => resolve(image))
    image.addEventListener('error', error => reject(error))
    image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox
    image.src = url
  })

function getRadianAngle(degreeValue) {
  return (degreeValue * Math.PI) / 180
}

/**
 * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
 * @param {File} image - Image File url
 * @param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop
 * @param {number} rotation - optional rotation parameter
 */
export default async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) {
  const image = await createImage(imageSrc)
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')

  const maxSize = Math.max(image.width, image.height)
  const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2))

  // set each dimensions to double largest dimension to allow for a safe area for the
  // image to rotate in without being clipped by canvas context
  canvas.width = safeArea
  canvas.height = safeArea

  // translate canvas context to a central location on image to allow rotating around the center.
  ctx.translate(safeArea / 2, safeArea / 2)
  ctx.rotate(getRadianAngle(rotation))
  ctx.translate(-safeArea / 2, -safeArea / 2)

  // draw rotated image and store data.
  ctx.drawImage(
    image,
    safeArea / 2 - image.width * 0.5,
    safeArea / 2 - image.height * 0.5
  )
  const data = ctx.getImageData(0, 0, safeArea, safeArea)

  // set canvas width to final desired crop size - this will clear existing context
  canvas.width = pixelCrop.width
  canvas.height = pixelCrop.height

  // paste generated rotate image with correct offsets for x,y crop values.
  ctx.putImageData(
    data,
    Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),
    Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)
  )

  // As Base64 string
  // return canvas.toDataURL('image/jpeg');

  // As a blob
  return new Promise(resolve => {
    canvas.toBlob(file => {
      resolve(file)
    }, 'image/jpeg')
  })
}

在此需要上传一个长宽为72px的图片

image.png

弹窗打开对图片进行裁剪处理

image.png 点击确定后上传图片

image.png