You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React JS上传图片前后如何按固定比例裁剪?react-easy-crop实现求助

React 头像裁剪上传实现方案(基于rsuite Uploader + react-easy-crop)

前置准备

先安装裁剪库依赖:

npm install react-easy-crop
# 或
yarn add react-easy-crop

核心实现逻辑

调整原有上传逻辑:选中图片后不直接上传,先弹出裁剪框按固定比例裁剪,确认后再上传裁剪后的图片。需要补充裁剪区域坐标计算、裁剪后图片生成两个核心逻辑。

完整修改后代码

import React from "react";
import { Uploader, Icon, Alert, Loader, ControlLabel, Modal, Button } from "rsuite";
import Cropper from "react-easy-crop";
import localStorage from "../../utils/lib/local-storage";
const dataUser = localStorage().get("auth");

// 工具函数:将file转base64
const previewFile = (file, callback) => {
    const reader = new FileReader();
    reader.onloadend = () => {
        callback(reader.result);
    };
    reader.readAsDataURL(file);
};

// 工具函数:根据裁剪区域生成裁剪后的blob文件
const getCroppedImg = (imageSrc, pixelCrop) => {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.src = imageSrc;
    image.onload = () => {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");
      canvas.width = pixelCrop.width;
      canvas.height = pixelCrop.height;
      ctx.drawImage(
        image,
        pixelCrop.x,
        pixelCrop.y,
        pixelCrop.width,
        pixelCrop.height,
        0,
        0,
        pixelCrop.width,
        pixelCrop.height
      );
      canvas.toBlob((blob) => {
        if (blob) {
          resolve(blob);
        } else {
          reject(new Error("裁剪图片生成失败"));
        }
      }, "image/jpeg");
    };
    image.onerror = reject;
  });
};

const Avatar = ({
    image = false,
    styles = {
        width: 120,
        height: 120,
        margin: 0,
        borderRadius: "50%",
        border: "1px solid #ddd"
    },
    label = false,
    urlUpload = "/users/set-avatar"
}) => {
    const [uploading, setUploading] = React.useState(false);
    const [fileInfo, setFileInfo] = React.useState(image);
    // 裁剪相关状态
    const [cropModalVisible, setCropModalVisible] = React.useState(false);
    const [originalImage, setOriginalImage] = React.useState("");
    const [crop, setCrop] = React.useState({ x: 0, y: 0 });
    const [zoom, setZoom] = React.useState(1);
    const [croppedAreaPixels, setCroppedAreaPixels] = React.useState(null);

    // 确认裁剪并上传
    const handleCropConfirm = async () => {
      try {
        setUploading(true);
        setCropModalVisible(false);
        // 生成裁剪后的blob
        const croppedBlob = await getCroppedImg(originalImage, croppedAreaPixels);
        // 构造FormData手动上传
        const formData = new FormData();
        formData.append("images", croppedBlob, "avatar.jpg");
        // 调用上传接口
        const res = await fetch(`${process.env.REACT_APP_BASE_URL}${urlUpload}`, {
          method: "POST",
          headers: {
            Authorization: "Bearer " + dataUser.token
          },
          body: formData
        });
        if (res.ok) {
          // 上传成功更新预览
          previewFile(croppedBlob, value => {
            setFileInfo(value);
          });
          Alert.success("上传成功");
          window.location.reload();
        } else {
          throw new Error("上传失败");
        }
      } catch (e) {
        setFileInfo(null);
        Alert.error("操作失败,请重试");
      } finally {
        setUploading(false);
      }
    };

    return (
        <div style={{ marginBottom: label ? 15 : 0 }}>
            {label && <ControlLabel>{label}</ControlLabel>}
            <Uploader
                fileListVisible={false}
                listType="picture"
                autoUpload={false} // 关闭自动上传,走自定义裁剪逻辑
                onChange={(file) => {
                    // 选中文件后先转base64打开裁剪框
                    previewFile(file.blobFile, value => {
                        setOriginalImage(value);
                        setCropModalVisible(true);
                    });
                }}
                name="images"
                headers={{
                    Authorization: "Bearer " + dataUser.token
                }}
            >
                <button type="button" style={styles}>
                    {uploading && <Loader backdrop center />}
                    {fileInfo ? (
                        <img
                            src={fileInfo}
                            alt="avatar"
                            width="100%"
                            height="100%"
                            style={{ borderRadius: "50%" }}
                        />
                    ) : (
                        <Icon icon="avatar" size="5x" />
                    )}
                </button>
            </Uploader>

            {/* 裁剪弹窗 */}
            <Modal open={cropModalVisible} onClose={() => setCropModalVisible(false)}>
              <Modal.Header>
                <Modal.Title>裁剪头像</Modal.Title>
              </Modal.Header>
              <Modal.Body>
                <div style={{ position: "relative", width: "100%", height: 400 }}>
                  <Cropper
                    image={originalImage}
                    crop={crop}
                    zoom={zoom}
                    aspect={1} // 固定1:1裁剪比例,可根据需求调整为其他比例,例如4/3
                    onCropChange={setCrop}
                    onZoomChange={setZoom}
                    onCropComplete={(_, pixelCrop) => setCroppedAreaPixels(pixelCrop)}
                  />
                </div>
                <div style={{ marginTop: 20 }}>
                  <p>缩放比例: {zoom.toFixed(1)}x</p>
                  <input
                    type="range"
                    min={1}
                    max={3}
                    step={0.1}
                    value={zoom}
                    onChange={(e) => setZoom(Number(e.target.value))}
                    style={{ width: "100%" }}
                  />
                </div>
              </Modal.Body>
              <Modal.Footer>
                <Button onClick={() => setCropModalVisible(false)}>取消</Button>
                <Button onClick={handleCropConfirm} appearance="primary">确认裁剪并上传</Button>
              </Modal.Footer>
            </Modal>
        </div>
    );
};
export default Avatar;

调整说明

  • 代码默认设置1:1的裁剪比例适配头像场景,可修改Cropper组件的aspect参数自定义宽高比
  • 保留了原有组件的所有入参和交互逻辑,原有业务代码无需修改即可直接接入
  • 手动处理上传逻辑避免了rsuite默认上传未裁剪原始图的问题

内容的提问来源于stack exchange,提问作者NicoGulo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 09:27:04