@arronqzy/vue-view 0.1.4 → 0.1.7

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 (41) hide show
  1. package/package.json +6 -5
  2. package/src/panel/VueViewOnlinePreview.vue +55 -19
  3. package/src/panel/VueViewPanel.vue +123 -66
  4. package/src/panel/components/BusyOverlay.vue +24 -0
  5. package/src/panel/components/ConfigHintIcon.vue +10 -10
  6. package/src/panel/components/ElementsLayer.vue +51 -8
  7. package/src/panel/components/MaterialPreview.vue +8 -0
  8. package/src/panel/components/PanelConfigSidebar.vue +32 -20
  9. package/src/panel/components/WorkspaceConfigSidebar.vue +11 -0
  10. package/src/panel/components/WorkspaceProjectNav.vue +36 -8
  11. package/src/panel/components/config/ConfigColorField.vue +33 -9
  12. package/src/panel/components/config/ConfigFieldGroup.vue +35 -6
  13. package/src/panel/components/config/PanelConfigAudioSection.vue +37 -19
  14. package/src/panel/components/config/PanelConfigChartSection.vue +101 -84
  15. package/src/panel/components/config/PanelConfigGeometrySection.vue +10 -2
  16. package/src/panel/components/config/PanelConfigImageSection.vue +18 -10
  17. package/src/panel/components/config/PanelConfigNodeInfo.vue +21 -12
  18. package/src/panel/components/config/PanelConfigReferenceSection.vue +5 -1
  19. package/src/panel/components/config/PanelConfigScene3dSection.vue +505 -0
  20. package/src/panel/components/config/PanelConfigStyleSections.vue +19 -11
  21. package/src/panel/components/config/PanelConfigTextSection.vue +25 -7
  22. package/src/panel/components/config/PanelConfigVideoSection.vue +13 -9
  23. package/src/panel/components/config/shared.ts +16 -21
  24. package/src/panel/components/elementsLayerNodes.ts +64 -59
  25. package/src/panel/components/materialSidebarData.ts +2 -0
  26. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +1 -1
  27. package/src/panel/components/table/PanelConfigTableSection.vue +39 -25
  28. package/src/panel/components/table/TableNodeContent.vue +36 -15
  29. package/src/panel/hooks/usePanelElements.ts +13 -1
  30. package/src/panel/hooks/useWorkspaceProjects.ts +128 -57
  31. package/src/panel/library/workspace-project-cache.ts +7 -2
  32. package/src/panel/library/workspace-project-db.ts +29 -11
  33. package/src/panel/library/workspace-snapshot.ts +21 -4
  34. package/src/panel/scope/view-scope-store.ts +8 -0
  35. package/src/panel/types.ts +4 -2
  36. package/src/panel/utils/async-work.ts +153 -0
  37. package/src/panel/utils/chartOptionBuilder.ts +7 -1
  38. package/src/panel/utils/gridPlacement.ts +29 -0
  39. package/src/panel/utils/outputScale.ts +56 -0
  40. package/src/panel/utils/panelElementDefaults.ts +37 -2
  41. package/src/panel/utils/scope-autocomplete.ts +0 -114
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,10 +23,11 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.5",
27
- "@arronqzy/vue-blueprint": "0.1.3",
28
- "@arronqzy/view-table": "0.1.3",
29
- "@arronqzy/i18n": "0.1.3",
26
+ "@arronqzy/blueprint-dsl": "1.0.7",
27
+ "@arronqzy/i18n": "0.1.6",
28
+ "@arronqzy/view-scene3d": "0.1.1",
29
+ "@arronqzy/view-table": "0.1.4",
30
+ "@arronqzy/vue-blueprint": "0.1.5",
30
31
  "@arronqzy/vue-rx-store": "0.1.2",
31
32
  "@arronqzy/rx-store": "1.0.4"
32
33
  },
@@ -8,8 +8,11 @@ import {
8
8
  listBlueprintLibrary,
9
9
  useBlueprintPageLifecycle,
10
10
  type BlueprintLibraryListItem,
11
+ resolveRunnableNodeType,
12
+ resolveViewElementIds,
11
13
  } from "@arronqzy/vue-blueprint";
