RegionSelect
region-select图上框选回坐标 · 拖框拿回原图像素 [x1,y1,x2,y2](区别 ImageCropper 出 Blob) · 反向拖规范化 + minSide 滤误点 + aspect 固定比例(撞边界整体缩不破比例) + boxes 只读其它框带标注 + maxHeight 内滚 · SVG viewBox 打底画框零换算,指针→图像素折算是纯函数可测 · 自然尺寸自量、量到前不画框 · touch-none + setPointerCapture 后置 try/catch + 描边按图宽 · 题库配图/文档标注/OCR 纠错/热区/打码
用法
基础用法
在图上拖一个框,拿回**原图像素**坐标 [x1,y1,x2,y2];反向拖也给规范化结果。
box(原图像素):[60, 290, 480, 550]
const [box, setBox] = useState<RegionBox | null>(null)
<RegionSelect
src={pageUrl}
value={box}
onChange={setBox} // 拖完给一个规范化的框
minSide={8} // 短边小于此视为误点
maxHeight="60vh" // 超高图内部滚动
/>同页其它框
boxes 传只读的其它框(虚线 + 标注),当前编辑的那个仍是实线主框。
虚线是同页已有的框(只读),实线是当前拖出来的:(未框选)
<RegionSelect
src={pageUrl}
value={box}
onChange={setBox}
boxes={[
{ id: "q1", box: [60, 100, 840, 250], label: "题 1", color: "chart-2" },
{ id: "q3", box: [520, 860, 840, 1060], label: "题 3 配图", color: "chart-4" },
]}
/>固定比例 · 只读
aspect 固定宽高比(撞边界时整体缩,不会被单轴钳位破坏比例);readOnly 只看不改。
box(原图像素):[60, 290, 480, 550]
<RegionSelect src={pageUrl} value={box} onChange={setBox} aspect={16 / 9} />
<RegionSelect src={pageUrl} value={box} readOnly />何时用
需要把「图上的一块区域」存下来:题库配图纠错、文档标注、OCR 纠错框、截图打码、商品热区、缺陷标注。
和 ImageCropper 的分工是产物不同,不能互相顶替:ImageCropper 出的是裁好的位图(onCropped(blob)),本组件出的是坐标。存「原页 + 框」的引用时,框错了只需重拖一下,裁死了就得推倒重来;裁图交给服务端按框现渲,永远与框一致。反过来,只要一张裁好的图、不关心它在原图哪个位置,就用 ImageCropper。
导入
import { RegionSelect, type RegionBox } from "@hulianui/ui"Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| src* | string | — | 底图地址 |
| value | RegionBox | null | — | 当前框(原图像素 [x1,y1,x2,y2]),受控 |
| onChange | (box: RegionBox) => void | — | 拖完一次给一个规范化的框(反向拖也成立) |
| onDrafting | (box: RegionBox | null) => void | — | 拖拽过程中的实时框,结束回 null |
| minSide | number | 8 | 误点阈值:框短边小于它(原图像素)不触发 onChange |
| boxes | { box, color?, label?, id? }[] | — | 只读的其它框(虚线 + 标注),同页多图时一并显示 |
| aspect | number | — | 固定宽高比(宽/高);不传则自由框选 |
| naturalSize | { width, height } | — | 已知的原图尺寸;不传则组件用 new Image() 自己量 |
| maxHeight | string | number | "60vh" | 超高图的内部滚动上限 |
| color | string | "primary" | 主框描边色(语义色名 / 任意 CSS 色) |
| readOnly | boolean | false | 不响应拖拽,仍显示已有框 |
| placeholder | ReactNode | "载入图片…" | 量到自然尺寸前的占位 |
| alt | string | "" | 画布的无障碍名 |
纯函数(已导出)
toImagePoint(clientX, clientY, rect, w, h) 指针→图像素(含钳位)· normalizeBox(a, b) 两点→规范化框 · applyAspect(anchor, point, aspect, w, h) 按比例修正 · boxMinSide(box) · strokeWidthFor(w)。自己画框但不想重写这套算术时直接用。
禁忌 / 坑
这几条是踩过才知道要写进组件的,消费方不必再踩:
- 坐标系只有原图像素,不是容器像素也不是百分比——存进库的必须能直接喂给服务端裁图。内部用
<svg viewBox="0 0 naturalW naturalH">打底,画框零换算,只有「指针 → 图像素」一个方向按getBoundingClientRect()折算。 - 自然尺寸量到之前不画任何框:拿上一张图的比例摆框,位置一定是错的。已知尺寸(库里存着)就传
naturalSize,省一次预读,也让 SSR/测试环境不必等图解码。 - `setPointerCapture` 放在拖拽状态落地之后并 try/catch:合成 PointerEvent(Playwright / 单测 dispatch)下它会抛,先捕获就把整个 handler 中断,拖拽根本起不来。
- 画布带
touch-none,否则触屏上「拖框」变成「滚页面」。 - 描边宽度按图宽给(
max(2, naturalW/400)):3000px 宽的扫描页上 2px 的线细到看不见。 - 键盘/读屏无法直接拖框(这是画布类交互的共性)。要可达就在旁边配四个数字输入框读写同一个
value,组件是受控的,两边同源。
相关
ImageCropper · ImageViewer · Image · Annotation · Flow
Playground
[60, 290, 480, 550]
<RegionSelect
src={pageUrl}
value={box}
onChange={setBox}
/>