【React】前端处理图片的上传、预览、移动、缩放和旋转

概要

一个老项目维护新功能,提出了对图片的各种处理,由于某些原因不能使用Antd的Image组件(但是实际上使用了Antd其他组件,客户总会提出奇怪的要求),于是自己实现了一个常见的处理图片功能。

本文将介绍如何实现一个完整的图片上传预览系统,包括上传、预览、移动、缩放和旋转等功能。
我们实现的功能包括:

  • 图片上传和预览
  • 鼠标拖动移动图片位置
  • 滚轮缩放图片大小
  • 按钮控制图片旋转
  • 滑块控制精确缩放比例。

完整代码跑出来的样子,可以根据自己的需求修改样式和自定义展示逻辑
在这里插入图片描述

核心代码

图片上传组件

使用Ant Design的Upload组件实现图片上传功能:

<Upload
  name="avatar"
  listType="picture-card"
  className="avatar-uploader"
  showUploadList={false}
  onChange={handleChange}
  beforeUpload={(file) => {
    const isJpgOrPng =
      file.type === "image/jpeg" || file.type === "image/png";
    if (!isJpgOrPng) {
      message.error("只能上传 JPG/PNG 文件!");
    }
    return false; // 返回false阻止自动上传
  }}
  accept="image/jpeg,image/png"
>
  {imgUrl ? (
    <img
      src={imgUrl}
      alt="avatar"
      style={{
        width: "auto",
        height: "auto",
        maxWidth: "100%",
        maxHeight: 300,
        objectFit: "contain",
      }}
    />
  ) : (
    <div>
      <PlusOutlined />
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  )}
</Upload>

图片移动功能

const handleMouseDown = (e: React.MouseEvent) => {
  setIsDragging(true);
  setDragStartPos({
    x: e.clientX - moveConfig.moveX,
    y: e.clientY - moveConfig.moveY,
  });
};

const handleMouseMove = (e: React.MouseEvent) => {
  if (!isDragging) return;
  setMoveConfig({
    moveX: e.clientX - dragStartPos.x,
    moveY: e.clientY - dragStartPos.y,
  });
};

const handleMouseUp = () => {
  setIsDragging(false);
};

图片缩放功能

useEffect(() => {
  if (imgDivRef.current) {
    const handleWheel = (e: WheelEvent) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      const delta = e.deltaY > 0 ? -0.1 : 0.1;
      setScale((prev) => Math.max(0.1, Math.min(3, prev + delta)));
    };

    imgDivRef.current.addEventListener("wheel", handleWheel, {
      passive: false,
    });

    return () => {
      imgDivRef.current?.removeEventListener("wheel", handleWheel);
    };
  }
}, [imgUrl]);

图片旋转功能

const handleRotate = (angle: number) => {
  setRotate((prevRotate) => prevRotate + angle);
};

// 在渲染中使用
<Button icon={<RotateLeftOutlined />} onClick={() => handleRotate(-90)} />
<Button icon={<RotateRightOutlined />} onClick={() => handleRotate(90)} />

技术要点分析

  1. 事件处理优化 :
    • 使用 passive: false 确保能阻止默认滚动行为
    • 通过 stopImmediatePropagation 阻止事件冒泡
    • 在useEffect中正确添加和移除事件监听器
  2. 性能考虑 :
    • 使用transform代替直接修改位置属性,利用GPU加速
    • 限制缩放范围(0.1-3倍)防止极端情况
    • 用户体验 :
  3. 用户体验 :
    • 拖动时显示grabbing光标提升交互感
    • 限制文件类型为JPG/PNG
    • 提供多种控制方式(按钮+滑块)

完整代码

import React, { useState, useRef, useEffect } from "react";
import { Upload, Button, Slider, message, Space } from "antd";
import {
  PlusOutlined,
  RotateLeftOutlined,
  RotateRightOutlined,
  ZoomInOutlined,
  ZoomOutOutlined,
} from "@ant-design/icons";
import type { RcFile, UploadProps } from "antd/es/upload";

interface MoveConfig {
  moveX: number;
  moveY: number;
}

