React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)

Posted 张志翔ۤ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)相关的知识,希望对你有一定的参考价值。

        最近工作中需要封装一个图片裁剪组件,也是封装了两天时间才完成,特此记录便于日后查阅。

        先看一下最终效果,图示如下:

 

        裁剪之后的图片可以预览,下载,删除,裁剪需要用到NodeJs的 jimp 组件,安装一下:

  $ npm install --save jimp

        组件代码,如下所示:

        1、UploadCutImage.jsx

import {message, Modal, Upload} from 'antd';
import React, {useEffect, useState} from 'react';
import {DeleteOutlined, DownloadOutlined, LoadingOutlined, UploadOutlined,} from '@ant-design/icons';
import {uploadFile} from './service';
import CutImg from "@/components/CutImg/CutImg";
import Jimp from 'jimp/es';

function beforeUpload(file) {
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
  if (!isJpgOrPng) {
    message.error('只能上传JPG/PNG文件!');
  }
  const isLt2M = file.size / 1024 / 1024 < 10;
  if (!isLt2M) {
    message.error('上传的文件不能超过10MB!');
  }
  return isJpgOrPng && isLt2M;
}

const UploadCutImage = (props) => {
  const {onChange, value, disable} = props; // 有默认传来的 chang事件,和 value值

  const [loading, setLoading] = useState(false);
  const [fileList, setFileList] = useState([]);
  const [imageUrl, setImageUrl] = useState();
  const [previewImage, setPreviewImage] = useState(null);
  const [previewVisible, setPreviewVisible] = useState(false);
  const [cutImgVisible, setCutImgVisible] = useState(false);
  const [originImgUrl, setOriginImgUrl] = useState();
  const [cutImgUrl, setCutImgUrl] = useState();

  // console.log(value);

  useEffect(() => {
    if (value !== null && value !== undefined && value.length > 0) {
      setImageUrl(value);
      setFileList([
        {
          uid: '-1',
          status: 'done',
          url: value,
        },
      ]);
    } else {
      setImageUrl(null);
      setFileList([]);
    }
  }, [value]);

  const uploadButton = (
    <div>
      {loading ? <LoadingOutlined/> : <UploadOutlined/>}
      <div style={{marginTop: 8}}>上传</div>
    </div>
  );

  const handleDownload = (info) => {
    window.location.href = imageUrl;
  };

  const handleRemove = (info) => {
    //setFileList([]);
    setOriginImgUrl(null);
    setCutImgUrl(null);
    setImageUrl(null);
    onChange('');
    return true;
  };
  
  const handlePreview = (info) => {
    setPreviewImage(imageUrl);
    setPreviewVisible(true);
  };

  const handleCancelPreview = () => {
    setPreviewVisible(false);
  };

  const handleChange = ({file, fileList}) => {
    if (file.status == 'removed') {
      setFileList([]);
    }
  };

  const doImgUpload = async (options) => {
    const {file} = options;
    const imgItem = {
      uid: '1', // 注意,这个uid一定不能少,否则上传失败
      name: file['name'],
      status: 'uploading',
      url: '',
      percent: 99, // 注意不要写100。100表示上传完成
    };

    setFileList([imgItem]);

    //原始图片上传
    let formData = new FormData();
    formData.append('file', file);

    await uploadFile(formData)
      .then(r => {
        // 弹出裁剪层,把url传过去
        const url = r['data']['url'];
        setOriginImgUrl(url);
        setCutImgVisible(true);
      })
      .catch((e) => {
        console.log('smyhvae 图片上传失败:' + JSON.stringify(e || ''));
        message.error('图片上传失败,请重试');
      });
  };

  return (
    <>
      <CutImg
        image={originImgUrl}
        visible={cutImgVisible}
        onOK={async (data) => {
          const wh = data['originSize']['width'] < data['originSize']['height'] ? data['originSize']['width'] : data['originSize']['height'];
          const x = -(data['position']['x'] * (data['originSize']['width'] / data['imgSize']['width'])), y = data['position']['y'], w = wh, h = wh;

          // 裁剪图片&上传
          await Jimp.read(data['img'],
            (err, image) => {
              if (err) throw err;
              //裁剪图片
              image.crop(x, y, w, h).getBase64Async(Jimp.MIME_PNG).then((base64Url) => {
                let bytes = window.atob(base64Url.split(',')[1]);
                let array = [];
                for(let i = 0; i < bytes.length; i++){
                  array.push(bytes.charCodeAt(i));
                }
                let blob = new Blob([new Uint8Array(array)], {type: 'image/png'});
                let formData = new FormData();
                const filename = Date.now() + '.png';
                formData.append('file', blob, filename);
                //上传图片
                uploadFile(formData)
                  .then(r => {
                    // 关闭裁剪层
                    const url = r['data']['url'];
                    setCutImgUrl(url);
                    setCutImgVisible(false);
                    const imgItem = {
                      uid: '1', // 注意,这个uid一定不能少,否则上传失败
                      name: filename,
                      status: 'done',
                      url: url, // url 是展示在页面上的绝对链接
                      imgUrl: url, // imgUrl 是存到 db 里的相对链接
                    };
                    onChange && onChange(url);
                    setImageUrl(url);
                    setFileList([imgItem]);
                  })
                  .catch((e) => {
                    console.log('图片上传失败:' + JSON.stringify(e || ''));
                    message.error('图片上传失败,请重试');
                  });
              });
            })
        }}
        onClose={() => {
          setFileList([]);
          setCutImgVisible(false);
        }}
      />

      <Upload
        accept="image/jpeg,image/png"
        multiple={false}
        name="file"
        fileList={fileList}
        listType="picture-card"
        showUploadList={{
          showDownloadIcon: true,
          downloadIcon: <DownloadOutlined style={{color: 'x00000'}}/>,
          showRemoveIcon: !disable,
          removeIcon: <DeleteOutlined/>,
        }}
        action="2"
        customRequest={doImgUpload}
        beforeUpload={beforeUpload}
        onChange={handleChange}
        onDownload={handleDownload}
        onRemove={handleRemove}
        onPreview={handlePreview}
        disabled={disable}
      >
        {imageUrl ? null : uploadButton}
      </Upload>
      <div>
        <Modal visible={previewVisible} footer={null} onCancel={handleCancelPreview}>
          <img alt="picture" style={{width: '100%', height: '100%'}} src={previewImage}/>
        </Modal>
      </div>
    </>
  );
};