12
14
  import type { LibraryBlueprintResolver, PageLifecyclePhase } from "@arronqzy/blueprint-dsl";
15
+ import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
13
16
  import type { State } from "@arronqzy/rx-store";
14
17
  import ElementsLayer from "./components/ElementsLayer.vue";
15
18
  import {
@@ -31,6 +34,7 @@ import {
31
34
  parsePanelLayers,
32
35
  resolvePreviewLayerElements,
33
36
  } from "./utils/panelStateIO";
37
+ import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
34
38
 
35
39
  const { t, locale } = useI18n();
36
40
 
@@ -49,6 +53,8 @@ const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
49
53
  const layoutRevision = ref(0);
50
54
  const layoutReady = ref(false);
51
55
  const sceneRef = ref<HTMLDivElement | null>(null);
56
+ const outputScale = ref(readOutputScale());
57
+ const fillScale = ref({ scaleX: 1, scaleY: 1 });
52
58
 
53
59
  const layers = computed(() => (panelState.value ? parsePanelLayers(panelState.value) : []));
54
60
  const activeLayerId = computed(() =>
@@ -146,24 +152,30 @@ const scopedLayerElements = computed(() => {
146
152
  const sceneBounds = computed(() => computePanelSceneBounds(scopedLayerElements.value));
147
153
 
148
154
  const displayElements = computed(() =>
149
- scopedLayerElements.value.map((el) => ({
150
- ...el,
151
- x: el.x - sceneBounds.value.minX,
152
- y: el.y - sceneBounds.value.minY,
153
- }))
155
+ scopedLayerElements.value.map((el) => {
156
+ const x = el.x - sceneBounds.value.minX;
157
+ const y = el.y - sceneBounds.value.minY;
158
+ if (outputScale.value) return { ...el, x, y };
159
+ return {
160
+ ...el,
161
+ x: x * fillScale.value.scaleX,
162
+ y: y * fillScale.value.scaleY,
163
+ width: el.width * fillScale.value.scaleX,
164
+ height: el.height * fillScale.value.scaleY,
165
+ };
166
+ })
154
167
  );
155
168
 
156
169
  function applySceneFit() {
157
- const scene = sceneRef.value;
158
- if (!scene) return;
159
- const vw = window.innerWidth || 1;
160
- const vh = window.innerHeight || 1;
161
- const sw = Math.max(1, sceneBounds.value.width);
162
- const sh = Math.max(1, sceneBounds.value.height);
163
- const scaleX = vw / sw;
164
- const scaleY = vh / sh;
165
- if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return;
166
- scene.style.transform = `scale(${scaleX}, ${scaleY})`;
170
+ const enabled = readOutputScale();
171
+ outputScale.value = enabled;
172
+ const fill = applyPreviewSceneFill(
173
+ sceneRef.value,
174
+ sceneBounds.value.width,
175
+ sceneBounds.value.height,
176
+ enabled
177
+ );
178
+ fillScale.value = { scaleX: fill.scaleX, scaleY: fill.scaleY };
167
179
  layoutRevision.value += 1;
168
180
  notifyPreviewLayoutChanged();
169
181
  }
@@ -195,7 +207,7 @@ const lifecycleReady = computed(
195
207
  () => Boolean(panelState.value && layerElements.value.length > 0 && layoutReady.value)
196
208
  );
197
209
 
198
- const { triggerBlueprintNode } = useBlueprintPageLifecycle({
210
+ const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
199
211
  graph: blueprintGraph,
200
212
  active: ref(true),
201
213
  enabled: lifecycleReady,
@@ -211,6 +223,22 @@ const { triggerBlueprintNode } = useBlueprintPageLifecycle({
211
223
  onViewScopeUpdate: handleViewScopeUpdate,
212
224
  });
213
225
 
226
+ const boundViewEventTypes = computed(() =>
227
+ collectArmedViewEventBindings(
228
+ blueprintGraph.value.document.nodes.map((node) => ({
229
+ id: node.id,
230
+ nodeType: resolveRunnableNodeType(node),
231
+ lifecyclePhase: node.lifecyclePhase,
232
+ viewElementIds: resolveViewElementIds(node),
233
+ eventConfig: node.eventConfig,
234
+ })),
235
+ blueprintGraph.value.document.edges,
236
+ EVENT_NODE_TYPE,
237
+ LIFECYCLE_NODE_TYPE,
238
+ firedLifecyclePhases.value
239
+ )
240
+ );
241
+
214
242
  onMounted(() => {
215
243
  const onResize = () => applySceneFit();
216
244
  window.addEventListener("resize", onResize);
@@ -231,8 +259,9 @@ function noopSelect() {}
231
259
 
232
260
  <div
233
261
  v-else-if="!panelState"
234
- class="flex min-h-screen w-full items-center justify-center bg-white text-sm text-gray-600"
262
+ class="flex min-h-screen w-full flex-col items-center justify-center gap-3 bg-white text-sm text-gray-600"
235
263
  >
264
+ <div class="h-9 w-9 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700" />
236
265
  {{ t("panel.workspace.previewLoading") }}
237
266
  </div>
238
267
 
@@ -258,9 +287,10 @@ function noopSelect() {}
258
287
  id="preview-scene"
259
288
  class="relative shrink-0 origin-top-left"
260
289
  :style="{
261
- width: `${sceneBounds.width}px`,
262
- height: `${sceneBounds.height}px`,
290
+ width: `${outputScale ? sceneBounds.width : sceneBounds.width * fillScale.scaleX}px`,
291
+ height: `${outputScale ? sceneBounds.height : sceneBounds.height * fillScale.scaleY}px`,
263
292
  transformOrigin: 'left top',
293
+ transform: outputScale ? `scale(${fillScale.scaleX}, ${fillScale.scaleY})` : 'none',
264
294
  }"
265
295
  >
266
296
  <ElementsLayer
@@ -276,6 +306,12 @@ function noopSelect() {}
276
306
  void triggerBlueprintNode(payload.blueprintNodeId, payload);
277
307
  }
278
308
  "
309
+ :bound-view-event-types="boundViewEventTypes"
310
+ :on-view-ui-event="
311
+ (payload) => {
312
+ void emitViewEvent(payload);
313
+ }
314
+ "
279
315
  @select-ids="noopSelect"
280
316
  />
281
317
  </div>
@@ -24,8 +24,10 @@ import {
24
24
  type BlueprintGraphNode,
25
25
  type BlueprintLibraryListItem,
26
26
  type BlueprintMetaDraft,
27
+ resolveRunnableNodeType,
28
+ resolveViewElementIds,
27
29
  } from "@arronqzy/vue-blueprint";
28
- import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
30
+ import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE, type LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
29
31
  import {
30
32
  Button,
31
33
  Checkbox,
@@ -65,6 +67,8 @@ import {
65
67
  import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
66
68
  import { useI18n } from "@arronqzy/i18n/vue";
67
69
  import { getPanelMessages } from "./constants/messages";
70
+ import BusyOverlay from "./components/BusyOverlay.vue";
71
+ import { assertFileSize, parseJsonText, runBusyTask } from "./utils/async-work";
68
72
  import "../tailwind.css";
69
73
 
70
74
  const { t, locale, setLocale } = useI18n();
@@ -400,7 +404,7 @@ const workspaceProjects = useWorkspaceProjects({
400
404
  onProjectApplied: handleWorkspaceProjectApplied,
401
405
  });
402
406
 
403
- const { triggerBlueprintNode } = useBlueprintPageLifecycle({
407
+ const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
404
408
  graph: blueprintGraph,
405
409
  active: blueprintOpen,
406
410
  bootKey: computed(() => workspaceProjects.activeProjectId.value ?? undefined),
@@ -412,6 +416,22 @@ const { triggerBlueprintNode } = useBlueprintPageLifecycle({
412
416
  onViewScopeUpdate: handleViewScopeUpdate,
413
417
  });
414
418
 
419
+ const boundViewEventTypes = computed(() =>
420
+ collectArmedViewEventBindings(
421
+ blueprintGraph.value.document.nodes.map((node) => ({
422
+ id: node.id,
423
+ nodeType: resolveRunnableNodeType(node),
424
+ lifecyclePhase: node.lifecyclePhase,
425
+ viewElementIds: resolveViewElementIds(node),
426
+ eventConfig: node.eventConfig,
427
+ })),
428
+ blueprintGraph.value.document.edges,
429
+ EVENT_NODE_TYPE,
430
+ LIFECYCLE_NODE_TYPE,
431
+ firedLifecyclePhases.value
432
+ )
433
+ );
434
+
415
435
  const blueprintNodeOptions = computed(() =>
416
436
  blueprintGraph.value.document.nodes.map((node) => ({
417
437
  id: node.id,
@@ -419,19 +439,21 @@ const blueprintNodeOptions = computed(() =>
419
439
  }))
420
440
  );
421
441
  async function loadBlueprintFromLibrary(id: string) {
422
- const record = await getBlueprintLibraryRecord(id);
423
- if (!record) {
424
- message.error(panelMessages().blueprintNotFound);
425
- void refreshBlueprintLibrary();
426
- return;
427
- }
428
- blueprintGraph.value = BlueprintGraph.fromDocument(record.document);
429
- blueprintMeta.value = { name: record.name, remark: record.remark ?? "" };
430
- activeBlueprintLibraryId.value = record.id;
431
- blueprintSyncedDocument.value = record.document;
432
- selectedBlueprintNodeId.value = null;
433
- configFocus.value = "blueprint";
434
- blueprintOpen.value = true;
442
+ await runBusyTask(t("common.loadingBlueprint"), async () => {
443
+ const record = await getBlueprintLibraryRecord(id);
444
+ if (!record) {
445
+ message.error(panelMessages().blueprintNotFound);
446
+ void refreshBlueprintLibrary();
447
+ return;
448
+ }
449
+ blueprintGraph.value = BlueprintGraph.fromDocument(record.document);
450
+ blueprintMeta.value = { name: record.name, remark: record.remark ?? "" };
451
+ activeBlueprintLibraryId.value = record.id;
452
+ blueprintSyncedDocument.value = record.document;
453
+ selectedBlueprintNodeId.value = null;
454
+ configFocus.value = "blueprint";
455
+ blueprintOpen.value = true;
456
+ });
435
457
  }
436
458
 
437
459
  function snapshotWorkspaceBlueprint() {
@@ -462,74 +484,97 @@ async function handleSelectBlueprintLibraryItem(id: string) {
462
484
  }
463
485
 
464
486
  async function syncBlueprintToLibrary() {
465
- if (!activeBlueprintLibraryId.value) return;
466
- const record = await getBlueprintLibraryRecord(activeBlueprintLibraryId.value);
467
- if (!record) return;
468
- await putBlueprintLibraryRecord({
469
- ...record,
470
- document: blueprintGraph.value.document,
471
- name: blueprintMeta.value.name,
472
- remark: blueprintMeta.value.remark,
487
+ const libraryId = activeBlueprintLibraryId.value;
488
+ if (!libraryId) return;
489
+ await runBusyTask(t("common.syncingBlueprint"), async () => {
490
+ const record = await getBlueprintLibraryRecord(libraryId);
491
+ if (!record) return;
492
+ await putBlueprintLibraryRecord({
493
+ ...record,
494
+ document: blueprintGraph.value.document,
495
+ name: blueprintMeta.value.name,
496
+ remark: blueprintMeta.value.remark,
497
+ });
498
+ blueprintSyncedDocument.value = blueprintGraph.value.document;
499
+ message.success(panelMessages().blueprintSyncedToLibrary);
500
+ await refreshBlueprintLibrary();
473
501
  });
474
- blueprintSyncedDocument.value = blueprintGraph.value.document;
475
- message.success(panelMessages().blueprintSyncedToLibrary);
476
- await refreshBlueprintLibrary();
477
502
  }
478
503
 
479
504
  async function saveBlueprintToLibrary(meta: BlueprintMetaDraft) {
480
- const id = activeBlueprintLibraryId.value ?? createLibraryBlueprintId();
481
- const record = buildLibraryRecord({
482
- id,
483
- document: blueprintGraph.value.document,
484
- meta,
485
- source: "saved",
505
+ await runBusyTask(t("common.savingBlueprint"), async () => {
506
+ const id = activeBlueprintLibraryId.value ?? createLibraryBlueprintId();
507
+ const record = buildLibraryRecord({
508
+ id,
509
+ document: blueprintGraph.value.document,
510
+ meta,
511
+ source: "saved",
512
+ });
513
+ await putBlueprintLibraryRecord(record);
514
+ activeBlueprintLibraryId.value = id;
515
+ blueprintSyncedDocument.value = blueprintGraph.value.document;
516
+ blueprintMeta.value = meta;
517
+ await refreshBlueprintLibrary();
518
+ message.success(panelMessages().blueprintSavedToLibrary);
486
519
  });
487
- await putBlueprintLibraryRecord(record);
488
- activeBlueprintLibraryId.value = id;
489
- blueprintSyncedDocument.value = blueprintGraph.value.document;
490
- blueprintMeta.value = meta;
491
- await refreshBlueprintLibrary();
492
- message.success(panelMessages().blueprintSavedToLibrary);
493
520
  }
494
521
 
495
522
  function handleExport() {
496
- const data = exportPanelData();
497
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
498
- const url = URL.createObjectURL(blob);
499
- const a = document.createElement("a");
500
- a.href = url;
501
- const safeName = (productName.value.trim() || "panel")
502
- .replace(/[\\/:*?"<>|]/g, "-")
503
- .replace(/\s+/g, "-");
504
- a.download = `${safeName}-${Date.now()}.json`;
505
- a.click();
506
- URL.revokeObjectURL(url);
523
+ void runBusyTask(t("common.exportingPanel"), async () => {
524
+ const data = exportPanelData();
525
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
526
+ const url = URL.createObjectURL(blob);
527
+ const a = document.createElement("a");
528
+ a.href = url;
529
+ const safeName = (productName.value.trim() || "panel")
530
+ .replace(/[\\/:*?"<>|]/g, "-")
531
+ .replace(/\s+/g, "-");
532
+ a.download = `${safeName}-${Date.now()}.json`;
533
+ a.click();
534
+ URL.revokeObjectURL(url);
535
+ });
507
536
  }
508
537
 
509
538
  async function handleImportFile(file: File) {
510
- const text = await file.text();
511
- const parsed = JSON.parse(text) as State;
512
- const ok = importPanelData(parsed);
513
- if (!ok) window.alert(panelMessages().importInvalidFormat);
514
- else selectedIds.value = [];
539
+ try {
540
+ await runBusyTask(t("common.importingPanel"), async () => {
541
+ assertFileSize(file, "json");
542
+ const text = await file.text();
543
+ const parsed = await parseJsonText<State>(text);
544
+ const ok = importPanelData(parsed);
545
+ if (!ok) window.alert(panelMessages().importInvalidFormat);
546
+ else selectedIds.value = [];
547
+ });
548
+ } catch (error) {
549
+ message.error(error instanceof Error ? error.message : panelMessages().importInvalidFormat);
550
+ }
515
551
  }
516
552
 
517
553
  function openBlueprintExport() {
518
- downloadBlueprintExport(
519
- buildBlueprintExportPayload(blueprintGraph.value.document, blueprintMeta.value)
520
- );
521
- message.success(panelMessages().blueprintExported);
554
+ void runBusyTask(t("common.exportingPanel"), async () => {
555
+ downloadBlueprintExport(
556
+ buildBlueprintExportPayload(blueprintGraph.value.document, blueprintMeta.value)
557
+ );
558
+ message.success(panelMessages().blueprintExported);
559
+ });
522
560
  }
523
561
 
524
562
  async function handleBlueprintImportFile(file: File) {
525
- const text = await file.text();
526
- const payload = parseBlueprintImportFile(JSON.parse(text) as unknown);
527
- const record = libraryRecordFromImport(payload);
528
- await putBlueprintLibraryRecord(record);
529
- await refreshBlueprintLibrary();
530
- if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
531
- await loadBlueprintFromLibrary(record.id);
532
- message.success(panelMessages().blueprintImported);
563
+ try {
564
+ await runBusyTask(t("common.importingBlueprint"), async () => {
565
+ assertFileSize(file, "json");
566
+ const text = await file.text();
567
+ const payload = parseBlueprintImportFile(await parseJsonText(text));
568
+ const record = libraryRecordFromImport(payload);
569
+ await putBlueprintLibraryRecord(record);
570
+ await refreshBlueprintLibrary();
571
+ if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
572
+ await loadBlueprintFromLibrary(record.id);
573
+ message.success(panelMessages().blueprintImported);
574
+ });
575
+ } catch (error) {
576
+ message.error(error instanceof Error ? error.message : panelMessages().importInvalidFormat);
577
+ }
533
578
  }
534
579
 
535
580
  async function handleWorkspaceCreateProject() {
@@ -539,6 +584,7 @@ async function handleWorkspaceCreateProject() {
539
584
 
540
585
  async function handleWorkspaceOpenProject(id: string) {
541
586
  try {
587
+ stopAllClockSchedules();
542
588
  clearViewElementScopes();
543
589
  await workspaceProjects.handleOpenProject(id);
544
590
  selectedIds.value = [];
@@ -662,6 +708,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
662
708
  :active-project-id="workspaceProjects.activeProjectId.value"
663
709
  :active-project-name="workspaceProjects.activeProjectName.value"
664
710
  :dirty="workspaceProjects.dirty.value"
711
+ :previewing-project-ids="workspaceProjects.previewingProjectIds.value"
665
712
  @create-project="handleWorkspaceCreateProject"
666
713
  @open-project="handleWorkspaceOpenProject"
667
714
  @sync-project="handleWorkspaceSyncProject"
@@ -763,6 +810,12 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
763
810
  void triggerBlueprintNode(payload.blueprintNodeId, payload);
764
811
  }
765
812
  "
813
+ :bound-view-event-types="boundViewEventTypes"
814
+ :on-view-ui-event="
815
+ (payload) => {
816
+ void emitViewEvent(payload);
817
+ }
818
+ "
766
819
  @select-ids="selectIds"
767
820
  />
768
821
  <SelectLayer
@@ -863,6 +916,9 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
863
916
  :on-exclude-selected-node="excludeSelectedNode"
864
917
  :on-adjust-node-z-order="adjustNodeZOrder"
865
918
  :selected-blueprint-node="selectedBlueprintNode"
919
+ :blueprint-graph-nodes="blueprintGraph.document.nodes"
920
+ :blueprint-graph-edges="blueprintGraph.document.edges"
921
+ :blueprint-trace-entries="blueprintDebugSession.entries.value"
866
922
  :allow-false-signal-propagation="blueprintGraph.document.allowFalseSignalPropagation"
867
923
  :blueprint-library-options="blueprintLibraryOptions"
868
924
  :blueprint-node-options="blueprintNodeOptions"
@@ -873,6 +929,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
873
929
  </Layout.Sider>
874
930
  </Layout>
875
931
 
932
+ <BusyOverlay />
876
933
  <input ref="importInputRef" type="file" accept="application/json" class="hidden" @change="async (e) => { const f = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; if (f) await handleImportFile(f); }" />
877
934
  <input ref="blueprintImportInputRef" type="file" accept="application/json" class="hidden" @change="async (e) => { const f = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; if (f) await handleBlueprintImportFile(f); }" />
878
935
 
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import { busyOverlayMessage } from "../utils/async-work";
4
+
5
+ const { t } = useI18n();
6
+ </script>
7
+
8
+ <template>
9
+ <Teleport to="body">
10
+ <div
11
+ v-if="busyOverlayMessage"
12
+ class="fixed inset-0 flex flex-col items-center justify-center gap-3 bg-black/45 px-6"
13
+ style="z-index: 40000"
14
+ role="status"
15
+ aria-live="polite"
16
+ aria-busy="true"
17
+ >
18
+ <div class="h-9 w-9 animate-spin rounded-full border-2 border-white/30 border-t-white" />
19
+ <div class="max-w-sm text-center text-sm text-white">
20
+ {{ busyOverlayMessage || t("common.loading") }}
21
+ </div>
22
+ </div>
23
+ </Teleport>
24
+ </template>
@@ -1,16 +1,16 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
2
3
  import { useI18n } from "@arronqzy/i18n/vue";
3
4
  import { Tooltip } from "ant-design-vue";
4
5
 
5
- const { t, locale } = useI18n();
6
- withDefaults(
7
- defineProps<{
8
- label?: string;
9
- contentClass?: string;
10
- buttonClass?: string;
11
- }>(),
12
- { label: t("common.hint") }
13
- );
6
+ const { t } = useI18n();
7
+ const props = defineProps<{
8
+ label?: string;
9
+ contentClass?: string;
10
+ buttonClass?: string;
11
+ }>();
12
+
13
+ const resolvedLabel = computed(() => props.label || t("common.hint"));
14
14
  </script>
15
15
 
16
16
  <template>
@@ -28,7 +28,7 @@ withDefaults(
28
28
  buttonClass ??
29
29
  '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'
30
30
  "
31
- :aria-label="t('common.hintAria', { label })"
31
+ :aria-label="t('common.hintAria', { label: resolvedLabel })"
32
32
  >
33
33
  ?
34
34
  </button>
@@ -15,7 +15,17 @@ import {
15
15
  VideoNodeContent,
16
16
  } from "./elementsLayerNodes";
17
17
  import TableNodeContent from "./table/TableNodeContent.vue";
18
+ import Scene3dNodeContent from "@arronqzy/view-scene3d/vue";
19
+ import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
18
20
  import type { TableCellActionHandler } from "./table/TableNodeContent.vue";
21
+ import { comparePanelElementsPaintOrder } from "../utils/gridPlacement";
22
+ import {
23
+ createViewEventSignal,
24
+ snapshotDomEvent,
25
+ snapshotViewEventNode,
26
+ type ViewEventSignal,
27
+ type ViewEventType,
28
+ } from "@arronqzy/blueprint-dsl";
19
29
 
20
30
  const props = withDefaults(
21
31
  defineProps<{
@@ -31,6 +41,8 @@ const props = withDefaults(
31
41
  previewMode?: boolean;
32
42
  previewLayoutKey?: number;
33
43
  onTableCellAction?: TableCellActionHandler;
44
+ boundViewEventTypes?: ReadonlyMap<string, ReadonlySet<ViewEventType>>;
45
+ onViewUiEvent?: (payload: ViewEventSignal) => void;
34
46
  }>(),
35
47
  { layerLocked: false, previewMode: false }
36
48
  );
@@ -104,6 +116,17 @@ const NodeContent = defineComponent({
104
116
  if (el.materialType === "geometry") {
105
117
  return h(GeometryNodeContent, { element: el });
106
118
  }
119
+ if (el.materialType === "scene3d") {
120
+ return h(Scene3dNodeContent, {
121
+ config: el.scene3d,
122
+ previewMode: p.previewMode,
123
+ selected: p.selected,
124
+ onUpdateConfig: (patch) =>
125
+ p.updateElement(el.id, {
126
+ scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
127
+ }),
128
+ });
129
+ }
107
130
  if (el.materialType === "image") {
108
131
  return h(ImageNodeContent, { element: el });
109
132
  }
@@ -119,14 +142,11 @@ const NodeContent = defineComponent({
119
142
  },
120
143
  });
121
144
 
122
- const sortedElements = computed(() =>
123
- [...props.elements].sort((a, b) => {
124
- const za = a.zIndex ?? 1;
125
- const zb = b.zIndex ?? 1;
126
- if (za !== zb) return za - zb;
127
- return a.id.localeCompare(b.id);
128
- })
129
- );
145
+ const sortedElements = computed(() => {
146
+ const byId = new Map<string, PanelElement>();
147
+ for (const el of props.allElements) byId.set(el.id, el);
148
+ return [...props.elements].sort((a, b) => comparePanelElementsPaintOrder(a, b, byId));
149
+ });
130
150
 
131
151
  function onSelect(id: string, event: MouseEvent) {
132
152
  if (props.previewMode) return;
@@ -141,6 +161,23 @@ function onSelect(id: string, event: MouseEvent) {
141
161
  }
142
162
  emit("selectIds", [id]);
143
163
  }
164
+
165
+ function emitViewUiEvent(el: PanelElement, eventType: ViewEventType, event: MouseEvent) {
166
+ if (!props.onViewUiEvent) return;
167
+ if (!props.boundViewEventTypes?.get(el.id)?.has(eventType)) return;
168
+ props.onViewUiEvent(
169
+ createViewEventSignal({
170
+ eventType,
171
+ event: snapshotDomEvent(event),
172
+ node: snapshotViewEventNode(el),
173
+ })
174
+ );
175
+ }
176
+
177
+ function hasViewEvent(el: PanelElement) {
178
+ const types = props.boundViewEventTypes?.get(el.id);
179
+ return Boolean(types && types.size > 0 && props.onViewUiEvent);
180
+ }
144
181
  </script>
145
182
 
146
183
  <template>
@@ -151,6 +188,7 @@ function onSelect(id: string, event: MouseEvent) {
151
188
  'absolute select-none',
152
189
  previewMode ? '' : 'rv-selectable',
153
190
  !previewMode && selectedIds.includes(el.id) ? 'ring-2 ring-blue-500/90 ring-offset-0' : '',
191
+ hasViewEvent(el) ? 'cursor-pointer' : '',
154
192
  ]"
155
193
  :data-element-id="el.id"
156
194
  :style="{
@@ -165,6 +203,11 @@ function onSelect(id: string, event: MouseEvent) {
165
203
  ...getNodeVisualStyle(el),
166
204
  }"
167
205
  @mousedown="onSelect(el.id, $event)"
206
+ @click="emitViewUiEvent(el, 'click', $event)"
207
+ @dblclick="emitViewUiEvent(el, 'dblclick', $event)"
208
+ @contextmenu="emitViewUiEvent(el, 'contextmenu', $event)"
209
+ @mouseenter="emitViewUiEvent(el, 'mouseenter', $event)"
210
+ @mouseleave="emitViewUiEvent(el, 'mouseleave', $event)"
168
211
  >
169
212
  <NodeContent
170
213
  :element="el"
@@ -106,6 +106,14 @@ const common =
106
106
  <polygon points="50,36 54,42 50,48 42,48 38,42 42,36" class="fill-primary/70" />
107
107
  </svg>
108
108
  </div>
109
+ <div v-else-if="id === 'scene3d'" :class="common">
110
+ <svg viewBox="0 0 80 60" class="h-full w-full">
111
+ <polygon points="40,8 62,22 62,44 40,56 18,44 18,22" class="fill-primary/20 stroke-primary/80" stroke-width="1.5" />
112
+ <polygon points="40,8 62,22 40,32 18,22" class="fill-primary/45" />
113
+ <polygon points="40,32 62,22 62,44 40,56" class="fill-primary/30" />
114
+ <polygon points="40,32 18,22 18,44 40,56" class="fill-primary/55" />
115
+ </svg>
116
+ </div>
109
117
  <div v-else-if="id === 'reference'" :class="common">
110
118
  <div class="absolute inset-2 rounded-md border border-dashed border-primary/70 bg-primary/10" />
111
119
  <div class="absolute inset-0 flex items-center justify-center text-[10px] text-primary/80">