@arronqzy/vue-view 0.1.9 → 0.1.11

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 (29) hide show
  1. package/README.md +15 -1
  2. package/package.json +8 -8
  3. package/src/index.ts +26 -0
  4. package/src/panel/VueViewOnlinePreview.vue +68 -26
  5. package/src/panel/VueViewPanel.vue +15 -10
  6. package/src/panel/components/ElementsLayer.vue +4 -2
  7. package/src/panel/components/MaterialPreview.vue +8 -0
  8. package/src/panel/components/MaterialSidebar.vue +15 -18
  9. package/src/panel/components/MaterialSidebarTreeNode.vue +4 -2
  10. package/src/panel/components/PanelConfigSidebar.vue +17 -1
  11. package/src/panel/components/WorkspaceConfigSidebar.vue +1 -0
  12. package/src/panel/components/config/PanelConfigMultiSelect.vue +30 -1
  13. package/src/panel/components/config/PanelConfigViewportSection.vue +114 -0
  14. package/src/panel/components/elementsLayerNodes.ts +75 -17
  15. package/src/panel/components/materialSidebarData.ts +2 -0
  16. package/src/panel/hooks/usePanelElements.ts +10 -4
  17. package/src/panel/hooks/useWorkspaceProjects.ts +144 -34
  18. package/src/panel/library/event-subscription.ts +60 -0
  19. package/src/panel/library/parse-workspace-data.ts +142 -0
  20. package/src/panel/library/workspace-project-cache.ts +13 -4
  21. package/src/panel/library/workspace-project-db.ts +24 -13
  22. package/src/panel/library/workspace-project-sync.ts +17 -6
  23. package/src/panel/parseOnlinePreviewSearchParams.ts +3 -0
  24. package/src/panel/types.ts +4 -0
  25. package/src/panel/utils/outputScale.ts +15 -2
  26. package/src/panel/utils/panelElementDefaults.ts +10 -0
  27. package/src/panel/utils/panelElementNodes.ts +5 -3
  28. package/src/panel/utils/panelStateIO.ts +18 -0
  29. package/src/panel/utils/viewportPlacement.ts +73 -0
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import { Select } from "ant-design-vue";
4
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
5
+ import {
6
+ VIEWPORT_OVERFLOW_MODES,
7
+ normalizeViewportOverflow,
8
+ type ViewportOverflowMode,
9
+ } from "../../utils/viewportPlacement";
10
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
11
+ import ConfigSection from "./ConfigSection.vue";
12
+
13
+ const { t } = useI18n();
14
+ const props = defineProps<{
15
+ element: PanelElement;
16
+ layers: PanelLayer[];
17
+ isEditable: boolean;
18
+ open: boolean;
19
+ forceOpen?: boolean;
20
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
21
+ updateElement: (
22
+ id: string,
23
+ patch: Partial<PanelElement>,
24
+ options?: { batchId?: string; meta?: Record<string, unknown> }
25
+ ) => void;
26
+ }>();
27
+
28
+ const emit = defineEmits<{
29
+ "update:open": [value: boolean];
30
+ }>();
31
+
32
+ const overflowLabelKey: Record<ViewportOverflowMode, string> = {
33
+ "scroll-x": "panel.config.viewportOverflowScrollX",
34
+ "scroll-y": "panel.config.viewportOverflowScrollY",
35
+ scroll: "panel.config.viewportOverflowScroll",
36
+ clip: "panel.config.viewportOverflowClip",
37
+ };
38
+
39
+ function patch(next: Partial<PanelElement>) {
40
+ props.updateElement(props.element.id, next);
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <ConfigSection
46
+ :title="t('panel.config.sectionViewport')"
47
+ :open="open"
48
+ :force-open="forceOpen"
49
+ @update:open="emit('update:open', $event)"
50
+ >
51
+ <template #hint>
52
+ {{ t("panel.config.viewportHint") }}
53
+ </template>
54
+ <ConfigFieldGroup :title="t('panel.config.groupRefSource')">
55
+ <label class="block space-y-1">
56
+ <div>{{ t("panel.config.refLayer") }}</div>
57
+ <Select
58
+ size="small"
59
+ class="w-full"
60
+ :value="element.refLayerId ?? '__none__'"
61
+ :disabled="!isEditable"
62
+ @update:value="(v) => patch({ refLayerId: v === '__none__' ? undefined : String(v) })"
63
+ >
64
+ <Select.Option value="__none__">{{ t("panel.config.noneNoRef") }}</Select.Option>
65
+ <Select.Option
66
+ v-for="layer in layers.filter((l) => l.id !== element.layerId)"
67
+ :key="layer.id"
68
+ :value="layer.id"
69
+ >
70
+ {{ layer.name }}
71
+ </Select.Option>
72
+ </Select>
73
+ </label>
74
+ </ConfigFieldGroup>
75
+ <ConfigFieldGroup
76
+ :title="t('panel.config.groupCopyStrategy')"
77
+ collapsible
78
+ :default-open="false"
79
+ >
80
+ <template #hint>
81
+ {{ t("panel.config.copyStrategyHint") }}
82
+ </template>
83
+ <label class="block space-y-1">
84
+ <div>{{ t("panel.config.copyMode") }}</div>
85
+ <Select
86
+ size="small"
87
+ class="w-full"
88
+ :value="element.refCopyMode ?? 'shallow'"
89
+ :disabled="!isEditable"
90
+ @update:value="(v) => setReferenceCopyMode?.(element.id, v as ReferenceCopyMode)"
91
+ >
92
+ <Select.Option value="shallow">{{ t("panel.config.shallowFollow") }}</Select.Option>
93
+ <Select.Option value="deep">{{ t("panel.config.deepFreeze") }}</Select.Option>
94
+ </Select>
95
+ </label>
96
+ </ConfigFieldGroup>
97
+ <ConfigFieldGroup :title="t('panel.config.groupViewportOverflow')">
98
+ <label class="block space-y-1">
99
+ <div>{{ t("panel.config.viewportOverflow") }}</div>
100
+ <Select
101
+ size="small"
102
+ class="w-full"
103
+ :value="normalizeViewportOverflow(element.viewportOverflow)"
104
+ :disabled="!isEditable"
105
+ @update:value="(v) => patch({ viewportOverflow: v as ViewportOverflowMode })"
106
+ >
107
+ <Select.Option v-for="mode in VIEWPORT_OVERFLOW_MODES" :key="mode" :value="mode">
108
+ {{ t(overflowLabelKey[mode]) }}
109
+ </Select.Option>
110
+ </Select>
111
+ </label>
112
+ </ConfigFieldGroup>
113
+ </ConfigSection>
114
+ </template>
@@ -16,6 +16,12 @@ import type { PanelElement } from "../types";
16
16
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
17
17
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
18
18
  import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
19
+ import {
20
+ isViewportNode,
21
+ normalizeViewportOverflow,
22
+ viewportOverflowStyle,
23
+ viewportWindowContentScale,
24
+ } from "../utils/viewportPlacement";
19
25
  import TableNodeContent from "./table/TableNodeContent.vue";
20
26
  import Scene3dNodeContent from "@arronqzy/view-scene3d/vue";
21
27
 
@@ -712,15 +718,25 @@ export const ReferenceNodeContent = defineComponent({
712
718
  const sourceHeight = Math.max(1, maxY - minY);
713
719
  const innerW = Math.max(1, p.element.width);
714
720
  const innerH = Math.max(1, p.element.height);
715
- const scale = Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
716
- const mappedW = sourceWidth * scale;
717
- const mappedH = sourceHeight * scale;
721
+ const windowFit = isViewportNode(p.element);
722
+ const windowScale = windowFit
723
+ ? viewportWindowContentScale(p.element, p.allElements)
724
+ : { scaleX: 1, scaleY: 1 };
725
+ const scaleX = windowFit
726
+ ? windowScale.scaleX
727
+ : Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
728
+ const scaleY = windowFit ? windowScale.scaleY : scaleX;
729
+ const mappedW = sourceWidth * scaleX;
730
+ const mappedH = sourceHeight * scaleY;
718
731
  return {
719
732
  minX,
720
733
  minY,
721
- scale,
722
- offsetX: (innerW - mappedW) / 2,
723
- offsetY: (innerH - mappedH) / 2,
734
+ scaleX,
735
+ scaleY,
736
+ sourceWidth: mappedW,
737
+ sourceHeight: mappedH,
738
+ offsetX: windowFit ? 0 : (innerW - mappedW) / 2,
739
+ offsetY: windowFit ? 0 : (innerH - mappedH) / 2,
724
740
  };
725
741
  });
726
742
 
@@ -739,20 +755,27 @@ export const ReferenceNodeContent = defineComponent({
739
755
  ]);
740
756
  }