export default UploadCutImage;

        上述代码用到了 <CutImg/> ,这个就是我们的核心实现类了。       

         2、CutEffect.jsx

import React, { Component } from "react";
import { Modal, Slider, Button } from "antd";
import Draggable from "react-simple-draggable";
import styles from "./CutEffect.less";

const Success = Modal.success;
const Error = Modal.error;

class CutEffect extends Component {
  constructor() {
    super();
    this.viewNode = React.createRef();
    this.state = {
      objStyles: { width: "auto", height: "auto" }, //首次渲染图片的显示样式
      controled: {}, //可拖拽的范围
      origin: { width: 0, height: 0 }, //第一次渲染图片后的图片的宽高
      computed: { computedWidth: 0, computedHeight: 0 }, //第一次渲染图片后的图片的宽高
      initPosition: { x: 0, y: 0 }, //初始拖拽的位置
      value: 0, //滑动条的值
      preRatio: 1, //前一次的放大缩小的  比例
      draggedPosition: {
        x: 0,
        y: 0
      }, //拖拽后的位置,仅传给后端
      title: "修改成功!" //弹窗title
    };
  }
  handleDrag(e) {
    // console.log(this.state);
    const { childNode } = this;
    childNode.style.top = e.y + "px";
    childNode.style.left = e.x + "px";
    this.setState({
      draggedPosition: e
    });
  }
  setImageData(ratio) {
    const {
      viewNode: { current }
    } = this;
    const { preRatio } = this.state;
    const { computedWidth, computedHeight } = this.state.computed;
    let width = computedWidth * ratio;
    let height = computedHeight * ratio;
    this.setState(
      () => {
        return {
          objStyles: { width, height }
        };
      },
      () => {
        let x, y;
        x = -((Math.abs(current.parentNode.offsetLeft) / preRatio) * ratio + 80 * (ratio - preRatio));
        y = -((Math.abs(current.parentNode.offsetTop) / preRatio) * ratio + 80 * (ratio - preRatio));
        if (x > 0) {
          x = 0;
        }
        if (y > 0) {
          y = 0;
        }
        if (preRatio > ratio) {
          //特殊位置处理  left
          if (current.offsetWidth - Math.abs(current.parentNode.offsetLeft) < 250) {
            x = -(current.offsetWidth - 250);
          }
          //特殊位置处理 top
          if (current.offsetHeight - Math.abs(current.parentNode.offsetTop) < 250) {
            y = -(current.offsetHeight - 250);
          }
        }
        let initPosition = {
          x: x,
          y: y
        };
        this.setState(() => {
          return {
            controled: {
              top: -(height - 250),
              left: -(width - 250),
              bottom: 0,
              right: 0
            },
            initPosition,
            draggedPosition:initPosition,
            preRatio: ratio
          };
        });
      }
    );
  }
  handleChange(val) {
    const ratio = 1 + val;
    this.setState(() => {
      return {
        value: val
      };
    });
    this.setImageData(ratio);
  }
  judgeProperty(target) {
    //原始图片的尺寸
    const imgObj = {
      width: target.naturalWidth,
      height: target.naturalHeight
    };
    //设置原始图片尺寸
    this.setState({
      origin: {width: target.naturalWidth, height: target.naturalHeight}
    });
    if (imgObj.width > imgObj.height) {
      this.setState(() => {
        return {
          objStyles: { width: "auto", height: "100%" },
          controled: {
            top: 0,
            left: -((250 / imgObj.height) * imgObj.width - 250),
            bottom: 0,
            right: 0
          }
        };
      });
    } else if (imgObj.width < imgObj.height) {
      this.setState(() => {
        return {
          objStyles: { width: "100%", height: "auto" },
          controled: {
            top: -((250 / imgObj.width) * imgObj.height - 250),
            left: 0,
            bottom: 0,
            right: 0
          }
        };
      });
    } else if (imgObj.width == imgObj.height) {
      this.setState(() => ({
        objStyles: { width: "100%", height: "auto" },
        controled: { top: 0, left: 0, bottom: 0, right: 0 }
      }));
    }
  }
  getComputedImgProprety(target) {
    const { width, height } = window.getComputedStyle(target);
    this.setState(() => ({
      computed: {
        computedWidth: parseFloat(width).toFixed(2),
        computedHeight: parseFloat(height).toFixed(2)
      }
    }));
  }
  async postData() {
    const { draggedPosition, objStyles, computed, origin } = this.state;
    const { onOK } = this.props;

    let data = {
      position: draggedPosition,
      originSize: {
        width: origin['width'],
        height: origin['height'],
      },
      imgSize: {
        width: objStyles.width == "100%" || objStyles.width == "auto" ? parseInt(computed.computedWidth) : objStyles.width,
        height: objStyles.height == "100%" || objStyles.height == "auto" ? parseInt(computed.computedHeight) : objStyles.height
      },
      img: this.props.imgUrl
    };
    onOK && onOK(data)
    this.props.closePost();
  }
  componentDidMount() {
    setTimeout(() => {
      this.initImage();
    }, 0);
  }
  initImage() {
    const { visible, imgUrl } = this.props;
    const { childNode } = this;
    childNode.onload = () => {
      this.judgeProperty(childNode);
      this.getComputedImgProprety(childNode);
    };
  }
  reInitImage(target) {
    if (target.isUpdate) {
      this.setState(
        {
          objStyles: { width: "auto", height: "auto" },
          controled: {},
          initPosition: { x: 0, y: 0 },
          value: 0,
          computed: { computedWidth: 0, computedHeight: 0 },
          preRatio: 1,
          draggedPosition: {
            x: 0,
            y: 0
          }
        },
        () => {
          target.closeUpdate();
        }
      );
    } else {
      const { childNode } = this;
      Promise.resolve().then(() => {
        this.judgeProperty.call(this, childNode);
        this.getComputedImgProprety.call(this, childNode);
      });
    }
  }
  componentWillReceiveProps(pre, next) {
    if (pre.isPost) {
      this.postData();
      return;
    }
    //初始化状态
    if (pre.isCloseToUpdate && pre.isPost == this.props.isPost) {
      this.reInitImage(pre);
    }
  }
  successRender() {
    const { title } = this.state;
    const modal = Success({
      title,
      centered: true,
      key: 1,
      okText: "确定",
      onOk: () => {
        this.props.closeCutting();
        modal.destroy();
      }
    });
    setTimeout(() => {
      modal.destroy();
      this.props.closeCutting();
    }, 3000);
  }
  errorRender() {
    const modal = Error({
      title: "未知错误!请稍后重试!",
      centered: true,
      key: 2,
      okText: "确定",
      onOk: () => {
        this.props.closeCutting();
        modal.destroy();
      }
    });
    setTimeout(() => {
      modal.destroy();
      this.props.closeCutting();
    }, 3000);
  }
  render() {
    // console.log(this.props);
    const { objStyles, controled, initPosition, value ,draggedPosition} = this.state;
    const { handleDrag } = this;
    const { imgUrl } = this.props;
    return (
      <div className={styles.model_wrap_sin}>
        <div className={styles.model_wrap_cox}>
          <div className={styles.model_wrap_cox_cen}>
            <div className={styles.model_wrap_cox_ast}>
              <div className={styles.model_wrap_liz}>
                <div className={styles.model_wrap_mas} />
                <div className={styles.model_wrap_lig}>
                  <Draggable OnDragging={handleDrag.bind(this)} controled={controled} initPosition={initPosition}>
                    <img
                      src={imgUrl}
                      ref={this.viewNode}
                      onDragStart={e => e.preventDefault()}
                      style={{
                        width: objStyles.width,
                        height: objStyles.height
                      }}
                    />
                  </Draggable>
                </div>
                <div className={styles.model_wrap_ligt}>
                  <img
                    src={imgUrl}
                    ref={ref => (this.childNode = ref)}
                    style={{
                      position: "relative",
                      top: draggedPosition.y,
                      left: draggedPosition.x,
                      width: objStyles.width,
                      height: objStyles.height
                    }}
                    data-ccc={initPosition.x}
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
        {/*<Slider*/}
        {/*  value={value}*/}
        {/*  tipFormatter={null}*/}
        {/*  disabled={false}*/}
        {/*  onChange={this.handleChange.bind(this)}*/}
        {/*  max={1}*/}
        {/*  min={0}*/}
        {/*  step={0.1}*/}
        {/*  style={{ width: "250px", margin: "15px auto" }}*/}
        {/*/>*/}
      </div>
    );
  }
}

