@arronqzy/react-view 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -584,10 +584,6 @@ video {
584
584
  pointer-events: none;
585
585
  }
586
586
 
587
- .visible {
588
- visibility: visible;
589
- }
590
-
591
587
  .collapse {
592
588
  visibility: collapse;
593
589
  }
@@ -767,10 +763,6 @@ video {
767
763
  margin-top: 0.25rem;
768
764
  }
769
765
 
770
- .mt-1\.5 {
771
- margin-top: 0.375rem;
772
- }
773
-
774
766
  .mt-2 {
775
767
  margin-top: 0.5rem;
776
768
  }
@@ -890,6 +882,10 @@ video {
890
882
  height: 100%;
891
883
  }
892
884
 
885
+ .max-h-28 {
886
+ max-height: 7rem;
887
+ }
888
+
893
889
  .max-h-44 {
894
890
  max-height: 11rem;
895
891
  }
@@ -1030,14 +1026,14 @@ video {
1030
1026
  max-width: 200px;
1031
1027
  }
1032
1028
 
1033
- .max-w-\[260px\] {
1034
- max-width: 260px;
1035
- }
1036
-
1037
1029
  .max-w-\[360px\] {
1038
1030
  max-width: 360px;
1039
1031
  }
1040
1032
 
1033
+ .max-w-\[380px\] {
1034
+ max-width: 380px;
1035
+ }
1036
+
1041
1037
  .max-w-\[420px\] {
1042
1038
  max-width: 420px;
1043
1039
  }
@@ -1278,10 +1274,18 @@ video {
1278
1274
  white-space: nowrap;
1279
1275
  }
1280
1276
 
1277
+ .whitespace-pre-wrap {
1278
+ white-space: pre-wrap;
1279
+ }
1280
+
1281
1281
  .break-words {
1282
1282
  overflow-wrap: break-word;
1283
1283
  }
1284
1284
 
1285
+ .break-all {
1286
+ word-break: break-all;
1287
+ }
1288
+
1285
1289
  .rounded {
1286
1290
  border-radius: 0.25rem;
1287
1291
  }
@@ -2010,6 +2014,10 @@ video {
2010
2014
  color: hsl(var(--foreground));
2011
2015
  }
2012
2016
 
2017
+ .text-foreground\/80 {
2018
+ color: hsl(var(--foreground) / 0.8);
2019
+ }
2020
+
2013
2021
  .text-foreground\/90 {
2014
2022
  color: hsl(var(--foreground) / 0.9);
2015
2023
  }
