@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 +56 -26
- package/package.json +6 -7
- package/readme.md +74 -5
- package/src/panel/ReactViewPanel.tsx +17 -7
- package/src/panel/components/ConfigHintIcon.tsx +46 -0
- package/src/panel/components/ElementsLayer.tsx +2 -4
- package/src/panel/components/MoveableLayer.tsx +68 -55
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +55 -77
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +5 -8
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +238 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +24 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +1 -1
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/types.ts +2 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/scope-field-labels.ts +1 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +110 -2
- package/src/panel/utils/scope-template.ts +25 -3
- package/src/tailwind.css +27 -14
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
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
-
|
|
2330
|
-
height: 1px !important;
|
|
2331
|
-
margin: 0 !important;
|
|
2340
|
+
background: var(--moveable-color) !important;
|
|
2332
2341
|
}
|
|
2333
2342
|
|
|
2334
|
-
.rv-moveable-layer .moveable-
|
|
2335
|
-
|
|
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
|
+
"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.
|
|
30
|
-
"@arronqzy/blueprint-dsl": "1.0.
|
|
31
|
-
"@arronqzy/react-blueprint": "1.0.
|
|
32
|
-
"@arronqzy/rx-store": "1.0.
|
|
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.
|
|
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
|
-
|
|
3
|
+
Abuilder 的**视图编辑器** React 包,提供可视化画布、图层、物料、在线预览与工作区管理能力。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 功能概览
|
|
6
6
|
|
|
7
|
-
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
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={
|
|
2409
|
-
|
|
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 +
|
|
2612
|
+
(centerX - rect.left + scrollPosRef.current.left) /
|
|
2603
2613
|
Math.max(0.0001, zoom.x);
|
|
2604
2614
|
targetY =
|
|
2605
|
-
(centerY - rect.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
|
-
/**
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
224
|
+
const propCanvasScale = useMemo(
|
|
214
225
|
() => uniformViewportZoom({ x: zoomX, y: zoomY }),
|
|
215
226
|
[zoomX, zoomY]
|
|
216
227
|
);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
282
|
+
const syncRafRef = useRef<number | null>(null);
|
|
283
|
+
const syncLockBadge = useCallback(() => {
|
|
274
284
|
const id = lockedSelected[0];
|
|
275
|
-
if (!id || !
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
canvasContainer.removeEventListener("scroll", sync);
|
|
317
|
+
viewportSyncRef.current = null;
|
|
302
318
|
};
|
|
303
|
-
}, [
|
|
319
|
+
}, [scheduleViewportSync, viewportSyncRef]);
|
|
304
320
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
464
|
+
return lockBadgeOnly && pointerRoot
|
|
465
|
+
? createPortal(lockBadgeOnly, pointerRoot)
|
|
466
|
+
: null;
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
if (!movableTargets) {
|
|
460
|
-
return lockBadgeOnly
|
|
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
|
|
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
|
|