export default CutEffect;

         3、CutEffect.less

.model_wrap_sin {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    .model_wrap_cox {
        width: 100%;
        max-width: 500px;
        height: 300px;
        .model_wrap_cox_cen {
            width: 300px;
            height: 300px;
            margin: 0 auto;
            background-color: rgba(255, 255, 255, 0.6);
            .model_wrap_cox_ast {
                background-color: #f7f8fa;
                display: flex;
                align-items: center;
                width: 100%;
                height: 100%;
                overflow: hidden;
                position: relative;
                .model_wrap_liz {
                    width: 250px;
                    height: 250px;
                    margin: 0 auto;
                    overflow: visible;
                    .model_wrap_mas {
                        width: 300px;
                        height: 300px;
                        background-color: rgba(255, 255, 255, 0.6);
                        position: absolute;
                        top: 0;
                        left: 0;
                        z-index: 2;
                    }
                    .model_wrap_lig {
                        display: flex;
                        width: 250px;
                        height: 250px;
                        position: absolute;
                        overflow: hidden;
                        z-index: 3;
                    }
                    .model_wrap_ligt {
                        width: 250px;
                        height: 250px;
                        z-index: 1;
                        position: relative;
                        top: 0;
                        left: 0;
                        overflow: visible;
                        img {
                            margin: 0;
                            padding: 0;
                        }
                    }
                }
            }
        }
    }
}

        4、CutImg.jsx