@@ -2311,34 +2319,48 @@ video {
2311
2319
 
2312
2320
  .rv-moveable-layer {
2313
2321
  --moveable-color: #1990ff;
2322
+ z-index: 9999 !important;
2314
2323
  }
2315
2324
 
2316
- .rv-moveable-layer .moveable-control {
2317
- width: 14px !important;
2318
- height: 14px !important;
2319
- margin-top: -7px !important;
2320
- margin-left: -7px !important;
2321
- border-radius: 50% !important;
2322
- box-sizing: border-box !important;
2323
- border: 2px solid hsl(var(--background)) !important;
2324
- background: #1990ff !important;
2325
+ .rv-moveable-layer.moveable-group,
2326
+ .rv-moveable-layer .moveable-group,
2327
+ .rv-moveable-layer.moveable-control-box,
2328
+ .rv-moveable-layer .moveable-control-box {
2329
+ z-index: 1 !important;
2330
+ }
2331
+
2332
+ .rv-moveable-layer .moveable-line {
2333
+ --moveable-color: #1990ff;
2325
2334
  background: var(--moveable-color) !important;
2326
2335
  }
2327
2336
 
2337
+ /* 组选/单边控制线:厚度由 moveable zoom 补偿,保持屏幕 1px */
2338
+
2328
2339
  .rv-moveable-layer .moveable-line.moveable-direction {
2329
- width: 1px !important;
2330
- height: 1px !important;
2331
- margin: 0 !important;
2340
+ background: var(--moveable-color) !important;
2332
2341
  }
2333
2342
 
2334
- .rv-moveable-layer .moveable-line {
2335
- --moveable-color: #1990ff;
2343
+ .rv-moveable-layer .moveable-group .moveable-control-box,
2344
+ .rv-moveable-layer.moveable-group .moveable-control-box {
2345
+ z-index: 2 !important;
2336
2346
  }
2337
2347
 
2338
2348
  .rv-moveable-layer .moveable-around-control {
2339
2349
  background: transparent !important;
2340
2350
  }
2341
2351
 
2352
+ .rv-moveable-layer .moveable-control {
2353
+ width: 14px !important;
2354
+ height: 14px !important;
2355
+ margin-top: -7px !important;
2356
+ margin-left: -7px !important;
2357
+ border-radius: 50% !important;
2358
+ box-sizing: border-box !important;
2359
+ border: 2px solid hsl(var(--background)) !important;
2360
+ background: #1990ff !important;
2361
+ background: var(--moveable-color) !important;
2362
+ }
2363
+
2342
2364
  .hover\:border-border:hover {
2343
2365
  border-color: hsl(var(--border));
2344
2366
  }
@@ -2575,10 +2597,18 @@ video {
2575
2597
  width: 0.5rem;
2576
2598
  }
2577
2599
 
2600
+ .\[\&_\.moveable-area\]\:pointer-events-auto .moveable-area {
2601
+ pointer-events: auto;
2602
+ }
2603
+
2578
2604
  .\[\&_\.moveable-control-box\]\:pointer-events-auto .moveable-control-box {
2579
2605
  pointer-events: auto;
2580
2606
  }
2581
2607
 
2608
+ .\[\&_\.moveable-group\]\:pointer-events-auto .moveable-group {
2609
+ pointer-events: auto;
2610
+ }
2611
+
2582
2612
  .\[\&_\.scope-field--highlight\]\:rounded-md .scope-field--highlight {
2583
2613
  border-radius: calc(var(--radius) - 2px);
2584
2614
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -16,7 +16,6 @@
16
16
  "dependencies": {
17
17
  "@scena/react-ruler": "^0.19.0",
18
18
  "react-infinite-viewer": "^0.28.1",
19
- "react-zoom-pan-pinch": "^4.0.3",
20
19
  "react-moveable": "^0.56.0",
21
20
  "react-selecto": "^1.26.3",
22
21
  "rxjs": "^7.8.2",
@@ -26,10 +25,10 @@
26
25
  "infinite-viewer": "^0.29.1",
27
26
  "echarts": "^6.0.0",
28
27
  "uuid": "^13.0.0",
29
- "@arronqzy/ui": "1.0.0",
30
- "@arronqzy/blueprint-dsl": "1.0.1",
31
- "@arronqzy/react-blueprint": "1.0.1",
32
- "@arronqzy/rx-store": "1.0.1"
28
+ "@arronqzy/ui": "1.0.2",
29
+ "@arronqzy/blueprint-dsl": "1.0.3",
30
+ "@arronqzy/react-blueprint": "1.0.3",
31
+ "@arronqzy/rx-store": "1.0.2"
33
32
  },
34
33
  "devDependencies": {
35
34
  "@types/react": "^19.0.1",
@@ -39,7 +38,7 @@
39
38
  "typescript": "^5.5.4",
40
39
  "@arronqzy/eslint-config": "0.0.0",
41
40
  "@arronqzy/typescript-config": "1.0.0",
42
- "@arronqzy/tailwind": "1.0.1"
41
+ "@arronqzy/tailwind": "1.0.2"
43
42
  },
44
43
  "scripts": {
45
44
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
package/readme.md CHANGED
@@ -1,17 +1,75 @@
1
1
  # @arronqzy/react-view
2
2
 
3
- ## Tailwind 使用方式
3
+ Abuilder 的**视图编辑器** React 包,提供可视化画布、图层、物料、在线预览与工作区管理能力。
4
4
 
5
- 本包内已内置 Tailwind 配置,并提供可直接引入的样式产物:
5
+ ## 功能概览
6
6
 
7
- - 生成样式:`pnpm -C packages/react-view build:css`
8
- - 使用样式:在你的应用入口中引入
7
+ - **无限画布**:平移、缩放、标尺、框选与 Moveable 拖拽/缩放/旋转
8
+ - **图层系统**:多图层、映射图层、主图层、锁定与合并
9
+ - **物料节点**:图表(ECharts)、文本、图片、音视频、几何、网格、引用节点等
10
+ - **Scope 模版**:节点属性支持 `{scope.xxx}` 表达式与解析预览
11
+ - **工作区**:IndexedDB 持久化、多项目切换、同步与在线预览
12
+ - **蓝图联动**:与 `@arronqzy/react-blueprint` 分屏编辑,运行时 Scope 回写视图
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ pnpm add @arronqzy/react-view
18
+ ```
19
+
20
+ 依赖 React 19,并需引入样式:
9
21
 
10
22
  ```ts
11
23
  import "@arronqzy/react-view/styles.css";
12
24
  ```
13
25
 
14
- 如果你希望由应用侧的 Tailwind 来扫描本包源码(不引入上面的 `styles.css`),请把本包路径加入应用的 `content` 配置,例如:
26
+ ## 使用
27
+
28
+ ### 完整编辑器面板
29
+
30
+ ```tsx
31
+ import { ReactViewPanel } from "@arronqzy/react-view";
32
+ import "@arronqzy/react-view/styles.css";
33
+
34
+ export function Editor() {
35
+ return <ReactViewPanel className="h-screen" initialZoom={1} />;
36
+ }
37
+ ```
38
+
39
+ ### 在线预览
40
+
41
+ ```tsx
42
+ import { ReactViewOnlinePreview } from "@arronqzy/react-view";
43
+
44
+ <ReactViewOnlinePreview projectId="your-project-id" />
45
+ ```
46
+
47
+ URL 参数解析:
48
+
49
+ ```ts
50
+ import { parseOnlinePreviewSearchParams } from "@arronqzy/react-view";
51
+
52
+ const params = parseOnlinePreviewSearchParams("?preview=online&projectId=xxx");
53
+ ```
54
+
55
+ ## 核心导出
56
+
57
+ | 导出 | 说明 |
58
+ |------|------|
59
+ | `ReactViewPanel` | 主编辑面板(画布 + 配置侧栏 + 蓝图分屏) |
60
+ | `ReactViewOnlinePreview` | 只读在线预览页 |
61
+ | `parseOnlinePreviewSearchParams` | 解析预览 URL 参数 |
62
+ | `createView` / `createAction` 等 | 视图 DSL 与指令扩展(`src/core`) |
63
+
64
+ ## 样式与 Tailwind
65
+
66
+ 本包内置 Tailwind 配置,构建产物为 `dist/styles.css`:
67
+
68
+ ```bash
69
+ pnpm -C packages/react-view build:css
70
+ ```
71
+
72
+ 若由应用侧 Tailwind 扫描源码,请将本包加入 `content`:
15
73
 
16
74
  ```ts
17
75
  content: [
@@ -19,3 +77,14 @@ content: [
19
77
  "./node_modules/@arronqzy/react-view/src/**/*.{ts,tsx}",
20
78
  ]
21
79
  ```
80
+
81
+ ## 依赖关系
82
+
83
+ - `@arronqzy/ui` — 基础 UI 组件
84
+ - `@arronqzy/rx-store` — 画布状态与历史
85
+ - `@arronqzy/react-blueprint` — 蓝图编辑与执行
86
+ - `@arronqzy/blueprint-dsl` — 蓝图运行时
87
+
88
+ ## 许可证
89
+
90
+ MIT
@@ -57,6 +57,7 @@ import {
57
57
  import { resolvePanelElementScope } from "./utils/scope-template";
58
58
  import { PANEL_MESSAGES } from "./constants/messages";
59
59
  import { PANEL_Z_INDEX } from "./constants/zIndex";
60
+ import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
60
61
  import {
61
62
  AlertDialog,
62
63
  AlertDialogAction,
@@ -111,6 +112,7 @@ import {
111
112
  TooltipProvider,
112
113
  TooltipTrigger,
113
114
  toast,
115
+ useTheme,
114
116
  } from "@arronqzy/ui";
115
117
 
116
118
  function IconPlus() {
@@ -304,6 +306,7 @@ function shouldClearSelectionOnBlank(target: HTMLElement | null) {
304
306
  if (
305
307
  target.closest(".rv-selectable") ||
306
308
  target.closest(".moveable-control-box") ||
309
+ target.closest(".moveable-group") ||
307
310
  target.closest(".moveable-line") ||
308
311
  target.closest(".moveable-control") ||
309
312
  target.closest(".moveable-direction")
@@ -333,6 +336,7 @@ export type ReactViewPanelProps = {
333
336
  export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelProps) {
334
337
  const THEME_STORAGE_KEY = "panel:theme";
335
338
  const TITLE_ICON_STORAGE_KEY = "panel:titleIconDataUrl";
339
+ const { setTheme } = useTheme();
336
340
  const themedScrollbarClass =
337
341
  "scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
338
342
  const {
@@ -396,7 +400,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
396
400
  return { x: rounded, y: rounded };
397
401
  });
398
402
  }, []);
399
- const [scroll, setScroll] = useState({ left: 0, top: 0 });
403
+ const { scroll, scrollRef: scrollPosRef, onScrollChange: onViewportScrollChange } =
404
+ useRafThrottledScroll();
405
+ const viewportSyncRef = useRef<(() => void) | null>(null);
400
406
 
401
407
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
402
408
  const [selectedTargets, setSelectedTargets] = useState<HTMLElement[]>([]);
@@ -487,7 +493,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
487
493
  const applyTheme = useCallback((checked: boolean) => {
488
494
  const root = document.documentElement;
489
495
  root.classList.toggle("dark", checked);
496
+ root.classList.toggle("light", !checked);
490
497
  root.dataset.theme = checked ? "dark" : "light";
498
+ setTheme(checked ? "dark" : "light");
491
499
  try {
492
500
  // panel 独立持久化键,避免与宿主应用的通用 theme 键互相覆盖
493
501
  localStorage.setItem(THEME_STORAGE_KEY, checked ? "dark" : "light");
@@ -497,7 +505,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
497
505
  // ignore storage errors
498
506
  }
499
507
  setIsDark(checked);
500
- }, [THEME_STORAGE_KEY]);
508
+ }, [THEME_STORAGE_KEY, setTheme]);
501
509
 
502
510
  useEffect(() => {
503
511
  const root = document.documentElement;
@@ -2369,6 +2377,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2369
2377
  const nodeIdFromElement = nodeEl?.dataset.elementId ?? null;
2370
2378
  const onMoveable =
2371
2379
  !!target?.closest(".moveable-control-box") ||
2380
+ !!target?.closest(".moveable-group") ||
2372
2381
  !!target?.closest(".moveable-line") ||
2373
2382
  !!target?.closest(".moveable-control") ||
2374
2383
  !!target?.closest(".moveable-direction");
@@ -2394,8 +2403,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2394
2403
  ref={syncScrollRef}
2395
2404
  zoom={zoom}
2396
2405
  onZoomChange={setZoom}
2397
- onScrollChange={setScroll}
2406
+ onScrollChange={onViewportScrollChange}
2398
2407
  canvasRef={syncCanvasRef}
2408
+ viewportSyncRef={viewportSyncRef}
2399
2409
  viewportOverlay={
2400
2410
  <MoveableLayer
2401
2411
  zoomX={zoom.x}
@@ -2405,8 +2415,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2405
2415
  selectedTargets={selectedTargets}
2406
2416
  elementsById={byId}
2407
2417
  updateElement={updateElement}
2408
- refreshToken={`${historyCursor}|${scroll.left}|${scroll.top}|${zoom.x}|${zoom.y}`}
2409
- viewportScroll={scroll}
2418
+ refreshToken={historyCursor}
2419
+ viewportSyncRef={viewportSyncRef}
2410
2420
  />
2411
2421
  }
2412
2422
  onCanvasMouseDownCapture={(e) => {
@@ -2599,10 +2609,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2599
2609
  const centerX = rect.left + rect.width / 2;
2600
2610
  const centerY = rect.top + rect.height / 2;
2601
2611
  targetX =
2602
- (centerX - rect.left + scroll.left) /
2612
+ (centerX - rect.left + scrollPosRef.current.left) /
2603
2613
  Math.max(0.0001, zoom.x);
2604
2614
  targetY =
2605
- (centerY - rect.top + scroll.top) /
2615
+ (centerY - rect.top + scrollPosRef.current.top) /
2606
2616
  Math.max(0.0001, zoom.y);
2607
2617
  }
2608
2618
  duplicateElement(
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ Tooltip,
4
+ TooltipContent,
5
+ TooltipProvider,
6
+ TooltipTrigger,
7
+ } from "@arronqzy/ui";
8
+
9
+ export type ConfigHintIconProps = {
10
+ label?: string;
11
+ children: ReactNode;
12
+ className?: string;
13
+ contentClassName?: string;
14
+ };
15
+
16
+ export function ConfigHintIcon({
17
+ label = "说明",
18
+ children,
19
+ className,
20
+ contentClassName,
21
+ }: ConfigHintIconProps) {
22
+ return (
23
+ <TooltipProvider delayDuration={120}>
24
+ <Tooltip>
25
+ <TooltipTrigger asChild>
26
+ <button
27
+ type="button"
28
+ className={
29
+ className ??
30
+ "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
31
+ }
32
+ aria-label={`${label}说明`}
33
+ >
34
+ ?
35
+ </button>
36
+ </TooltipTrigger>
37
+ <TooltipContent
38
+ side="top"
39
+ className={`z-[10120] max-w-[360px] text-[11px] leading-5 ${contentClassName ?? ""}`}
40
+ >
41
+ {children}
42
+ </TooltipContent>
43
+ </Tooltip>
44
+ </TooltipProvider>
45
+ );
46
+ }
@@ -607,8 +607,6 @@ function ChartNodeContent({
607
607
  window.addEventListener(PREVIEW_LAYOUT_EVENT, onLayout);
608
608
  const obs = new ResizeObserver(onLayout);
609
609
  obs.observe(host);
610
- const parent = host.parentElement;
611
- if (parent) obs.observe(parent);
612
610
  return () => {
613
611
  window.removeEventListener(PREVIEW_LAYOUT_EVENT, onLayout);
614
612
  obs.disconnect();
@@ -792,7 +790,7 @@ function ReferenceNodeContent({
792
790
  );
793
791
  }
794
792
 
795
- export function ElementsLayer({
793
+ export const ElementsLayer = React.memo(function ElementsLayer({
796
794
  elements,
797
795
  allElements,
798
796
  selectedIds,
@@ -887,5 +885,5 @@ export function ElementsLayer({
887
885
  })}
888
886
  </>
889
887
  );
890
- }
888
+ });
891
889
 
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import Moveable from "react-moveable";
4
4
 
@@ -41,8 +41,8 @@ export type MoveableLayerProps = {
41
41
  ) => void;
42
42
  /** 画布平移/缩放等视口变化时递增或变化,用于触发控制框重算 */
43
43
  refreshToken?: number | string;
44
- /** InfiniteViewer 虚拟滚动位置(无原生 scroll 事件) */
45
- viewportScroll?: { left: number; top: number };
44
+ /** 平移后立即同步控制框(由 PanelCanvas 写入,绕过 React scroll 节流) */
45
+ viewportSyncRef?: React.MutableRefObject<(() => void) | null>;
46
46
  };
47
47
 
48
48
  function getGridSlotLayout(grid: PanelElement) {
@@ -155,6 +155,17 @@ function isDescendantGrid(
155
155
  return false;
156
156
  }
157
157
 
158
+ function readLiveCanvasScale(
159
+ canvasContainer: HTMLElement | null,
160
+ fallback: number
161
+ ): number {
162
+ if (!canvasContainer) return fallback;
163
+ const raw = canvasContainer.dataset.canvasScale;
164
+ if (!raw) return fallback;
165
+ const parsed = Number(raw);
166
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
167
+ }
168
+
158
169
  export function MoveableLayer({
159
170
  zoomX,
160
171
  zoomY,
@@ -164,7 +175,7 @@ export function MoveableLayer({
164
175
  elementsById,
165
176
  updateElement,
166
177
  refreshToken,
167
- viewportScroll,
178
+ viewportSyncRef,
168
179
  }: MoveableLayerProps) {
169
180
  const moveableRef = useRef<any>(null);
170
181
  const targets = useMemo(() => {
@@ -210,15 +221,13 @@ export function MoveableLayer({
210
221
  [getId, movableTargets]
211
222
  );
212
223
  const moveableRenderKey = useMemo(() => movableIds.join(","), [movableIds]);
213
- const canvasScale = useMemo(
224
+ const propCanvasScale = useMemo(
214
225
  () => uniformViewportZoom({ x: zoomX, y: zoomY }),
215
226
  [zoomX, zoomY]
216
227
  );
217
- /** 控制框在 scale 画布内渲染,用倒数保持屏幕像素尺寸 */
218
- const moveableControlZoom = useMemo(
219
- () => 1 / Math.max(0.0001, canvasScale),
220
- [canvasScale]
221
- );
228
+ const canvasScale = readLiveCanvasScale(canvasContainer, propCanvasScale);
229
+ /** 在 scale 画布内渲染,用倒数补偿,保证锚点/边线屏幕像素恒定 */
230
+ const moveableControlZoom = 1 / Math.max(0.0001, canvasScale);
222
231
  const pointerRoot = dragContainer ?? canvasContainer;
223
232
  const resolveSingleEventId = useCallback(
224
233
  (target: HTMLElement | undefined | null) => {
@@ -270,9 +279,10 @@ export function MoveableLayer({
270
279
  const [lockBadgeScreen, setLockBadgeScreen] = useState<{ x: number; y: number } | null>(
271
280
  null
272
281
  );
273
- useEffect(() => {
282
+ const syncRafRef = useRef<number | null>(null);
283
+ const syncLockBadge = useCallback(() => {
274
284
  const id = lockedSelected[0];
275
- if (!id || !canvasContainer) {
285
+ if (!id || !pointerRoot) {
276
286
  setLockBadgeScreen(null);
277
287
  return;
278
288
  }
@@ -283,33 +293,37 @@ export function MoveableLayer({
283
293
  setLockBadgeScreen(null);
284
294
  return;
285
295
  }
286
- const sync = () => {
287
- const rect = target.getBoundingClientRect();
288
- const rootRect = canvasContainer.getBoundingClientRect();
289
- setLockBadgeScreen({
290
- x: rect.left - rootRect.left,
291
- y: rect.top - rootRect.top,
292
- });
293
- };
294
- sync();
295
- const ro = new ResizeObserver(sync);
296
- ro.observe(target);
297
- ro.observe(canvasContainer);
298
- canvasContainer.addEventListener("scroll", sync, { passive: true });
296
+ const rect = target.getBoundingClientRect();
297
+ const rootRect = pointerRoot.getBoundingClientRect();
298
+ setLockBadgeScreen({
299
+ x: rect.left - rootRect.left,
300
+ y: rect.top - rootRect.top,
301
+ });
302
+ }, [lockedSelected, pointerRoot, selectedTargets]);
303
+
304
+ const scheduleViewportSync = useCallback(() => {
305
+ if (syncRafRef.current != null) return;
306
+ syncRafRef.current = requestAnimationFrame(() => {
307
+ syncRafRef.current = null;
308
+ moveableRef.current?.updateRect?.();
309
+ syncLockBadge();
310
+ });
311
+ }, [syncLockBadge]);
312
+
313
+ useEffect(() => {
314
+ if (!viewportSyncRef) return;
315
+ viewportSyncRef.current = scheduleViewportSync;
299
316
  return () => {
300
- ro.disconnect();
301
- canvasContainer.removeEventListener("scroll", sync);
317
+ viewportSyncRef.current = null;
302
318
  };
303
- }, [canvasContainer, lockedSelected, selectedTargets, viewportScroll, zoomX, zoomY]);
319
+ }, [scheduleViewportSync, viewportSyncRef]);
304
320
 
305
- const updateRectNextFrame = useCallback(() => {
306
- // InfiniteViewer 用 transform 平移,需等视口与节点布局提交后再 updateRect
307
- requestAnimationFrame(() => {
308
- requestAnimationFrame(() => {
309
- moveableRef.current?.updateRect?.();
310
- });
311
- });
312
- }, []);
321
+ useLayoutEffect(() => {
322
+ moveableRef.current?.updateRect?.();
323
+ syncLockBadge();
324
+ }, [canvasScale, syncLockBadge]);
325
+
326
+ const updateRectNextFrame = scheduleViewportSync;
313
327
  const getGridDirectChildren = useCallback(
314
328
  (gridId: string) => {
315
329
  const result: PanelElement[] = [];
@@ -421,22 +435,16 @@ export function MoveableLayer({
421
435
  useEffect(() => {
422
436
  if (!targets) return;
423
437
  updateRectNextFrame();
424
- }, [targets, updateRectNextFrame]);
425
-
426
- useEffect(() => {
427
- if (!targets) return;
428
- updateRectNextFrame();
429
- }, [refreshToken, targets, updateRectNextFrame]);
438
+ }, [moveableRenderKey, refreshToken, targets, updateRectNextFrame]);
430
439
 
431
- useEffect(() => {
432
- if (!targets) return;
433
- updateRectNextFrame();
434
- }, [targets, updateRectNextFrame, viewportScroll?.left, viewportScroll?.top]);
435
-
436
- useEffect(() => {
437
- if (!targets) return;
438
- updateRectNextFrame();
439
- }, [targets, updateRectNextFrame, zoomX, zoomY]);
440
+ useEffect(
441
+ () => () => {
442
+ if (syncRafRef.current != null) {
443
+ cancelAnimationFrame(syncRafRef.current);
444
+ }
445
+ },
446
+ []
447
+ );
440
448
 
441
449
  if (!canvasContainer) return null;
442
450
 
@@ -453,11 +461,15 @@ export function MoveableLayer({
453
461
  ) : null;
454
462
 
455
463
  if (!targets) {
456
- return lockBadgeOnly ? createPortal(lockBadgeOnly, canvasContainer) : null;
464
+ return lockBadgeOnly && pointerRoot
465
+ ? createPortal(lockBadgeOnly, pointerRoot)
466
+ : null;
457
467
  }
458
468
 
459
469
  if (!movableTargets) {
460
- return lockBadgeOnly ? createPortal(lockBadgeOnly, canvasContainer) : null;
470
+ return lockBadgeOnly && pointerRoot
471
+ ? createPortal(lockBadgeOnly, pointerRoot)
472
+ : null;
461
473
  }
462
474
 
463
475
  const layer = (
@@ -469,9 +481,10 @@ export function MoveableLayer({
469
481
  target={movableTargets as unknown as HTMLElement[]}
470
482
  rootContainer={canvasContainer}
471
483
  container={canvasContainer}
472
- dragContainer={pointerRoot ?? canvasContainer}
484
+ dragContainer={pointerRoot}
473
485
  className="rv-moveable-layer"
474
486
  zoom={moveableControlZoom}
487
+ hideChildMoveableDefaultLines={false}
475
488
  // 允许在控制框内部区域拖动(不必必须点到某个节点本体)
476
489
  dragArea
477
490
  draggable
@@ -798,6 +811,6 @@ export function MoveableLayer({
798
811
  </>
799
812
  );
800
813
 
801
- return createPortal(layer, canvasContainer);
814
+ return canvasContainer ? createPortal(layer, canvasContainer) : null;
802
815
  }
803
816