@arronqzy/vue-view 0.1.0

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.
Files changed (75) hide show
  1. package/README.md +50 -0
  2. package/package.json +49 -0
  3. package/src/env.d.ts +62 -0
  4. package/src/index.ts +4 -0
  5. package/src/panel/VueViewOnlinePreview.vue +276 -0
  6. package/src/panel/VueViewPanel.vue +871 -0
  7. package/src/panel/components/ConfigHintIcon.vue +34 -0
  8. package/src/panel/components/ElementsLayer.vue +165 -0
  9. package/src/panel/components/MaterialPreview.vue +135 -0
  10. package/src/panel/components/MaterialSidebar.vue +526 -0
  11. package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
  12. package/src/panel/components/MoveableLayer.vue +859 -0
  13. package/src/panel/components/PanelCanvas.vue +630 -0
  14. package/src/panel/components/PanelConfigSidebar.vue +397 -0
  15. package/src/panel/components/PanelRulers.vue +177 -0
  16. package/src/panel/components/SelectLayer.vue +115 -0
  17. package/src/panel/components/ViewElementScopePanel.vue +76 -0
  18. package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
  19. package/src/panel/components/WorkspaceProjectNav.vue +192 -0
  20. package/src/panel/components/WorkspaceStageSplit.vue +258 -0
  21. package/src/panel/components/config/ConfigColorField.vue +52 -0
  22. package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
  23. package/src/panel/components/config/ConfigSection.vue +50 -0
  24. package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
  25. package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
  26. package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
  27. package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
  28. package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
  29. package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
  30. package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
  31. package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
  32. package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
  33. package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
  34. package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
  35. package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
  36. package/src/panel/components/config/shared.ts +74 -0
  37. package/src/panel/components/elementsLayerNodes.ts +830 -0
  38. package/src/panel/components/materialSidebarData.ts +85 -0
  39. package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
  40. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
  41. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
  42. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
  43. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
  44. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
  45. package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
  46. package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
  47. package/src/panel/constants/messages.ts +34 -0
  48. package/src/panel/constants/zIndex.ts +6 -0
  49. package/src/panel/hooks/usePanelElements.ts +1075 -0
  50. package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
  51. package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
  52. package/src/panel/lib/panel-ruler-canvas.ts +139 -0
  53. package/src/panel/library/workspace-project-cache.ts +23 -0
  54. package/src/panel/library/workspace-project-db.ts +111 -0
  55. package/src/panel/library/workspace-project-sync.ts +41 -0
  56. package/src/panel/library/workspace-snapshot.ts +30 -0
  57. package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
  58. package/src/panel/scope/view-scope-store.ts +82 -0
  59. package/src/panel/types.ts +127 -0
  60. package/src/panel/utils/chartOptionBuilder.ts +327 -0
  61. package/src/panel/utils/gridPlacement.ts +189 -0
  62. package/src/panel/utils/mappingLayerOps.ts +142 -0
  63. package/src/panel/utils/panelElementDefaults.ts +161 -0
  64. package/src/panel/utils/panelElementNodes.ts +35 -0
  65. package/src/panel/utils/panelStateIO.ts +124 -0
  66. package/src/panel/utils/scope-autocomplete.ts +114 -0
  67. package/src/panel/utils/scope-field-labels.ts +46 -0
  68. package/src/panel/utils/scope-template-chart.ts +92 -0
  69. package/src/panel/utils/scope-template-preview.ts +124 -0
  70. package/src/panel/utils/scope-template-spread.ts +229 -0
  71. package/src/panel/utils/scope-template-warnings.ts +243 -0
  72. package/src/panel/utils/scope-template.ts +97 -0
  73. package/src/panel/utils/updateElementDraft.ts +221 -0
  74. package/src/panel/viewportZoom.ts +26 -0
  75. package/src/tailwind.css +43 -0