741
757
 
742
- return h("div", { class: "pointer-events-none relative h-full w-full overflow-hidden" }, [
743
- ...sourceNodes.value.map((node) => {
758
+ const overflowCss = isViewportNode(p.element)
759
+ ? viewportOverflowStyle(normalizeViewportOverflow(p.element.viewportOverflow))
760
+ : { overflowX: "hidden" as const, overflowY: "hidden" as const };
761
+ const canScroll =
762
+ isViewportNode(p.element) &&
763
+ (overflowCss.overflowX === "auto" || overflowCss.overflowY === "auto");
764
+
765
+ const innerNodes = sourceNodes.value.map((node) => {
744
766
  const box = getNodeAABB(node);
745
- const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.scale;
746
- const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.scale;
747
- const width = Math.max(12, node.width * layout.value!.scale);
748
- const height = Math.max(10, node.height * layout.value!.scale);
749
- const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.scale);
750
- const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.scale);
767
+ const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.scaleX;
768
+ const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.scaleY;
769
+ const width = Math.max(12, node.width * layout.value!.scaleX);
770
+ const height = Math.max(10, node.height * layout.value!.scaleY);
771
+ const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.scaleX);
772
+ const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.scaleY);
751
773
  return h(
752
774
  "div",
753
775
  {
754
776
  key: node.id,
755
777
  class: "absolute overflow-visible",
778
+ "data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
756
779
  style: { left, top, width: boxWidth, height: boxHeight },
757
780
  },
758
781
  [
@@ -760,6 +783,7 @@ export const ReferenceNodeContent = defineComponent({
760
783
  "div",
761
784
  {
762
785
  class: "absolute",
786
+ "data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
763
787
  style: {
764
788
  left: (boxWidth - width) / 2,
765
789
  top: (boxHeight - height) / 2,
@@ -777,7 +801,7 @@ export const ReferenceNodeContent = defineComponent({
777
801
  previewLayoutKey: p.previewLayoutKey,
778
802
  previewMode: p.previewMode,
779
803
  })
780
- : node.materialType === "reference"
804
+ : node.materialType === "reference" || node.materialType === "viewport"
781
805
  ? h(ReferenceNodeContent as Component, {
782
806
  element: node,
783
807
  allElements: p.allElements,
@@ -800,8 +824,42 @@ export const ReferenceNodeContent = defineComponent({
800
824
  ),
801
825
  ]
802
826
  );
803
- }),
804
- ]);
827
+ });
828
+
829
+ return h(
830
+ "div",
831
+ {
832
+ class: "relative h-full w-full",
833
+ style: {
834
+ overflowX: overflowCss.overflowX,
835
+ overflowY: overflowCss.overflowY,
836
+ pointerEvents: isViewportNode(p.element) ? "auto" : "none",
837
+ },
838
+ onWheel: canScroll
839
+ ? (e: WheelEvent) => {
840
+ e.stopPropagation();
841
+ }
842
+ : undefined,
843
+ },
844
+ [
845
+ h(
846
+ "div",
847
+ {
848
+ class: "pointer-events-none relative",
849
+ "data-viewport-sizer": isViewportNode(p.element) ? "true" : undefined,
850
+ style: isViewportNode(p.element)
851
+ ? {
852
+ width: layout.value.sourceWidth,
853
+ height: layout.value.sourceHeight,
854
+ minWidth: "100%",
855
+ minHeight: "100%",
856
+ }
857
+ : { width: "100%", height: "100%" },
858
+ },
859
+ innerNodes
860
+ ),
861
+ ]
862
+ );
805
863
  };
806
864
  },
807
865
  }) as Component;
