图片裁切+预览

96 阅读3分钟
import { Button, InputNumber } from 'antd';
import { useEffect, useRef, useState } from 'react';
import Cropper from 'react-cropper';
import { UNVMessageBox, UNVModal } from 'UNV-DESIGN';
import styles from './index.less';

interface types {
  // 图片URL
  src?: string;
  // 图片文件对象(src、file二选一,同时传会使用file)
  file?: File;
  // 弹窗显示
  setVisible: (visible: boolean) => void;
  // 确认的回调
  onOK: (newFile: File) => void;
  // 默认尺寸
  defaultSize?: { width: number, height: number };
}

const CropperModal = (props: types) => {

  const { src, setVisible, file, onOK, defaultSize } = props;

  // 原图片url
  const [originalSrc, setOriginalSrc] = useState<any>(src || '');
  // 原图片名
  const [fileName, setFileName] = useState<string>('');
  // 原图片长宽
  const [originalSize, setOriginalSize] = useState<{ width: number, height: number }>({ width: 0, height: 0 });
  // 预览url
  const [viewSrc, setViewSrc] = useState<string>('');

  const cropperRef = useRef<any>(null);


  useEffect(() => {
    if (file) {
      setFileName(file.name);
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = (event) => {
        setOriginalSrc(event.target?.result);
      };
    }
  }, []);

  useEffect(() => {
    if (originalSrc) {
      const originalImg = new Image;
      originalImg.src = originalSrc;
      if (defaultSize && (originalImg.width < defaultSize.width || originalImg.height < defaultSize.height)) {
        UNVMessageBox.error(
          intl.formatMessage({ id: 'The image is too small. The image size must be greater than xx * xx' }, { size: `${defaultSize.width}*${defaultSize.height}` })
        );
        setVisible(false);
      }
      setOriginalSize({ width: originalImg.width, height: originalImg.height });
      if (defaultSize) {
        const originalImg = new Image;
        originalImg.src = originalSrc;
        const originalScaleX = originalImg.width / 500;
        const originalScaleY = originalImg.height / 400;
        const realScale = Math.max(originalScaleX, originalScaleY);
        if (realScale) {
          const imageElement = cropperRef?.current;
          const cropper = imageElement?.cropper;
          setTimeout(() => {
            cropper.cropBoxData.minWidth = defaultSize.width / realScale;
            cropper.cropBoxData.minHeight = defaultSize.height / realScale;
            // 模拟一次调整
            cropper.setData({ width: 0.8 * originalImg.width });
            _crop();
          }, 10);
        }
      }
    }
  }, [originalSrc]);

  /**
   * @description 确定生成裁剪图片
   */
  const _handleSaveImg = () => {
    const uint8 = _getUint8Arr(viewSrc);
    const newFile = new File([uint8.u8arr], fileName, { type: uint8.mime });
    onOK(newFile);
  };

  /**
 * 二进制容器
 * @param {String} dataurl 图片URL
 */
  const _getUint8Arr = (dataurl: string) => {
    // 截取base64的数据内容
    const arr: any = dataurl.split(',');
    const mime = arr[0].match(/:(.*?);/)[1];
    const bstr = atob(arr[1]);
    // 获取解码后的二进制数据的长度,用于后面创建二进制数据容器
    let n = bstr.length;
    // 创建一个Uint8Array类型的数组以存放二进制数据
    const u8arr = new Uint8Array(n);
    // 将二进制数据存入Uint8Array类型的数组中
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return { u8arr, mime };
  };

  /**
   * @description 刷新预览图像
   */
  const _crop = () => {
    const imageElement = cropperRef?.current;
    const cropper = imageElement?.cropper;
    const newURL = cropper.getCroppedCanvas({ ...defaultSize, imageSmoothingQuality: 'high' }).toDataURL('image/png');
    setViewSrc(newURL);
  };

  return (
    <UNVModal
      title={intl.formatMessage({ id: 'Crop Image' })}
      visible
      onCancel={() => setVisible(false)}
      width={800}
      className={styles.cropperModal_model_theModal}
      footer={[
        <Button key="ok" type="primary" onClick={() => _handleSaveImg()}>
          {intl.formatMessage({ id: 'OK' })}
        </Button>,
        <Button key="cancel" onClick={() => setVisible(false)}>{intl.formatMessage({ id: 'Cancel' })}</Button>
      ]}
      destroyOnClose
      canDragm
    >
      <div className={styles.cropperModal_model_content}>
        <Cropper
          src={originalSrc}
          ref={cropperRef}
          viewMode={1} // 定义cropper的视图模式
          zoomable={false} // 是否允许放大图像
          movable={false} // 是否允许移动图像
          guides={false} // 显示在裁剪框上方的虚线
          background={false} // 是否显示背景的马赛克
          rotatable={false} // 是否旋转
          className={styles.cropperModal_model_cropperArea}
          // autoCropArea={1} // 默认值0.8图片的80%)。--0-1之间的数值定义自动剪裁区域的大小
          aspectRatio={defaultSize ? defaultSize.width / defaultSize.height : NaN} // 固定为1:1  可以自己设置比例, 默认情况为自由比例
          // cropBoxResizable={false} // 默认true ,是否允许拖动 改变裁剪框大小
          // cropBoxMovable  // 是否可以拖拽裁剪框 默认true
          dragMode="move" // 拖动模式, 默认crop当鼠标 点击一处时根据这个点重新生成一个 裁剪框move可以拖动图片none:图片不能拖动
          cropend={_crop}
          center
        />
        <div className={styles.cropperModal_model_InfoArea}>
          <img src={viewSrc || originalSrc} />
          {defaultSize ? `${defaultSize.width}*${defaultSize.height}` : ''}
          {
            !defaultSize &&
            <div className={styles.cropperModal_model_sizeArea}>
              <div>
                {intl.formatMessage({ id: 'Width' })}:
                <InputNumber
                  value={viewSrc ? cropperRef?.current?.cropper.getData().width : originalSize.width}
                  onChange={(value) => cropperRef?.current?.cropper.setData({ width: value })}
                  min={0}
                  max={originalSize.width}
                />
              </div>
              <div>
                {intl.formatMessage({ id: 'Height' })}:
                <InputNumber
                  value={viewSrc ? cropperRef?.current?.cropper.getData().height : originalSize.height}
                  onChange={(value) => cropperRef?.current?.cropper.setData({ height: value })}
                  min={0}
                  max={originalSize.height}
                />
              </div>
            </div>
          }
        </div>
      </div>
    </UNVModal>
  );
};

export default CropperModal;

组件引用

 {showCropper &&
   <CropperModal
     setVisible={setShowCropper}
     file={picFile}
     defaultSize={{ width: 230, height: 150 }}
     onOK={goUpPic}
   />
  }

图片上传校验

 /**
  * 图片上传校验
  * @param param0 上传的图片文件
  * @returns
  */
  const _onChange = ({ file, fileList }: any) => {
    const isImg =
      file.type === 'image/jpeg' ||
      file.type === 'image/png' ||
      file.type === 'image/jpg';
    if (!isImg) {
      UNVMessageBox.warn(intl.formatMessage({ id: 'Only JPG, PNG, and JPEG images are allowed.' }));
      return false;
    } else {
      setPicFile(file);
      setShowCropper(true);
    }
  };

裁切后的图片上传

  /**
   * 上传图片
   * @param file 图片文件
   */
  const goUpPic = (file: File) => {
    const picFormData = new FormData();
    picFormData.append('file', file);
    dispatch({
      type: 'banner/uploadPic',
      payload: picFormData,
      callback: (data: any) => {
        setCoverImgs([...coverImgs, data]);
        setShowCropper(false);
      }
    });
  };