前端使用cropper.js封装react+ts组件实现图片上传裁剪保存功能

159 阅读2分钟

效果

image.png 去cropper.js官网查看下载使用

  1. npm 安装使用
  2. 添加上传按钮,图片显示,配置cropper参数,(有使用其他ui组件库可以使用自己的ui),使用原生或自定义不用隐藏input直接绑定onClick使用即可,不用使用下面ui button
  3. 使用
import React, { useEffect, useRef, useState } from 'react';
import Button from '@material-ui/core/Button';
import imageCompression from 'browser-image-compression';
import Cropper from 'cropperjs';

import 'cropperjs/dist/cropper.min.css';

interface IProps {

  onCrop?: (base64: string) => void;//返回裁剪后图片
  avatarImage?: string | undefined | null; //传入已有图片
  rotateBut?: boolean;//是否加载旋转
}

const ImageCropper: React.FC<IProps> = ({ onCrop, avatarImage, rotateBut = false }) => {
  const imageRef = useRef<HTMLImageElement>(null);
  const cropperRef = useRef<Cropper | null>(null);

  const [imageSrc, setImageSrc] = useState('');
  const [preImgUrl, setPreImgUrl] = useState('');

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = e => {
      setImageSrc(e.target?.result as string);
    };
    reader.readAsDataURL(file);
  };

  const debounce = (func: Function, wait: number) => {
    let timeout: NodeJS.Timeout | null = null;
    return function() {
      if (timeout !== null) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(() => {
        func();
      }, wait);
    };
  };
  
  useEffect(() => {
    if (avatarImage) {
      setImageSrc(avatarImage);
    }
  }, [avatarImage]);

  const onChangeCrop = () => {
    if (cropperRef?.current?.getCropBoxData()) {
      const croppedCanvas = cropperRef?.current?.getCroppedCanvas().toDataURL('image/png');
      croppedCanvas && setPreImgUrl(croppedCanvas);
    } else {
      console.error('请先选择裁剪区域!');
    }
  };

  const dd = debounce(onChangeCrop, 500);

  useEffect(() => {
    if (imageSrc && imageRef.current) {
      cropperRef.current = new Cropper(imageRef.current, {
        aspectRatio: 1,//裁剪框比例
        autoCropArea: 0.8,//裁剪框显示
        rotatable: true,//图片旋转
        dragMode: 'move',//图片移动
        // minCropBoxWidth: 100,//裁剪框宽
        // minCropBoxHeight: 100,
        movable: true,
        crop() {
          dd();
        },
      });
    }

    return () => {
      if (cropperRef.current) {
        cropperRef.current.destroy();
        cropperRef.current = null;
      }
    };
    
  }, [imageSrc]);

  const handleRotate = () => {
    if (cropperRef.current) {
      cropperRef.current.rotate(90);
    }
  };

  const handleCrop = async () => {
    if (cropperRef.current) {
      cropperRef.current.getCroppedCanvas().toBlob(async blob => {
        if (blob) {
          const options = {
            maxSizeMB: 0.2,
            maxWidthOrHeight: 1920,
            useWebWorker: true,
          };
          const compressedFile = await imageCompression(blob as File, options);
          const reader = new FileReader();
          reader.onloadend = () => {
            const base64data = reader.result as string;
            if (onCrop) {
              onCrop(base64data);
            }
          };
          reader.readAsDataURL(compressedFile);
        }
      }, 'image/png');
    }
  };

  return (
    <div>
      <input
        type="file"
        id="contained-button-file"
        onChange={handleFileChange}
        accept="image/*"
        style={{ display: 'none' }}
      />
      <div style={{ display: 'flex', flexWrap: 'wrap', width: '100%', justifyContent: 'center' }}>
        {imageSrc && (
          <div style={{ margin: '1rem', width: '180px', height: '180px', position: 'relative' }}>
            <img ref={imageRef} src={imageSrc} alt="Cropped Image" />
          </div>
        )}
        <div>
          {preImgUrl && (
            <img
              src={preImgUrl}
              alt="预览图像"
              style={{
                width: '80px',
                height: '80px',
                margin: '1rem',
                border: '1px solid #6666',
                borderRadius: '50%',
              }}
            />
          )}
          <br />
          <label htmlFor="contained-button-file">
            <Button
              variant="contained"
              size="small"
              style={{ margin: '0.5rem 1rem' }}
              color="primary"
              component="span"
            >
              上传图片
            </Button>
          </label>
          {preImgUrl && (
            <div>
              {rotateBut && (
                <Button
                  variant="contained"
                  size="small"
                  color="primary"
                  component="span"
                  style={{ margin: '0.5rem 1rem' }}
                  onClick={handleRotate}
                  disabled={!imageSrc}
                >
                  旋转图片
                </Button>
              )}

              <Button
                variant="contained"
                size="small"
                color="primary"
                component="span"
                onClick={handleCrop}
                style={{ margin: '0.5rem 1rem' }}
                disabled={!imageSrc}
              >
                保存图片
              </Button>
            </div>
          )}
        </div>
      </div>
      <div style={{ textAlign: 'center', fontSize: '0.8rem', color: '#999' }}> 上传logo</div>
    </div>
  );
};

export default ImageCropper;

使用

  const handleCrop = (blob: string) => {
      console.log(blob);//获取图片
  };
    <ImageCropper onCrop={handleCrop} avatarImage={img}/>