@@ -26,6 +26,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
26
26
  text: "panel.material.text",
27
27
  rect: "panel.material.rect",
28
28
  grid: "panel.material.grid",
29
+ viewport: "panel.material.viewport",
29
30
  image: "panel.material.image",
30
31
  video: "panel.material.video",
31
32
  audio: "panel.material.audio",
@@ -68,6 +69,7 @@ export function getDefaultCategories(t: TranslateFn): MaterialCategory[] {
68
69
  { id: "geometry", title: t("panel.material.geometry") },
69
70
  { id: "scene3d", title: t("panel.material.scene3d") },
70
71
  { id: "grid", title: t("panel.material.grid") },
72
+ { id: "viewport", title: t("panel.material.viewport") },
71
73
  { id: "image", title: t("panel.material.image") },
72
74
  { id: "reference", title: t("panel.material.reference") },
73
75
  ],
@@ -16,6 +16,7 @@ import {
16
16
  DEFAULT_LAYER_ID,
17
17
  getDefaultChartConfig,
18
18
  getDefaultGridConfig,
19
+ getDefaultViewportConfig,
19
20
  getDefaultNodeName,
20
21
  getDefaultScene3dConfigForMaterial,
21
22
  getDefaultSizeByMaterial,
@@ -569,7 +570,10 @@ export function usePanelElements() {
569
570
  .filter((n) => isPanelElementNode(n) && n.props)
570
571
  .map((n) => n.props as PanelElement);
571
572
  const desiredMode = options?.referenceCopyMode;
572
- if (nextProps.materialType === "reference" && desiredMode) {
573
+ if (
574
+ (nextProps.materialType === "reference" || nextProps.materialType === "viewport") &&
575
+ desiredMode
576
+ ) {
573
577
  if (desiredMode === "deep" && nextProps.refLayerId) {
574
578
  nextProps.refCopyMode = "deep";
575
579
  nextProps.refSnapshot = buildDeepReferenceSnapshot(all, nextProps.refLayerId);
@@ -611,7 +615,9 @@ export function usePanelElements() {
611
615
  name: getDefaultNodeName(materialType, t),
612
616
  ...getDefaultTextContent(materialType, t),
613
617
  ...getDefaultGridConfig(materialType),
614
- refCopyMode: materialType === "reference" ? "shallow" : undefined,
618
+ ...getDefaultViewportConfig(materialType),
619
+ refCopyMode:
620
+ materialType === "reference" || materialType === "viewport" ? "shallow" : undefined,
615
621
  chart: getDefaultChartConfig(materialType, t),
616
622
  table: getDefaultTableConfig(materialType, t),
617
623
  geometryShape: materialType === "geometry" ? "rect" : undefined,
@@ -712,7 +718,7 @@ export function usePanelElements() {
712
718
  .filter((n) => isPanelElementNode(n) && n.props)
713
719
  .map((n) => n.props as PanelElement);
714
720
  const target = all.find((el) => el.id === id);
715
- if (!target || target.materialType !== "reference") return;
721
+ if (!target || (target.materialType !== "reference" && target.materialType !== "viewport")) return;
716
722
  const currentLayers =
717
723
  (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
718
724
  const layer = currentLayers.find((l) => l.id === target.layerId);
@@ -904,7 +910,7 @@ export function usePanelElements() {
904
910
  if (target.isMapping) return false;
905
911
  const willBeDeleted = mode === "remove" && el.layerId === layerId;
906
912
  if (willBeDeleted) return false;
907
- if (el.materialType !== "reference") return false;
913
+ if (el.materialType !== "reference" && el.materialType !== "viewport") return false;
908
914
  if (el.refLayerId !== layerId) return false;
909
915
  return (el.refCopyMode ?? "shallow") !== "deep";
910
916
  });
@@ -1,6 +1,7 @@
1
1
  import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, type MaybeRefOrGetter } from "vue";
2
2
  import {
3
3
  BlueprintGraph,
4
+ parseBlueprintData,
4
5
  type BlueprintDocument,
5
6
  type BlueprintMetaDraft,
6
7
  } from "@arronqzy/vue-blueprint";
@@ -23,6 +24,8 @@ import {
23
24
  type WorkspaceSnapshot,
24
25
  } from "../library/workspace-snapshot";
25
26
  import { runBusyTask } from "../utils/async-work";
27
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
28
+ import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
26
29
 
27
30
  export type UseWorkspaceProjectsOptions = {
28
31
  exportPanelData: () => State;
@@ -39,13 +42,21 @@ export type UseWorkspaceProjectsOptions = {
39
42
  panelRevision: MaybeRefOrGetter<unknown>;
40
43
  /** 工作区记录应用到编辑器后的回调(用于恢复蓝图面板等 UI 状态) */
41
44
  onProjectApplied?: (record: WorkspaceProjectRecord) => void;
45
+ /** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
46
+ initialWorkspace?: MaybeRefOrGetter<WorkspaceProjectRecord | null | undefined>;
47
+ nameSpace?: MaybeRefOrGetter<string | null | undefined>;
42
48
  };
43
49
 
44
- export function buildOnlinePreviewUrl(projectId: string): string {
50
+ export function buildOnlinePreviewUrl(
51
+ projectId: string,
52
+ nameSpace?: string | null
53
+ ): string {
45
54
  const url = new URL(window.location.href);
46
55
  url.search = "";
47
56
  url.searchParams.set("preview", "online");
48
57
  url.searchParams.set("projectId", projectId);
58
+ const ns = (nameSpace ?? "").trim();
59
+ if (ns) url.searchParams.set("ns", ns);
49
60
  const previewInstanceId =
50
61
  typeof crypto !== "undefined" && "randomUUID" in crypto
51
62
  ? crypto.randomUUID()
@@ -54,8 +65,31 @@ export function buildOnlinePreviewUrl(projectId: string): string {
54
65
  return url.toString();
55
66
  }
56
67
 
68
+ function previewWindowName(projectId: string, nameSpace?: string | null): string {
69
+ const ns = (nameSpace ?? "").trim();
70
+ return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
71
+ }
72
+
73
+ function hasWorkspacePayload(
74
+ record?: WorkspaceProjectRecord | null
75
+ ): record is WorkspaceProjectRecord {
76
+ if (!record || typeof record !== "object") return false;
77
+ return Boolean(
78
+ record.panelState ||
79
+ record.blueprintDocument ||
80
+ record.id ||
81
+ record.productName ||
82
+ record.titleIconDataUrl
83
+ );
84
+ }
85
+
86
+ function workspaceApplyKey(record: WorkspaceProjectRecord): string {
87
+ return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
88
+ }
89
+
57
90
  export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
58
91
  const { t, locale } = useI18nOptional();
92
+ const nameSpace = () => toValue(options.nameSpace) ?? null;
59
93
  const projects = ref<WorkspaceProjectListItem[]>([]);
60
94
  const activeProjectId = ref<string | null>(null);
61
95
  const activeProjectName = ref<string | null>(null);
@@ -77,7 +111,8 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
77
111
 
78
112
  function resolveProjectName() {
79
113
  void locale.value;
80
- return toValue(options.productName).trim() || t("panel.defaults.unnamedProduct");
114
+ const name = toValue(options.productName);
115
+ return (typeof name === "string" ? name : "").trim() || t("panel.defaults.unnamedProduct");
81
116
  }
82
117
 
83
118
  function buildCurrentSnapshot(): WorkspaceSnapshot {
@@ -90,13 +125,15 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
90
125
  }
91
126
 
92
127
  async function refreshProjects() {
93
- projects.value = await listWorkspaceProjects();
128
+ projects.value = await listWorkspaceProjects(nameSpace());
94
129
  }
95
130
 
96
- onMounted(() => {
97
- void refreshProjects();
98
- previewPollTimer = window.setInterval(refreshPreviewing, 2000);
99
- });
131
+ watch(
132
+ () => nameSpace(),
133
+ () => {
134
+ void refreshProjects();
135
+ }
136
+ );
100
137
 
101
138
  onUnmounted(() => {
102
139
  if (previewPollTimer !== undefined) window.clearInterval(previewPollTimer);
@@ -122,36 +159,104 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
122
159
  return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.value);
123
160
  });
124
161
 
125
- function applyProjectRecord(record: WorkspaceProjectRecord) {
162
+ function applyProjectRecord(
163
+ record: WorkspaceProjectRecord,
164
+ applyOptions?: { notifyApplied?: boolean }
165
+ ) {
166
+ const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
126
167
  const blueprintDocument =
127
- record.blueprintDocument ?? BlueprintGraph.empty().document;
168
+ parsedBlueprint.value ?? BlueprintGraph.empty().document;
128
169
  const blueprintMeta: BlueprintMetaDraft = {
129
170
  name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
130
171
  remark: record.blueprintMeta?.remark ?? "",
131
172
  };
173
+ const panelState =
174
+ normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
175
+ const productName =
176
+ typeof record.productName === "string" && record.productName.trim()
177
+ ? record.productName
178
+ : t("panel.defaults.unnamedProduct");
179
+ const titleIconDataUrl = record.titleIconDataUrl ?? "";
132
180
 
133
- options.importPanelData(record.panelState);
181
+ options.importPanelData(panelState);
134
182
  options.setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
135
183
  options.setBlueprintMeta(blueprintMeta);
136
- options.setProductName(record.productName);
137
- options.setTitleIconDataUrl(record.titleIconDataUrl ?? "");
138
- activeProjectId.value = record.id;
139
- activeProjectName.value = record.name;
184
+ options.setProductName(productName);
185
+ options.setTitleIconDataUrl(titleIconDataUrl);
186
+ activeProjectId.value = record.id || null;
187
+ activeProjectName.value = record.name ?? null;
140
188
  syncedSnapshotRef.value = cloneWorkspaceSnapshot({
141
189
  blueprintDocument,
142
190
  blueprintMeta,
143
- productName: record.productName,
144
- titleIconDataUrl: record.titleIconDataUrl ?? "",
191
+ productName,
192
+ titleIconDataUrl,
145
193
  });
146
194
  syncedPanelRevision = toValue(options.panelRevision);
147
195
  pendingRevisionSync = true;
148
- options.onProjectApplied?.({
149
- ...record,
150
- blueprintDocument,
151
- blueprintMeta,
196
+ if (applyOptions?.notifyApplied !== false) {
197
+ options.onProjectApplied?.({
198
+ ...record,
199
+ blueprintDocument,
200
+ blueprintMeta,
201
+ panelState,
202
+ productName,
203
+ titleIconDataUrl: titleIconDataUrl || undefined,
204
+ });
205
+ }
206
+ }
207
+
208
+ function applyEmptyWorkspace() {
209
+ options.importPanelData(createEmptyPanelState());
210
+ options.setBlueprintGraph(BlueprintGraph.empty());
211
+ options.setBlueprintMeta({
212
+ name: t("panel.defaults.unnamedBlueprint"),
213
+ remark: "",
152
214
  });
215
+ options.setProductName(t("panel.defaults.unnamedProduct"));
216
+ options.setTitleIconDataUrl("");
217
+ activeProjectId.value = null;
218
+ activeProjectName.value = null;
219
+ syncedSnapshotRef.value = null;
153
220
  }
154
221
 
222
+ let bootstrapped = false;
223
+ let appliedWorkspaceKey: string | null = null;
224
+
225
+ function bootstrapInitialWorkspace() {
226
+ const initialWorkspace = toValue(options.initialWorkspace) ?? null;
227
+ if (!bootstrapped) {
228
+ bootstrapped = true;
229
+ if (hasWorkspacePayload(initialWorkspace)) {
230
+ applyProjectRecord(initialWorkspace, { notifyApplied: false });
231
+ appliedWorkspaceKey = workspaceApplyKey(initialWorkspace);
232
+ } else {
233
+ applyEmptyWorkspace();
234
+ }
235
+ return;
236
+ }
237
+ if (!hasWorkspacePayload(initialWorkspace)) return;
238
+ const key = workspaceApplyKey(initialWorkspace);
239
+ if (appliedWorkspaceKey === key) return;
240
+ appliedWorkspaceKey = key;
241
+ applyProjectRecord(initialWorkspace, { notifyApplied: false });
242
+ }
243
+
244
+ onMounted(() => {
245
+ bootstrapInitialWorkspace();
246
+ void refreshProjects();
247
+ previewPollTimer = window.setInterval(refreshPreviewing, 2000);
248
+ });
249
+
250
+ watch(
251
+ () => {
252
+ const record = toValue(options.initialWorkspace);
253
+ return record ? workspaceApplyKey(record) : "";
254
+ },
255
+ () => {
256
+ bootstrapInitialWorkspace();
257
+ }
258
+ );
259
+
155
260
  async function persistProject(persistOptions: {
156
261
  id?: string;
157
262
  name: string;
@@ -161,7 +266,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
161
266
  const now = Date.now();
162
267
  const id = persistOptions.id ?? createWorkspaceProjectId();
163
268
  const createdAt = persistOptions.createdAt ?? now;
164
- const name = persistOptions.name.trim() || resolveProjectName();
269
+ const name = (persistOptions.name ?? "").trim() || resolveProjectName();
165
270
 
166
271
  const record: WorkspaceProjectRecord = {
167
272
  id,
@@ -171,13 +276,14 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
171
276
  panelState: options.exportPanelData(),
172
277
  blueprintDocument: snapshot.blueprintDocument,
173
278
  blueprintMeta: snapshot.blueprintMeta,
174
- productName: snapshot.productName,
279
+ productName:
280
+ typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
175
281
  titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
176
282
  };
177
283
 
178
- await putWorkspaceProject(record);
179
- writeWorkspacePreviewCache(record);
180
- broadcastWorkspaceProjectUpdate(id, now);
284
+ await putWorkspaceProject(record, nameSpace());
285
+ writeWorkspacePreviewCache(record, nameSpace());
286
+ broadcastWorkspaceProjectUpdate(id, now, nameSpace());
181
287
  activeProjectId.value = id;
182
288
  activeProjectName.value = name;
183
289
  syncedSnapshotRef.value = snapshot;
@@ -191,6 +297,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
191
297
  return runBusyTask(t("common.savingWorkspace"), async () => {
192
298
  const name = resolveProjectName();
193
299
  const record = await persistProject({ name });
300
+ emitAbuilderEvent(AbuilderEvents.workspaceAdd, record);
194
301
  return { name, id: record.id };
195
302
  });
196
303
  }
@@ -201,7 +308,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
201
308
  return handleCreateProject();
202
309
  }
203
310
  return runBusyTask(t("common.savingWorkspace"), async () => {
204
- const existing = await getWorkspaceProject(projectId);
311
+ const existing = await getWorkspaceProject(projectId, nameSpace());
205
312
  const name = resolveProjectName();
206
313
  const record = await persistProject({
207
314
  id: projectId,
@@ -214,7 +321,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
214
321
 
215
322
  async function handleOpenProject(id: string) {
216
323
  await runBusyTask(t("common.loadingWorkspace"), async () => {
217
- const record = await getWorkspaceProject(id);
324
+ const record = await getWorkspaceProject(id, nameSpace());
218
325
  if (!record) {
219
326
  await refreshProjects();
220
327
  throw new Error(t("panel.messages.workspaceNotFound"));
@@ -228,7 +335,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
228
335
  if (!activeProjectId.value) {
229
336
  throw new Error(t("panel.messages.saveWorkspaceFirst"));
230
337
  }
231
- const existing = await getWorkspaceProject(activeProjectId.value);
338
+ const existing = await getWorkspaceProject(activeProjectId.value, nameSpace());
232
339
  if (!existing) {
233
340
  activeProjectId.value = null;
234
341
  activeProjectName.value = null;
@@ -237,18 +344,19 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
237
344
  throw new Error(t("panel.messages.workspaceNotFound"));
238
345
  }
239
346
  const name = resolveProjectName() || existing.name;
240
- await persistProject({
347
+ const record = await persistProject({
241
348
  id: activeProjectId.value,
242
349
  name,
243
350
  createdAt: existing.createdAt,
244
351
  });
352
+ emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
245
353
  return name;
246
354
  });
247
355
  }
248
356
 
249
357
  async function handleDeleteProject(id: string) {
250
358
  await runBusyTask(t("common.deletingWorkspace"), async () => {
251
- await deleteWorkspaceProject(id);
359
+ await deleteWorkspaceProject(id, nameSpace());
252
360
  if (activeProjectId.value === id) {
253
361
  activeProjectId.value = null;
254
362
  activeProjectName.value = null;
@@ -266,9 +374,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
266
374
  await handleSyncProject();
267
375
  }
268
376
  const record =
269
- (await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
377
+ (await getWorkspaceProject(id, nameSpace())) ??
378
+ readWorkspacePreviewCache(id, nameSpace());
270
379
  if (record) {
271
- writeWorkspacePreviewCache(record);
380
+ writeWorkspacePreviewCache(record, nameSpace());
272
381
  }
273
382
  const existing = previewWindows.get(id);
274
383
  if (existing && !existing.closed) {
@@ -280,9 +389,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
280
389
  refreshPreviewing();
281
390
  return;
282
391
  }
392
+ const ns = nameSpace();
283
393
  const win = window.open(
284
- buildOnlinePreviewUrl(id),
285
- `abuilder-preview-${id}`
394
+ buildOnlinePreviewUrl(id, ns),
395
+ previewWindowName(id, ns)
286
396
  );
287
397
  if (win) {
288
398
  previewWindows.set(id, win);