@@ -0,0 +1,859 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ onMounted,
5
+ onUnmounted,
6
+ ref,
7
+ shallowRef,
8
+ watch,
9
+ } from "vue";
10
+ import Moveable from "moveable";
11
+ import type { PanelElement } from "../types";
12
+ import { uniformViewportZoom } from "../viewportZoom";
13
+ import { notifyPreviewLayoutChanged } from "../utils/panelStateIO";
14
+
15
+ const props = defineProps<{
16
+ zoomX: number;
17
+ zoomY: number;
18
+ canvasContainer: HTMLElement | null;
19
+ dragContainer?: HTMLElement | null;
20
+ selectedTargets: HTMLElement[];
21
+ elementsById: Map<string, PanelElement>;
22
+ updateElement: (
23
+ id: string,
24
+ patch: Partial<PanelElement>,
25
+ options?: { batchId?: string; meta?: Record<string, unknown> }
26
+ ) => void;
27
+ refreshToken?: number | string;
28
+ viewportSyncRef?: { current: (() => void) | null };
29
+ }>();
30
+
31
+ function getGridSlotLayout(grid: PanelElement) {
32
+ const rows = Math.max(1, Math.floor(grid.gridRows ?? 2));
33
+ const cols = Math.max(1, Math.floor(grid.gridCols ?? 3));
34
+ const gap = Math.max(0, grid.gridGap ?? 8);
35
+ const padding = Math.max(0, grid.gridPadding ?? 10);
36
+ const innerWidth = Math.max(1, grid.width - padding * 2);
37
+ const innerHeight = Math.max(1, grid.height - padding * 2);
38
+ const cellWidth = Math.max(1, (innerWidth - gap * (cols - 1)) / cols);
39
+ const cellHeight = Math.max(1, (innerHeight - gap * (rows - 1)) / rows);
40
+ const slots: Array<{
41
+ index: number;
42
+ row: number;
43
+ col: number;
44
+ centerX: number;
45
+ centerY: number;
46
+ x: number;
47
+ y: number;
48
+ }> = [];
49
+ for (let r = 0; r < rows; r++) {
50
+ for (let c = 0; c < cols; c++) {
51
+ const x = grid.x + padding + c * (cellWidth + gap);
52
+ const y = grid.y + padding + r * (cellHeight + gap);
53
+ slots.push({
54
+ index: r * cols + c,
55
+ row: r,
56
+ col: c,
57
+ x,
58
+ y,
59
+ centerX: x + cellWidth / 2,
60
+ centerY: y + cellHeight / 2,
61
+ });
62
+ }
63
+ }
64
+ return { slots, cellWidth, cellHeight, rows, cols, gap };
65
+ }
66
+
67
+ function inferSpanBySize(size: number, cellSize: number, gap: number, maxSpan: number) {
68
+ const safeMax = Math.max(1, Math.floor(maxSpan));
69
+ const unit = Math.max(1, cellSize + gap);
70
+ const ratio = (Math.max(1, size) + gap) / unit;
71
+ const whole = Math.floor(ratio);
72
+ const fraction = ratio - whole;
73
+ const promoted = fraction >= 0.72 ? whole + 1 : Math.max(1, whole);
74
+ return Math.max(1, Math.min(safeMax, promoted));
75
+ }
76
+
77
+ function getGridSpanCells(
78
+ rows: number,
79
+ cols: number,
80
+ slotIndex: number,
81
+ colSpan: number,
82
+ rowSpan: number
83
+ ) {
84
+ const total = rows * cols;
85
+ const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
86
+ const startRow = Math.floor(safeIndex / cols);
87
+ const startCol = safeIndex % cols;
88
+ const safeColSpan = Math.max(1, Math.min(cols - startCol, Math.floor(colSpan || 1)));
89
+ const safeRowSpan = Math.max(1, Math.min(rows - startRow, Math.floor(rowSpan || 1)));
90
+ const indices: number[] = [];
91
+ for (let r = startRow; r < startRow + safeRowSpan; r++) {
92
+ for (let c = startCol; c < startCol + safeColSpan; c++) {
93
+ indices.push(r * cols + c);
94
+ }
95
+ }
96
+ return {
97
+ safeIndex,
98
+ startRow,
99
+ startCol,
100
+ colSpan: safeColSpan,
101
+ rowSpan: safeRowSpan,
102
+ indices,
103
+ };
104
+ }
105
+
106
+ function getOccupiedSlotSet(
107
+ elementsById: Map<string, PanelElement>,
108
+ gridId: string,
109
+ selfId: string,
110
+ layerId?: string
111
+ ) {
112
+ const occupied = new Set<number>();
113
+ const grid = elementsById.get(gridId);
114
+ if (!grid || grid.materialType !== "grid") return occupied;
115
+ const { rows, cols } = getGridSlotLayout(grid);
116
+ for (const el of elementsById.values()) {
117
+ if (el.id === selfId) continue;
118
+ if (layerId && el.layerId !== layerId) continue;
119
+ if (el.parentGridId !== gridId) continue;
120
+ if (el.gridSlotIndex === undefined) continue;
121
+ const span = getGridSpanCells(
122
+ rows,
123
+ cols,
124
+ el.gridSlotIndex,
125
+ el.gridColSpan ?? 1,
126
+ el.gridRowSpan ?? 1
127
+ );
128
+ span.indices.forEach((idx) => occupied.add(idx));
129
+ }
130
+ return occupied;
131
+ }
132
+
133
+ function isDescendantGrid(
134
+ elementsById: Map<string, PanelElement>,
135
+ possibleDescendantGridId: string,
136
+ ancestorGridId: string
137
+ ) {
138
+ let current = elementsById.get(possibleDescendantGridId);
139
+ const visited = new Set<string>();
140
+ while (current?.parentGridId) {
141
+ if (visited.has(current.id)) return false;
142
+ visited.add(current.id);
143
+ if (current.parentGridId === ancestorGridId) return true;
144
+ current = elementsById.get(current.parentGridId);
145
+ }
146
+ return false;
147
+ }
148
+
149
+ function readLiveCanvasScale(canvasContainer: HTMLElement | null, fallback: number): number {
150
+ if (!canvasContainer) return fallback;
151
+ const raw = canvasContainer.dataset.canvasScale;
152
+ if (!raw) return fallback;
153
+ const parsed = Number(raw);
154
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
155
+ }
156
+
157
+ const moveableRef = shallowRef<Moveable | null>(null);
158
+ const lockBadgeScreen = ref<{ x: number; y: number } | null>(null);
159
+ const syncRafRef = ref<number | null>(null);
160
+
161
+ const targets = computed(() => {
162
+ const validTargets = props.selectedTargets.filter(
163
+ (target) =>
164
+ !!target && target.isConnected && !!target.closest("[data-panel-canvas]")
165
+ );
166
+ return validTargets.length ? validTargets : null;
167
+ });
168
+
169
+ function getId(target: HTMLElement) {
170
+ const el = target.closest<HTMLElement>(".rv-selectable");
171
+ return el?.dataset.elementId ?? null;
172
+ }
173
+
174
+ const selectedIds = computed(() =>
175
+ (targets.value ?? [])
176
+ .map((target) => getId(target))
177
+ .filter((id): id is string => !!id)
178
+ );
179
+
180
+ const lockedSelected = computed(() =>
181
+ selectedIds.value.filter((id) => props.elementsById.get(id)?.locked)
182
+ );
183
+
184
+ const hasLockedSelected = computed(() => lockedSelected.value.length > 0);
185
+
186
+ const movableTargets = computed(() => {
187
+ if (!targets.value) return null;
188
+ const unlocked = targets.value.filter((target) => {
189
+ const id = getId(target);
190
+ if (!id) return false;
191
+ return !props.elementsById.get(id)?.locked;
192
+ });
193
+ return unlocked.length ? unlocked : null;
194
+ });
195
+
196
+ const movableIds = computed(() =>
197
+ (movableTargets.value ?? [])
198
+ .map((target) => getId(target))
199
+ .filter((id): id is string => !!id)
200
+ );
201
+
202
+ const propCanvasScale = computed(() =>
203
+ uniformViewportZoom({ x: props.zoomX, y: props.zoomY })
204
+ );
205
+
206
+ const canvasScale = computed(() =>
207
+ readLiveCanvasScale(props.canvasContainer, propCanvasScale.value)
208
+ );
209
+
210
+ const moveableControlZoom = computed(() => 1 / Math.max(0.0001, canvasScale.value));
211
+
212
+ const pointerRoot = computed(() => props.dragContainer ?? props.canvasContainer);
213
+
214
+ function toCanvasDeltaX(value: number) {
215
+ return value / Math.max(0.0001, canvasScale.value);
216
+ }
217
+
218
+ function toCanvasDeltaY(value: number) {
219
+ return value / Math.max(0.0001, canvasScale.value);
220
+ }
221
+
222
+ function readClientPoint(eventLike: {
223
+ clientX?: number;
224
+ clientY?: number;
225
+ inputEvent?: { clientX?: number; clientY?: number };
226
+ }) {
227
+ const x =
228
+ typeof eventLike?.clientX === "number"
229
+ ? eventLike.clientX
230
+ : typeof eventLike?.inputEvent?.clientX === "number"
231
+ ? eventLike.inputEvent.clientX
232
+ : null;
233
+ const y =
234
+ typeof eventLike?.clientY === "number"
235
+ ? eventLike.clientY
236
+ : typeof eventLike?.inputEvent?.clientY === "number"
237
+ ? eventLike.inputEvent.clientY
238
+ : null;
239
+ if (x === null || y === null) return null;
240
+ return { x, y };
241
+ }
242
+
243
+ function resolveSingleEventId(target: HTMLElement | undefined | null) {
244
+ const id = target ? getId(target) : null;
245
+ if (id) return id;
246
+ if (movableIds.value.length === 1) return movableIds.value[0] ?? null;
247
+ return null;
248
+ }
249
+
250
+ function readTargetCanvasSize(
251
+ target: HTMLElement | null | undefined,
252
+ fallback: { width: number; height: number }
253
+ ) {
254
+ const rect = target?.getBoundingClientRect?.();
255
+ if (rect && rect.width > 0 && rect.height > 0) {
256
+ return {
257
+ width: Math.max(1, toCanvasDeltaX(rect.width)),
258
+ height: Math.max(1, toCanvasDeltaY(rect.height)),
259
+ };
260
+ }
261
+ return { width: Math.max(1, fallback.width), height: Math.max(1, fallback.height) };
262
+ }
263
+
264
+ function getGridDirectChildren(gridId: string) {
265
+ const result: PanelElement[] = [];
266
+ for (const el of props.elementsById.values()) {
267
+ if (el.parentGridId !== gridId) continue;
268
+ result.push(el);
269
+ }
270
+ return result;
271
+ }
272
+
273
+ function getSnapPatch(
274
+ movingId: string,
275
+ x: number,
276
+ y: number,
277
+ width: number,
278
+ height: number
279
+ ): Partial<PanelElement> {
280
+ const moving = props.elementsById.get(movingId);
281
+ if (!moving) return {};
282
+ const centerX = x + width / 2;
283
+ const centerY = y + height / 2;
284
+ let nearest: {
285
+ gridId: string;
286
+ slotIndex: number;
287
+ slotX: number;
288
+ slotY: number;
289
+ width: number;
290
+ height: number;
291
+ colSpan: number;
292
+ rowSpan: number;
293
+ distance: number;
294
+ } | null = null;
295
+
296
+ for (const [id, el] of props.elementsById.entries()) {
297
+ if (id === movingId || el.materialType !== "grid") continue;
298
+ if (el.layerId !== moving.layerId) continue;
299
+ if (moving.materialType === "grid" && isDescendantGrid(props.elementsById, id, movingId)) {
300
+ continue;
301
+ }
302
+ const { slots, cellWidth, cellHeight, rows, cols, gap } = getGridSlotLayout(el);
303
+ const occupiedSlots = getOccupiedSlotSet(
304
+ props.elementsById,
305
+ el.id,
306
+ movingId,
307
+ moving.layerId
308
+ );
309
+ const threshold = Math.max(8, el.gridSnapThreshold ?? 36);
310
+ const inferredColSpan = inferSpanBySize(width, cellWidth, gap, cols);
311
+ const inferredRowSpan = inferSpanBySize(height, cellHeight, gap, rows);
312
+ const movingColSpan = Math.max(1, inferredColSpan);
313
+ const movingRowSpan = Math.max(1, inferredRowSpan);
314
+ const insideGridBounds =
315
+ centerX >= el.x &&
316
+ centerX <= el.x + Math.max(1, el.width) &&
317
+ centerY >= el.y &&
318
+ centerY <= el.y + Math.max(1, el.height);
319
+
320
+ for (const slot of slots) {
321
+ const spanCells = getGridSpanCells(
322
+ rows,
323
+ cols,
324
+ slot.index,
325
+ movingColSpan,
326
+ movingRowSpan
327
+ );
328
+ if (spanCells.colSpan !== movingColSpan || spanCells.rowSpan !== movingRowSpan) {
329
+ continue;
330
+ }
331
+ const blocked = spanCells.indices.some((idx) => occupiedSlots.has(idx));
332
+ if (blocked) continue;
333
+ const distance = Math.hypot(slot.centerX - centerX, slot.centerY - centerY);
334
+ if (!insideGridBounds && distance > threshold) continue;
335
+ if (!nearest || distance < nearest.distance) {
336
+ const spanWidth = spanCells.colSpan * cellWidth + (spanCells.colSpan - 1) * gap;
337
+ const spanHeight = spanCells.rowSpan * cellHeight + (spanCells.rowSpan - 1) * gap;
338
+ nearest = {
339
+ gridId: el.id,
340
+ slotIndex: spanCells.safeIndex,
341
+ slotX: slot.x,
342
+ slotY: slot.y,
343
+ width: spanWidth,
344
+ height: spanHeight,
345
+ colSpan: spanCells.colSpan,
346
+ rowSpan: spanCells.rowSpan,
347
+ distance,
348
+ };
349
+ }
350
+ }
351
+ }
352
+
353
+ if (!nearest) {
354
+ if (moving.parentGridId || moving.gridSlotIndex !== undefined) {
355
+ return { parentGridId: undefined, gridSlotIndex: undefined };
356
+ }
357
+ return {};
358
+ }
359
+
360
+ return {
361
+ x: nearest.slotX,
362
+ y: nearest.slotY,
363
+ parentGridId: nearest.gridId,
364
+ gridSlotIndex: nearest.slotIndex,
365
+ gridColSpan: nearest.colSpan,
366
+ gridRowSpan: nearest.rowSpan,
367
+ width: nearest.width,
368
+ height: nearest.height,
369
+ };
370
+ }
371
+
372
+ function syncLockBadge() {
373
+ const id = lockedSelected.value[0];
374
+ const root = pointerRoot.value;
375
+ if (!id || !root) {
376
+ lockBadgeScreen.value = null;
377
+ return;
378
+ }
379
+ const target = props.selectedTargets.find(
380
+ (node) => node.closest<HTMLElement>(".rv-selectable")?.dataset.elementId === id
381
+ );
382
+ if (!target) {
383
+ lockBadgeScreen.value = null;
384
+ return;
385
+ }
386
+ const rect = target.getBoundingClientRect();
387
+ const rootRect = root.getBoundingClientRect();
388
+ lockBadgeScreen.value = {
389
+ x: rect.left - rootRect.left,
390
+ y: rect.top - rootRect.top,
391
+ };
392
+ }
393
+
394
+ function scheduleViewportSync() {
395
+ if (syncRafRef.value != null) return;
396
+ syncRafRef.value = requestAnimationFrame(() => {
397
+ syncRafRef.value = null;
398
+ moveableRef.value?.updateRect?.();
399
+ syncLockBadge();
400
+ });
401
+ }
402
+
403
+ function destroyMoveable() {
404
+ moveableRef.value?.destroy();
405
+ moveableRef.value = null;
406
+ }
407
+
408
+ function createMoveable() {
409
+ destroyMoveable();
410
+ const container = props.canvasContainer;
411
+ const movable = movableTargets.value;
412
+ const root = pointerRoot.value;
413
+ if (!container || !movable?.length || !root) return;
414
+
415
+ const moveable = new Moveable(container, {
416
+ target: movable,
417
+ rootContainer: container,
418
+ container,
419
+ dragContainer: root,
420
+ className: "rv-moveable-layer",
421
+ zoom: moveableControlZoom.value,
422
+ hideChildMoveableDefaultLines: false,
423
+ dragArea: true,
424
+ draggable: true,
425
+ resizable: true,
426
+ rotatable: true,
427
+ origin: false,
428
+ throttleDrag: 0,
429
+ throttleResize: 0,
430
+ throttleRotate: 0,
431
+ });
432
+
433
+ moveable.on("dragStart", (e: any) => {
434
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
435
+ if (!id) return;
436
+ const data = props.elementsById.get(id);
437
+ if (!data) return;
438
+ e.set([0, 0]);
439
+ e.datas.__startX = data.x;
440
+ e.datas.__startY = data.y;
441
+ const point = readClientPoint(e);
442
+ e.datas.__startClientX = point?.x ?? null;
443
+ e.datas.__startClientY = point?.y ?? null;
444
+ });
445
+
446
+ moveable.on("drag", (e: any) => {
447
+ if (!e?.target?.style) return;
448
+ if (e.datas.__startX === undefined || e.datas.__startY === undefined) return;
449
+ const sx = e.datas.__startX ?? 0;
450
+ const sy = e.datas.__startY ?? 0;
451
+ const point = readClientPoint(e);
452
+ const hasClient =
453
+ typeof e.datas.__startClientX === "number" &&
454
+ typeof e.datas.__startClientY === "number" &&
455
+ !!point;
456
+ const tx = hasClient
457
+ ? toCanvasDeltaX(point.x - e.datas.__startClientX)
458
+ : toCanvasDeltaX(e.beforeTranslate?.[0] ?? 0);
459
+ const ty = hasClient
460
+ ? toCanvasDeltaY(point.y - e.datas.__startClientY)
461
+ : toCanvasDeltaY(e.beforeTranslate?.[1] ?? 0);
462
+ e.target.style.left = `${sx + tx}px`;
463
+ e.target.style.top = `${sy + ty}px`;
464
+ });
465
+
466
+ moveable.on("dragGroupStart", (e: any) => {
467
+ e.events.forEach((ev: any) => {
468
+ const id = ev.target ? getId(ev.target) : null;
469
+ if (!id) return;
470
+ const data = props.elementsById.get(id);
471
+ if (!data) return;
472
+ ev.set([0, 0]);
473
+ ev.datas.__startX = data.x;
474
+ ev.datas.__startY = data.y;
475
+ const point = readClientPoint(ev) ?? readClientPoint(e);
476
+ ev.datas.__startClientX = point?.x ?? null;
477
+ ev.datas.__startClientY = point?.y ?? null;
478
+ });
479
+ });
480
+
481
+ moveable.on("dragGroup", (e: any) => {
482
+ e.events.forEach((ev: any) => {
483
+ if (!ev?.target?.style) return;
484
+ const sx = ev.datas.__startX ?? 0;
485
+ const sy = ev.datas.__startY ?? 0;
486
+ const point = readClientPoint(ev) ?? readClientPoint(e);
487
+ const hasClient =
488
+ typeof ev.datas.__startClientX === "number" &&
489
+ typeof ev.datas.__startClientY === "number" &&
490
+ !!point;
491
+ const tx = hasClient
492
+ ? toCanvasDeltaX(point.x - ev.datas.__startClientX)
493
+ : toCanvasDeltaX(ev.beforeTranslate?.[0] ?? 0);
494
+ const ty = hasClient
495
+ ? toCanvasDeltaY(point.y - ev.datas.__startClientY)
496
+ : toCanvasDeltaY(ev.beforeTranslate?.[1] ?? 0);
497
+ ev.target.style.left = `${sx + tx}px`;
498
+ ev.target.style.top = `${sy + ty}px`;
499
+ });
500
+ });
501
+
502
+ moveable.on("resizeStart", (e: any) => {
503
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
504
+ if (!id) return;
505
+ const data = props.elementsById.get(id);
506
+ if (!data) return;
507
+ e.setOrigin(["%", "%"]);
508
+ e.dragStart?.set([0, 0]);
509
+ e.datas.__startX = data.x;
510
+ e.datas.__startY = data.y;
511
+ e.datas.__startClientX = e.inputEvent?.clientX ?? null;
512
+ e.datas.__startClientY = e.inputEvent?.clientY ?? null;
513
+ });
514
+
515
+ moveable.on("resize", (e: any) => {
516
+ if (!e?.target?.style) return;
517
+ if (e.datas.__startX === undefined || e.datas.__startY === undefined) return;
518
+ e.target.style.width = `${e.width}px`;
519
+ e.target.style.height = `${e.height}px`;
520
+ const sx = e.datas.__startX ?? 0;
521
+ const sy = e.datas.__startY ?? 0;
522
+ const tx = toCanvasDeltaX(e.drag.beforeTranslate?.[0] ?? 0);
523
+ const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
524
+ e.target.style.left = `${sx + tx}px`;
525
+ e.target.style.top = `${sy + ty}px`;
526
+ notifyPreviewLayoutChanged();
527
+ });
528
+
529
+ moveable.on("resizeGroupStart", (e: any) => {
530
+ e.events.forEach((ev: any) => {
531
+ const id = ev.target ? getId(ev.target) : null;
532
+ if (!id) return;
533
+ const data = props.elementsById.get(id);
534
+ if (!data) return;
535
+ ev.setOrigin(["%", "%"]);
536
+ ev.dragStart?.set([0, 0]);
537
+ ev.datas.__startX = data.x;
538
+ ev.datas.__startY = data.y;
539
+ const input = ev.inputEvent ?? e.inputEvent;
540
+ ev.datas.__startClientX = input?.clientX ?? null;
541
+ ev.datas.__startClientY = input?.clientY ?? null;
542
+ });
543
+ });
544
+
545
+ moveable.on("resizeGroup", (e: any) => {
546
+ e.events.forEach((ev: any) => {
547
+ if (!ev?.target?.style) return;
548
+ ev.target.style.width = `${ev.width}px`;
549
+ ev.target.style.height = `${ev.height}px`;
550
+ const sx = ev.datas.__startX ?? 0;
551
+ const sy = ev.datas.__startY ?? 0;
552
+ const tx = toCanvasDeltaX(ev.drag.beforeTranslate?.[0] ?? 0);
553
+ const ty = toCanvasDeltaY(ev.drag.beforeTranslate?.[1] ?? 0);
554
+ ev.target.style.left = `${sx + tx}px`;
555
+ ev.target.style.top = `${sy + ty}px`;
556
+ });
557
+ notifyPreviewLayoutChanged();
558
+ });
559
+
560
+ moveable.on("rotateStart", (e: any) => {
561
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
562
+ if (!id) return;
563
+ const data = props.elementsById.get(id);
564
+ if (!data) return;
565
+ e.set(data.rotate ?? 0);
566
+ });
567
+
568
+ moveable.on("rotate", (e: any) => {
569
+ if (!e?.target?.style) return;
570
+ e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
571
+ });
572
+
573
+ moveable.on("rotateGroup", (e: any) => {
574
+ e.events.forEach((ev: any) => {
575
+ if (!ev?.target?.style) return;
576
+ ev.target.style.transform = `rotate(${ev.beforeRotate}deg)`;
577
+ });
578
+ });
579
+
580
+ moveable.on("dragEnd", (e: any) => {
581
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
582
+ if (!id) return;
583
+ const data = props.elementsById.get(id);
584
+ if (!data) return;
585
+ const sx = e.datas.__startX ?? data.x;
586
+ const sy = e.datas.__startY ?? data.y;
587
+ const point = readClientPoint(e);
588
+ const hasClient =
589
+ typeof e.datas.__startClientX === "number" &&
590
+ typeof e.datas.__startClientY === "number" &&
591
+ !!point;
592
+ const tx = hasClient
593
+ ? toCanvasDeltaX(point.x - e.datas.__startClientX)
594
+ : toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
595
+ const ty = hasClient
596
+ ? toCanvasDeltaY(point.y - e.datas.__startClientY)
597
+ : toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
598
+ const nextX = sx + tx;
599
+ const nextY = sy + ty;
600
+ const size = readTargetCanvasSize(e.target as HTMLElement | null, {
601
+ width: data.width,
602
+ height: data.height,
603
+ });
604
+ const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
605
+ props.updateElement(id, { x: nextX, y: nextY, ...patch });
606
+ if (data.materialType === "grid") {
607
+ const dx = nextX - data.x;
608
+ const dy = nextY - data.y;
609
+ if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
610
+ const children = getGridDirectChildren(id);
611
+ const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
612
+ children.forEach((child) => {
613
+ props.updateElement(
614
+ child.id,
615
+ { x: child.x + dx, y: child.y + dy },
616
+ { batchId, meta: { type: "node.group-drag" } }
617
+ );
618
+ });
619
+ }
620
+ }
621
+ scheduleViewportSync();
622
+ });
623
+
624
+ moveable.on("dragGroupEnd", (e: any) => {
625
+ const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
626
+ const selectedSet = new Set(
627
+ e.events
628
+ .map((ev: any) => (ev.target ? getId(ev.target) : null))
629
+ .filter((id: string | null): id is string => !!id)
630
+ );
631
+ const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
632
+ e.events.forEach((ev: any) => {
633
+ const id = ev.target ? getId(ev.target) : null;
634
+ if (!id) return;
635
+ const data = props.elementsById.get(id);
636
+ if (!data) return;
637
+ const sx = ev.datas.__startX ?? data.x;
638
+ const sy = ev.datas.__startY ?? data.y;
639
+ const point = readClientPoint(ev) ?? readClientPoint(e);
640
+ const hasClient =
641
+ typeof ev.datas.__startClientX === "number" &&
642
+ typeof ev.datas.__startClientY === "number" &&
643
+ !!point;
644
+ const tx = hasClient
645
+ ? toCanvasDeltaX(point.x - ev.datas.__startClientX)
646
+ : toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
647
+ const ty = hasClient
648
+ ? toCanvasDeltaY(point.y - ev.datas.__startClientY)
649
+ : toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
650
+ const nextX = sx + tx;
651
+ const nextY = sy + ty;
652
+ const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
653
+ width: data.width,
654
+ height: data.height,
655
+ });
656
+ const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
657
+ props.updateElement(
658
+ id,
659
+ { x: nextX, y: nextY, ...patch },
660
+ { batchId, meta: { type: "node.group-drag" } }
661
+ );
662
+ if (data.materialType === "grid") {
663
+ const dx = nextX - data.x;
664
+ const dy = nextY - data.y;
665
+ if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
666
+ gridDeltaMap.set(id, { dx, dy });
667
+ }
668
+ }
669
+ });
670
+ for (const [gridId, delta] of gridDeltaMap.entries()) {
671
+ const children = getGridDirectChildren(gridId);
672
+ children.forEach((child) => {
673
+ if (selectedSet.has(child.id)) return;
674
+ props.updateElement(
675
+ child.id,
676
+ { x: child.x + delta.dx, y: child.y + delta.dy },
677
+ { batchId, meta: { type: "node.group-drag" } }
678
+ );
679
+ });
680
+ }
681
+ scheduleViewportSync();
682
+ });
683
+
684
+ moveable.on("resizeEnd", (e: any) => {
685
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
686
+ if (!id) return;
687
+ const data = props.elementsById.get(id);
688
+ if (!data) return;
689
+ const width = e.lastEvent?.width ?? data.width;
690
+ const height = e.lastEvent?.height ?? data.height;
691
+ const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
692
+ const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
693
+ const sx = e.datas.__startX ?? data.x;
694
+ const sy = e.datas.__startY ?? data.y;
695
+ const nextX = sx + tx;
696
+ const nextY = sy + ty;
697
+ const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
698
+ const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
699
+ props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
700
+ scheduleViewportSync();
701
+ });
702
+
703
+ moveable.on("resizeGroupEnd", (e: any) => {
704
+ const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
705
+ e.events.forEach((ev: any) => {
706
+ const id = ev.target ? getId(ev.target) : null;
707
+ if (!id) return;
708
+ const data = props.elementsById.get(id);
709
+ if (!data) return;
710
+ const width = ev.lastEvent?.width ?? data.width;
711
+ const height = ev.lastEvent?.height ?? data.height;
712
+ const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
713
+ const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
714
+ const sx = ev.datas.__startX ?? data.x;
715
+ const sy = ev.datas.__startY ?? data.y;
716
+ const nextX = sx + tx;
717
+ const nextY = sy + ty;
718
+ const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
719
+ const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
720
+ props.updateElement(
721
+ id,
722
+ { width, height, x: nextX, y: nextY, ...snapPatch },
723
+ { batchId, meta: { type: "node.group-resize" } }
724
+ );
725
+ });
726
+ scheduleViewportSync();
727
+ });
728
+
729
+ moveable.on("rotateEnd", (e: any) => {
730
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
731
+ if (!id) return;
732
+ const data = props.elementsById.get(id);
733
+ if (!data) return;
734
+ const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
735
+ props.updateElement(id, { rotate });
736
+ scheduleViewportSync();
737
+ });
738
+
739
+ moveable.on("rotateGroupEnd", (e: any) => {
740
+ const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
741
+ e.events.forEach((ev: any) => {
742
+ const id = ev.target ? getId(ev.target) : null;
743
+ if (!id) return;
744
+ const data = props.elementsById.get(id);
745
+ if (!data) return;
746
+ const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
747
+ props.updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
748
+ });
749
+ scheduleViewportSync();
750
+ });
751
+
752
+ moveableRef.value = moveable;
753
+ scheduleViewportSync();
754
+ }
755
+
756
+ watch(
757
+ () => [
758
+ props.canvasContainer,
759
+ movableIds.value.join(","),
760
+ props.refreshToken,
761
+ moveableControlZoom.value,
762
+ ] as const,
763
+ () => {
764
+ createMoveable();
765
+ syncLockBadge();
766
+ }
767
+ );
768
+
769
+ watch(canvasScale, () => {
770
+ if (moveableRef.value) {
771
+ moveableRef.value.zoom = moveableControlZoom.value;
772
+ moveableRef.value.updateRect?.();
773
+ }
774
+ syncLockBadge();
775
+ });
776
+
777
+ watch(
778
+ () => props.viewportSyncRef,
779
+ (refObj) => {
780
+ if (!refObj) return;
781
+ refObj.current = scheduleViewportSync;
782
+ },
783
+ { immediate: true }
784
+ );
785
+
786
+ onMounted(() => {
787
+ if (props.viewportSyncRef) {
788
+ props.viewportSyncRef.current = scheduleViewportSync;
789
+ }
790
+ createMoveable();
791
+ });
792
+
793
+ onUnmounted(() => {
794
+ if (props.viewportSyncRef) {
795
+ props.viewportSyncRef.current = null;
796
+ }
797
+ if (syncRafRef.value != null) {
798
+ cancelAnimationFrame(syncRafRef.value);
799
+ }
800
+ destroyMoveable();
801
+ });
802
+
803
+ const showLockBadgeOnly = computed(
804
+ () => lockBadgeScreen.value && hasLockedSelected.value && !movableTargets.value
805
+ );
806
+
807
+ const showPartialLockBadge = computed(
808
+ () => hasLockedSelected.value && lockBadgeScreen.value && !!movableTargets.value
809
+ );
810
+ </script>
811
+
812
+ <template>
813
+ <Teleport v-if="pointerRoot && showLockBadgeOnly" :to="pointerRoot">
814
+ <div
815
+ class="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
816
+ :style="{ left: `${lockBadgeScreen!.x + 6}px`, top: `${lockBadgeScreen!.y - 22}px` }"
817
+ title="节点已锁定"
818
+ >
819
+ <svg
820
+ viewBox="0 0 24 24"
821
+ fill="none"
822
+ stroke="currentColor"
823
+ stroke-width="1.9"
824
+ stroke-linecap="round"
825
+ stroke-linejoin="round"
826
+ class="h-3.5 w-3.5"
827
+ aria-hidden="true"
828
+ >
829
+ <rect x="4.5" y="10.5" width="15" height="10" rx="2.5" />
830
+ <path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
831
+ <circle cx="12" cy="15.5" r="1.2" />
832
+ </svg>
833
+ <span>已锁定</span>
834
+ </div>
835
+ </Teleport>
836
+ <Teleport v-else-if="pointerRoot && showPartialLockBadge" :to="pointerRoot">
837
+ <div
838
+ class="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
839
+ :style="{ left: `${lockBadgeScreen!.x + 6}px`, top: `${lockBadgeScreen!.y - 22}px` }"
840
+ title="部分节点已锁定,已自动排除"
841
+ >
842
+ <svg
843
+ viewBox="0 0 24 24"
844
+ fill="none"
845
+ stroke="currentColor"
846
+ stroke-width="1.9"
847
+ stroke-linecap="round"
848
+ stroke-linejoin="round"
849
+ class="h-3.5 w-3.5"
850
+ aria-hidden="true"
851
+ >
852
+ <rect x="4.5" y="10.5" width="15" height="10" rx="2.5" />
853
+ <path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
854
+ <circle cx="12" cy="15.5" r="1.2" />
855
+ </svg>
856
+ <span>锁定节点已排除</span>
857
+ </div>
858
+ </Teleport>
859
+ </template>