import React, { Component } from "react";
import { Col, Modal, Button, Slider } from "antd";
import CutEffect from "./CutEffect";
import styles from "./CutImg.less";

class CutImg extends React.Component {
  constructor(props) {
    super(props);
    this.handleOk = this.handleOk.bind(this);
    this.state = {
      isPost: false, //是否允许子组件提交表单
      isUpdate: false //是否初始化裁剪组件状态
    };
  }
  handleCancel() {
    this.props.onClose();
    this.openUpdate();
  }
  handleOk() {
    this.setState(() => ({
      isPost: true
    }));
  }
  closePost() {
    this.setState(() => ({
      isPost: false
    }));
  }
  openUpdate() {
    this.setState(() => ({
      isUpdate: true
    }));
  }
  closeUpdate() {
    this.setState(() => ({
      isUpdate: false
    }));
  }
  render() {
    const { image, visible, isCloseToUpdate, onOK } = this.props;
    const { isPost, isUpdate } = this.state;
    return (
      <Col xs={24} md={12} className={styles.model_wrap} style={{ ...this.props.style }}>
        <Modal
          title='拖动图片调整显示区域'
          wrapClassName={styles.model_wrap_modal}
          visible={visible}
          onOk={this.handleOk}
          width={500}
          onCancel={this.handleCancel.bind(this)}
          footer={[
            <Button key='back' onClick={this.handleCancel.bind(this)}>
              取消
            </Button>,
            <Button key='submit' type='primary' onClick={this.handleOk}>
              确定
            </Button>
          ]}
        >
          <CutEffect
            imgUrl={image}
            visible={visible}
            isPost={isPost}
            isUpdate={isUpdate}
            isCloseToUpdate={isCloseToUpdate}
            closePost={this.closePost.bind(this)}
            closeUpdate={this.closeUpdate.bind(this)}
            openUpdate={this.openUpdate.bind(this)}
            closeCutting={this.handleCancel.bind(this)}
            onOK={onOK}
          />
        </Modal>
      </Col>
    );
  }
}
export default CutImg;

        5、CutImg.less