const PictureUpload: React.FC = () => {
  const [imgUrl, setImgUrl] = useState<string>("");
  const [scale, setScale] = useState<number>(1);
  const [rotate, setRotate] = useState<number>(0);
  const [moveConfig, setMoveConfig] = useState<MoveConfig>({
    moveX: 0,
    moveY: 0,
  });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 });
  const imgRef = useRef<HTMLImageElement>(null);
  const imgDivRef = useRef<HTMLDivElement>(null);

  const getBase64 = (img: RcFile, callback: (url: string) => void) => {
    const reader = new FileReader();
    reader.addEventListener("load", () => callback(reader.result as string));
    reader.readAsDataURL(img);
  };

  const handleChange: UploadProps["onChange"] = (info) => {
    const file = info.file as RcFile;
    console.log("Uploaded file:", info, file); // 打印上传的文件对象
    if (file) {
      getBase64(file, (url) => {
        setImgUrl(url);
      });
    }
  };

  const handleMouseDown = (e: React.MouseEvent) => {
    setIsDragging(true);
    setDragStartPos({
      x: e.clientX - moveConfig.moveX,
      y: e.clientY - moveConfig.moveY,
    });
  };

  const handleMouseMove = (e: React.MouseEvent) => {
    if (!isDragging) return;

    setMoveConfig({
      moveX: e.clientX - dragStartPos.x,
      moveY: e.clientY - dragStartPos.y,
    });
  };

  const handleMouseUp = () => {
    setIsDragging(false);
  };

  const handleRotate = (angle: number) => {
    setRotate((prevRotate) => prevRotate + angle);
  };

  const handleScale = (value: number | Function) => {
    // 如果直接传入值(来自Slider)
    if (typeof value === "number") {
      setScale(Math.max(0.1, Math.min(3, value))); // 限制在0.1-3范围内
    }
    // 如果来自按钮点击(增量)
    else if (typeof value === "function") {
      setScale((prev) => {
        const newScale = value(prev);
        return Math.max(0.1, Math.min(3, newScale)); // 限制在0.1-3范围内
      });
    }
  };

  useEffect(() => {
    if (imgDivRef.current) {
      const handleWheel = (e: WheelEvent) => {
        console.log("wheel event:", e);
        e.preventDefault();
        e.stopImmediatePropagation();
        const delta = e.deltaY > 0 ? -0.1 : 0.1;
        setScale((prev) => Math.max(0.1, Math.min(3, prev + delta)));
        return false;
      };

      // 添加非passive事件监听
      imgDivRef.current.addEventListener("wheel", handleWheel, {
        passive: false,
      });

      return () => {
        imgDivRef.current.removeEventListener("wheel", handleWheel);
      };
    }
  }, [imgUrl]); // 依赖imgUrl,确保每次imgUrl变化时重新添加事件

  // 修改Upload组件配置
  return (
    <div style={{ padding: 24 }}>
      <Upload
        name="avatar"
        listType="picture-card"
        className="avatar-uploader"
        showUploadList={false}
        onChange={handleChange}
        beforeUpload={(file) => {
          const isJpgOrPng =
            file.type === "image/jpeg" || file.type === "image/png";
          if (!isJpgOrPng) {
            message.error("只能上传 JPG/PNG 文件!");
          }
          return false; // 返回false阻止自动上传
        }}
        accept="image/jpeg,image/png"
      >
        {imgUrl ? (
          <img
            src={imgUrl}
            alt="avatar"
            style={{
              width: "auto",
              height: "auto",
              maxWidth: "100%",
              maxHeight: 300,
              objectFit: "contain",
            }}
          />
        ) : (
          <div>
            <PlusOutlined />
            <div style={{ marginTop: 8 }}>Upload</div>
          </div>
        )}
      </Upload>

      {imgUrl && (
        <div>
          {/* 图片编辑区域 */}
          <div
            ref={imgDivRef}
            style={{
              position: "relative",
              overflow: "hidden",
              width: "100%",
              height: 660,
              border: "1px dashed #ccc",
              marginTop: 16,
            }}
            onMouseDown={handleMouseDown}
            onMouseMove={handleMouseMove}
            onMouseUp={handleMouseUp}
            onMouseLeave={handleMouseUp}
          >
            <img
              ref={imgRef}
              src={imgUrl}
              style={{
                position: "absolute",
                top: "50%",
                left: "50%",
                transform: `translate(calc(-50% + ${moveConfig.moveX}px), calc(-50% + ${moveConfig.moveY}px)) scale(${scale}) rotate(${rotate}deg)`,
                cursor: isDragging ? "grabbing" : "grab",
                transformOrigin: "center center",
                maxWidth: "none",
                maxHeight: "none",
                width: "auto",
                height: "auto",
              }}
              draggable={false}
              onLoad={(e) => {
                const img = e.target as HTMLImageElement;
                if (imgRef.current) {
                  imgRef.current.width = img.naturalWidth;
                  imgRef.current.height = img.naturalHeight;
                }
              }}
            />
          </div>
          {/* 控制按钮区域 */}
          <div style={{ marginTop: 16 }}>
            <Space.Compact>
              <Button
                icon={<RotateLeftOutlined />}
                onClick={() => handleRotate(-90)}
              />
              <Button
                icon={<RotateRightOutlined />}
                onClick={() => handleRotate(90)}
              />
              <Button
                icon={<ZoomInOutlined />}
                onClick={() => handleScale(scale + 0.1)}
              />
              <Button
                icon={<ZoomOutOutlined />}
                onClick={() => handleScale(scale - 0.1)}
              />
            </Space.Compact>

            {/* 缩放控制 */}
            <div style={{ marginTop: 16 }}>
              <h4>缩放: {scale.toFixed(1)}x</h4>
              <Slider
                min={0.1}
                max={3}
                step={0.1}
                value={scale}
                onChange={handleScale}
              />
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default PictureUpload;

实现效果

最终实现的图片编辑器具有以下特点:

  1. 简洁的上传界面
  2. 直观的拖拽移动体验
  3. 平滑的滚轮缩放效果
  4. 精确的旋转控制
  5. 实时的缩放比例显示

小结

这个实现可以轻松集成到各种Web应用中,为用户提供完善的图片编辑体验。通过进一步扩展,还可以添加更多功能如裁剪、滤镜等,打造更强大的图片编辑器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值