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

React可拖拽缩放多矩形选区实现相关工具与教程求助

React图片矩形标注功能实现方案

推荐NPM模块

直接使用成熟的开源模块可以快速实现需求,以下是匹配度最高的几个选择:

  • react-image-annotate:专门针对图片标注场景封装的组件,开箱即用支持矩形标注,自带拖拽调整大小、多标注管理、标注分类等功能,不需要自己处理底层交互逻辑,适合快速上线的场景。
  • react-rnd:轻量级的可拖拽、可调整大小的React组件封装,没有多余的标注相关逻辑,灵活度极高,你可以基于它自己在上层封装标注绘制、矩形管理的逻辑,适合需要高度自定义交互的场景。
  • react-konva:基于Canvas封装的React 2D图形库,内置所有图形元素的拖拽、变形、事件监听能力,如果你需要同时支持除了矩形之外的其他标注类型、或者需要处理大量标注不卡顿的场景,可以选择这个库。

自定义实现核心思路

如果不想引入第三方标注组件,也可以基于基础组件自行实现:

  • 布局层:底层放置展示图片的img标签,上层叠加一个和图片尺寸一致的透明定位层,用来监听鼠标绘制事件
  • 绘制逻辑:监听透明层的鼠标按下、移动、抬起事件,按下时记录起始坐标,移动时实时计算宽高渲染临时矩形,抬起时将矩形坐标存入列表完成初始绘制
  • 交互逻辑:给每个已绘制的矩形绑定拖拽、调整大小能力,选中矩形时显示8个方向的调整锚点,支持修改矩形尺寸和位置
  • 坐标转换:注意如果图片有缩放展示的情况,需要将页面坐标转换为图片的实际相对坐标,避免导出标注坐标时出现误差

最简实现代码示例

下面是基于react-rnd实现的核心代码片段,可以直接在此基础上扩展:

import { useState } from 'react';
import { Rnd } from 'react-rnd';

const ImageMarker = () => {
  // 存储所有标注矩形的坐标信息
  const [markBoxes, setMarkBoxes] = useState([]);
  // 绘制临时矩形的状态
  const [drawingBox, setDrawingBox] = useState(null);

  // 监听透明层鼠标按下,开始绘制
  const handleMouseDown = (e) => {
    const { left, top } = e.currentTarget.getBoundingClientRect();
    const startX = e.clientX - left;
    const startY = e.clientY - top;
    setDrawingBox({ startX, startY, width: 0, height: 0 });
  };

  // 监听鼠标移动,更新临时矩形尺寸
  const handleMouseMove = (e) => {
    if (!drawingBox) return;
    const { left, top } = e.currentTarget.getBoundingClientRect();
    const currentX = e.clientX - left;
    const currentY = e.clientY - top;
    setDrawingBox({
      ...drawingBox,
      width: currentX - drawingBox.startX,
      height: currentY - drawingBox.startY
    });
  };

  // 监听鼠标抬起,完成矩形绘制
  const handleMouseUp = () => {
    if (!drawingBox || drawingBox.width < 5 || drawingBox.height < 5) {
      setDrawingBox(null);
      return;
    }
    setMarkBoxes([
      ...markBoxes,
      {
        x: drawingBox.width > 0 ? drawingBox.startX : drawingBox.startX + drawingBox.width,
        y: drawingBox.height > 0 ? drawingBox.startY : drawingBox.startY + drawingBox.height,
        width: Math.abs(drawingBox.width),
        height: Math.abs(drawingBox.height)
      }
    ]);
    setDrawingBox(null);
  };

  return (
    <div style={{ position: 'relative', width: '800px', margin: '0 auto' }}>
      {/* 底图 */}
      <img 
        src="替换为你的图片地址" 
        alt="标注底图"
        style={{ width: '100%', display: 'block' }}
      />
      {/* 事件监听透明层 */}
      <div
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%'
        }}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseUp}
      >
        {/* 绘制中的临时矩形 */}
        {drawingBox && (
          <div
            style={{
              position: 'absolute',
              left: drawingBox.width > 0 ? drawingBox.startX : drawingBox.startX + drawingBox.width,
              top: drawingBox.height > 0 ? drawingBox.startY : drawingBox.startY + drawingBox.height,
              width: Math.abs(drawingBox.width),
              height: Math.abs(drawingBox.height),
              border: '2px solid #f00',
              background: 'rgba(255,0,0,0.1)'
            }}
          />
        )}
        {/* 已完成的可拖拽调整矩形 */}
        {markBoxes.map((box, index) => (
          <Rnd
            key={index}
            position={{ x: box.x, y: box.y }}
            size={{ width: box.width, height: box.height }}
            onDragStop={(e, data) => {
              const newBoxes = [...markBoxes];
              newBoxes[index] = { ...newBoxes[index], x: data.x, y: data.y };
              setMarkBoxes(newBoxes);
            }}
            onResizeStop={(e, direction, ref, delta, position) => {
              const newBoxes = [...markBoxes];
              newBoxes[index] = {
                ...newBoxes[index],
                width: ref.offsetWidth,
                height: ref.offsetHeight,
                x: position.x,
                y: position.y
              };
              setMarkBoxes(newBoxes);
            }}
            style={{
              border: '2px solid #f00',
              background: 'rgba(255,0,0,0.1)',
              boxSizing: 'border-box'
            }}
          />
        ))}
      </div>
    </div>
  );
};

export default ImageMarker;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 05:09:03