.model_wrap {
    width: 100%;
    max-width: 500px;
    margin: 0 auto;
}
.model_wrap_modal {
    width: 100%;
    max-width: 500px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-top: -100px;
    // transform: translate(0, -50%);
}

        6、service.js

import request from 'umi-request';

export async function uploadFile(formData) {
  return await request("/v1.0/sys/admin/files/upload", {
    method: 'post',
    headers: {},
    requestType: 'form',
    data: formData
  })
}

        我是把 CutImg 放到了全局组件的文件夹下面,你们也可以这么干,图示如下:

       6、调用的代码

        用的时候直接通过下面这种方式引入,就可以了,代码如下:

  <UploadCutImage disable={false}/>

        到此 React 实现图片裁剪介绍完成。

以上是关于React 实现图片裁剪/上传/预览/下载/删除(主要是图片裁剪)的主要内容,如果未能解决你的问题,请参考以下文章

Java实现图片裁剪预览功能

HTML5 MUI 手机预览图片,裁剪上传base64,保存数据库

Java实现图片裁剪预览功能

jQuery插件ImgAreaSelect 实例讲解一(头像上传预览和裁剪功能)

vue elementUi中uolad文件上传和v-viewer相结合实现图片的预览下载和删除功能

React+Antd+Antd-Img-Crop实现上传固定大小的裁剪头像或者图片(且可控制图片数量)