说明

是在react-image-crop的基础上封装的功能组件。可以获取剪裁后的图片的blob对象,方便日后拓展使用。

npm i react-image-crop --save

完整代码

import { blobToBlobURL, blobToDataURL } from '@/utils/data';
import { Modal } from 'antd';
import { t } from 'i18next';
import ReactCrop, {
  Crop,
  PixelCrop,
  centerCrop,
  makeAspectCrop,
} from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css';

const UploadAvatar: React.FC = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [imgFile, setImgFile] = useState<File>();
  const [imgSrc, setImgSrc] = useState<string>('');
  const [crop, setCrop] = useState<Crop>();
  const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
  const inputRef = useRef<HTMLInputElement>(null);
  const imgRef = useRef<HTMLImageElement>(null);

  const showModal = () => {
    setIsModalOpen(true);
  };

  const handleOk = () => {
    getCroppedImageBlob().then((blob) => {
      console.info('>>> blob: ', blob);
      console.info('>>> blobURL: ', blobToBlobURL(blob));
      setIsModalOpen(false);
    });
  };

  const handleCancel = () => {
    setIsModalOpen(false);
  };

  const handleBtnClick = () => {
    if (inputRef.current) {
      inputRef.current.click();
    }
  };

  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file) {
      console.info('>>> file: ', file);
      setImgFile(file);
      const reader = new FileReader();
      reader.onload = function (event) {
        const dataURL = event.target?.result;
        setImgSrc(dataURL as string);
        setCrop(undefined);
        showModal();
      };
      reader.readAsDataURL(file);
    }
  };

  function centerAspectCrop(
    mediaWidth: number,
    mediaHeight: number,
    aspect: number,
  ) {
    return centerCrop(
      makeAspectCrop(
        {
          unit: '%',
          width: 90,
        },
        aspect,
        mediaWidth,
        mediaHeight,
      ),
      mediaWidth,
      mediaHeight,
    );
  }

  function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
    const { width, height } = e.currentTarget;
    setCrop(centerAspectCrop(width, height, 1));
  }

  // 获取剪裁后的图片的 blob 对象
  async function getCroppedImageBlob() {
    const image = imgRef.current;
    if (!image || !completedCrop) {
      throw new Error('Image or crop data is missing');
    }

    // 计算裁剪后图片的尺寸
    const scaleX = image.naturalWidth / image.width;
    const scaleY = image.naturalHeight / image.height;
    const croppedWidth = completedCrop.width * scaleX;
    const croppedHeight = completedCrop.height * scaleY;

    // 创建 OffscreenCanvas 对象
    const offscreenCanvas = new OffscreenCanvas(croppedWidth, croppedHeight);
    const ctx = offscreenCanvas.getContext('2d');
    if (!ctx) {
      throw new Error('No 2D context available');
    }

    // 在 OffscreenCanvas 上绘制裁剪后的图片
    ctx.drawImage(
      image,
      completedCrop.x * scaleX,
      completedCrop.y * scaleY,
      croppedWidth,
      croppedHeight,
      0,
      0,
      croppedWidth,
      croppedHeight,
    );

    // 将 OffscreenCanvas 转换为 Blob 对象
    const blob = await offscreenCanvas.convertToBlob({ type: 'image/png' });

    return blob;
  }

  return (
    <div>
      <Modal
        title={t('upload-avatar')}
        open={isModalOpen}
        onOk={handleOk}
        onCancel={handleCancel}
        destroyOnClose
      >
        <div className="overflow-hidden text-center py-2">
          {imgSrc && (
            <ReactCrop
              crop={crop}
              aspect={1}
              minHeight={64}
              onComplete={(c) => setCompletedCrop(c)}
              onChange={(c) => setCrop(c)}
            >
              <img
                ref={imgRef}
                className="max-h-[360px]"
                src={imgSrc}
                onLoad={onImageLoad}
              />
            </ReactCrop>
          )}
        </div>
      </Modal>

      <Button onClick={handleBtnClick}>Upload</Button>

      {/* 隐藏的文件选择按钮 */}
      <input
        ref={inputRef}
        type="file"
        accept=".jpg, .jpeg, .png"
        style={{ display: 'none' }}
        // 在选择文件后触发的事件处理函数
        onChange={handleFileSelect}
        // 设置多选
        multiple={false}
      />
    </div>
  );
};

export default UploadAvatar;

A Student on the way to full stack of Web3.