@arronqzy/vue-view 0.1.10 → 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.
package/README.md CHANGED
@@ -29,7 +29,21 @@ createApp(VueViewPanel).use(Antd).mount("#app");
29
29
  import { VueViewOnlinePreview, parseOnlinePreviewSearchParams } from "@arronqzy/vue-view";
30
30
  ```
31
31
 
32
- URL 参数:`?preview=online&projectId=<id>&pid=<instanceId>`
32
+ URL 参数:`?preview=online&projectId=<id>&pid=<instanceId>`。也可传入 `workspace` 直接预览宿主数据(优先于 `projectId`)。
33
+
34
+ ### 宿主集成
35
+
36
+ ```ts
37
+ import {
38
+ addEventSubscription,
39
+ AbuilderEvents,
40
+ parseWorkspaceData,
41
+ createEmptyWorkspace,
42
+ createWorkspaceProjectId,
43
+ } from "@arronqzy/vue-view";
44
+ ```
45
+
46
+ 创建 / 同步工作区会发出 `workspace:add` / `workspace:sync`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。IndexedDB CRUD 与 `getPreviewSnapshot` **不导出**(后者仅 React 实现)。
33
47
 
34
48
  ## 依赖
35
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,12 +23,12 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.10",
27
- "@arronqzy/rx-store": "1.0.5",
28
- "@arronqzy/view-table": "0.1.5",
29
- "@arronqzy/vue-blueprint": "0.1.8",
30
26
  "@arronqzy/i18n": "0.1.8",
27
+ "@arronqzy/blueprint-dsl": "1.0.11",
28
+ "@arronqzy/rx-store": "1.0.5",
31
29
  "@arronqzy/view-scene3d": "0.1.2",
30
+ "@arronqzy/view-table": "0.1.5",
31
+ "@arronqzy/vue-blueprint": "0.1.9",
32
32
  "@arronqzy/vue-rx-store": "0.1.3"
33
33
  },
34
34
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -1,4 +1,30 @@
1
1
  export { default as VueViewPanel } from "./panel/VueViewPanel.vue";
2
2
  export { default as VueViewOnlinePreview } from "./panel/VueViewOnlinePreview.vue";
3
3
  export { parseOnlinePreviewSearchParams } from "./panel/parseOnlinePreviewSearchParams";
4
+ export type { WorkspaceProjectRecord } from "./panel/library/workspace-project-db";
4
5
  export type * from "./panel/types";
6
+ export {
7
+ parseViewData,
8
+ validateViewData,
9
+ parseBlueprintData,
10
+ validateBlueprintData,
11
+ validateWorkspaceData,
12
+ parseWorkspaceData,
13
+ createEmptyWorkspace,
14
+ } from "./panel/library/parse-workspace-data";
15
+ export { createWorkspaceProjectId } from "./panel/library/workspace-project-db";
16
+ export {
17
+ addEventSubscription,
18
+ AbuilderEvents,
19
+ } from "./panel/library/event-subscription";
20
+ export type {
21
+ AbuilderEventName,
22
+ AbuilderEventPayloadMap,
23
+ WorkspaceAddEventPayload,
24
+ WorkspaceSyncEventPayload,
25
+ WorkspaceData,
26
+ } from "./panel/library/event-subscription";
27
+ export type {
28
+ ParseCheckResult,
29
+ WorkspaceParseCheckResult,
30
+ } from "./panel/library/parse-workspace-data";
@@ -22,7 +22,10 @@ import {
22
22
  useViewScopeStoreVersion,
23
23
  } from "./scope/view-scope-store";
24
24
  import { resolvePanelElementScope } from "./utils/scope-template";
25
- import { getWorkspaceProject } from "./library/workspace-project-db";
25
+ import {
26
+ getWorkspaceProject,
27
+ type WorkspaceProjectRecord,
28
+ } from "./library/workspace-project-db";
26
29
  import { readWorkspacePreviewCache } from "./library/workspace-project-cache";
27
30
  import { subscribeWorkspaceProjectUpdates } from "./library/workspace-project-sync";
28
31
  import {
@@ -41,8 +44,12 @@ const { t, locale } = useI18n();
41
44
  const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
42
45
 
43
46
  const props = defineProps<{
44
- projectId: string;
47
+ /** 从 IndexedDB / 缓存按 ID 加载;与 `workspace` 二选一,优先 `workspace` */
48
+ projectId?: string;
45
49
  previewInstanceId?: string;
50
+ nameSpace?: string | null;
51
+ /** 直接传入完整工作区,用于宿主后端加载后预览 */
52
+ workspace?: WorkspaceProjectRecord | null;
46
53
  }>();
47
54
 
48
55
  const loadError = ref<string | null>(null);
@@ -53,7 +60,7 @@ const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
53
60
  const layoutRevision = ref(0);
54
61
  const layoutReady = ref(false);
55
62
  const sceneRef = ref<HTMLDivElement | null>(null);
56
- const outputScale = ref(readOutputScale());
63
+ const outputScale = ref(readOutputScale(props.nameSpace));
57
64
  const fillScale = ref({ scaleX: 1, scaleY: 1 });
58
65
 
59
66
  const layers = computed(() => (panelState.value ? parsePanelLayers(panelState.value) : []));
@@ -64,10 +71,19 @@ const allElements = computed(() =>
64
71
  panelState.value ? parseAllPanelElements(panelState.value) : []
65
72
  );
66
73
 
74
+ const resolvedProjectId = computed(
75
+ () => props.workspace?.id ?? props.projectId ?? ""
76
+ );
77
+
78
+ function workspaceTitle(record: WorkspaceProjectRecord) {
79
+ const product = typeof record.productName === "string" ? record.productName : "";
80
+ return product.trim() || record.name || t("panel.workspace.previewDocTitle");
81
+ }
82
+
67
83
  async function loadWorkspaceRecord(projectId: string) {
68
- const fromDb = await getWorkspaceProject(projectId);
84
+ const fromDb = await getWorkspaceProject(projectId, props.nameSpace);
69
85
  if (fromDb) return fromDb;
70
- return readWorkspacePreviewCache(projectId);
86
+ return readWorkspacePreviewCache(projectId, props.nameSpace);
71
87
  }
72
88
 
73
89
  function applyTitleIcon(titleIconDataUrl?: string) {
@@ -82,13 +98,7 @@ function applyTitleIcon(titleIconDataUrl?: string) {
82
98
  }
83
99
  }
84
100
 
85
- async function loadProject() {
86
- const record = await loadWorkspaceRecord(props.projectId);
87
- if (!record) {
88
- loadError.value = t("panel.messages.workspaceNotFound");
89
- panelState.value = null;
90
- return;
91
- }
101
+ function applyWorkspaceRecord(record: WorkspaceProjectRecord) {
92
102
  const normalized = normalizeImportedPanelState(record.panelState);
93
103
  if (!normalized) {
94
104
  loadError.value = t("panel.messages.workspaceDataInvalid");
@@ -99,24 +109,56 @@ async function loadProject() {
99
109
  clearViewElementScopes();
100
110
  panelState.value = normalized;
101
111
  blueprintGraph.value = BlueprintGraph.fromDocument(record.blueprintDocument);
102
- document.title = record.productName.trim() || record.name || t("panel.workspace.previewDocTitle");
112
+ document.title = workspaceTitle(record);
103
113
  applyTitleIcon(record.titleIconDataUrl);
104
114
  projectRevision.value += 1;
105
115
  }
106
116
 
107
- onMounted(() => {
108
- void loadProject();
109
- });
117
+ async function loadProject() {
118
+ if (props.workspace) {
119
+ applyWorkspaceRecord(props.workspace);
120
+ return;
121
+ }
122
+ if (!props.projectId) {
123
+ loadError.value = t("panel.messages.workspaceNotFound");
124
+ panelState.value = null;
125
+ return;
126
+ }
127
+ const record = await loadWorkspaceRecord(props.projectId);
128
+ if (!record) {
129
+ loadError.value = t("panel.messages.workspaceNotFound");
130
+ panelState.value = null;
131
+ return;
132
+ }
133
+ applyWorkspaceRecord(record);
134
+ }
110
135
 
111
- onMounted(() => {
112
- const unsub = subscribeWorkspaceProjectUpdates(props.projectId, () => {
136
+ watch(
137
+ () => [props.workspace, props.projectId, props.nameSpace] as const,
138
+ () => {
113
139
  void loadProject();
114
- });
115
- onUnmounted(unsub);
116
- });
140
+ },
141
+ { immediate: true }
142
+ );
143
+
144
+ watch(
145
+ () => [props.workspace, props.projectId, props.nameSpace] as const,
146
+ ([workspace, projectId, nameSpace], _prev, onCleanup) => {
147
+ if (workspace || !projectId) return;
148
+ const unsub = subscribeWorkspaceProjectUpdates(
149
+ projectId,
150
+ () => {
151
+ void loadProject();
152
+ },
153
+ nameSpace
154
+ );
155
+ onCleanup(unsub);
156
+ },
157
+ { immediate: true }
158
+ );
117
159
 
118
160
  watch(projectRevision, () => {
119
- void listBlueprintLibrary().then((items) => {
161
+ void listBlueprintLibrary(props.nameSpace).then((items) => {
120
162
  blueprintLibraryItems.value = items;
121
163
  });
122
164
  });
@@ -126,9 +168,9 @@ const blueprintLibraryNameById = computed(
126
168
  );
127
169
 
128
170
  const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
129
- const record = await getBlueprintLibraryRecord(libraryBlueprintId);
171
+ const record = await getBlueprintLibraryRecord(libraryBlueprintId, props.nameSpace);
130
172
  if (!record) return null;
131
- const items = await listBlueprintLibrary();
173
+ const items = await listBlueprintLibrary(props.nameSpace);
132
174
  const nameById = new Map(items.map((item) => [item.id, item.name]));
133
175
  return documentToRunnableGraph(record.document, { libraryNameById: nameById });
134
176
  };
@@ -167,7 +209,7 @@ const displayElements = computed(() =>
167
209
  );
168
210
 
169
211
  function applySceneFit() {
170
- const enabled = readOutputScale();
212
+ const enabled = readOutputScale(props.nameSpace);
171
213
  outputScale.value = enabled;
172
214
  const fill = applyPreviewSceneFill(
173
215
  sceneRef.value,
@@ -277,7 +319,7 @@ function noopSelect() {}
277
319
  v-else
278
320
  class="min-h-screen w-full overflow-hidden bg-white text-gray-900"
279
321
  data-preview-mode="online"
280
- :data-project-id="projectId"
322
+ :data-project-id="resolvedProjectId"
281
323
  :data-preview-instance-id="previewInstanceId ?? ''"
282
324
  :data-preview-node-count="String(displayElements.length)"
283
325
  >
@@ -78,8 +78,10 @@ const props = withDefaults(
78
78
  defineProps<{
79
79
  class?: string;
80
80
  initialZoom?: number;
81
+ nameSpace?: string | null;
82
+ initialWorkspace?: WorkspaceProjectRecord | null;
81
83
  }>(),
82
- { initialZoom: 1 }
84
+ { initialZoom: 1, nameSpace: null, initialWorkspace: null }
83
85
  );
84
86
 
85
87
  const {
@@ -296,9 +298,9 @@ const {
296
298
  } = useBlueprintNodeSelectionGuard(selectedBlueprintNodeId, applyBlueprintNodeSelection);
297
299
 
298
300
  const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
299
- const record = await getBlueprintLibraryRecord(libraryBlueprintId);
301
+ const record = await getBlueprintLibraryRecord(libraryBlueprintId, props.nameSpace);
300
302
  if (!record) return null;
301
- const items = await listBlueprintLibrary();
303
+ const items = await listBlueprintLibrary(props.nameSpace);
302
304
  const nameById = new Map(items.map((item) => [item.id, item.name]));
303
305
  return documentToRunnableGraph(record.document, { libraryNameById: nameById });
304
306
  };
@@ -319,6 +321,7 @@ const blueprintDebugSession = useBlueprintDebugSession({
319
321
  libraryNameById: blueprintLibraryNameById,
320
322
  onExecutionBlocked: handleBlueprintExecutionBlocked,
321
323
  onViewScopeUpdate: handleViewScopeUpdate,
324
+ nameSpace: () => props.nameSpace,
322
325
  });
323
326
 
324
327
  const blueprintDebugToolbar = computed(() => ({
@@ -359,7 +362,7 @@ function handleUpdateAllowFalseSignalPropagation(value: boolean) {
359
362
  }
360
363
 
361
364
  async function refreshBlueprintLibrary() {
362
- blueprintLibraryItems.value = await listBlueprintLibrary();
365
+ blueprintLibraryItems.value = await listBlueprintLibrary(props.nameSpace);
363
366
  }
364
367
 
365
368
  onMounted(() => {
@@ -402,6 +405,8 @@ const workspaceProjects = useWorkspaceProjects({
402
405
  },
403
406
  panelRevision: computed(() => `${historyCursor.value}|${allElements.value.length}`),
404
407
  onProjectApplied: handleWorkspaceProjectApplied,
408
+ initialWorkspace: () => props.initialWorkspace,
409
+ nameSpace: () => props.nameSpace,
405
410
  });
406
411
 
407
412
  const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
@@ -440,7 +445,7 @@ const blueprintNodeOptions = computed(() =>
440
445
  );
441
446
  async function loadBlueprintFromLibrary(id: string) {
442
447
  await runBusyTask(t("common.loadingBlueprint"), async () => {
443
- const record = await getBlueprintLibraryRecord(id);
448
+ const record = await getBlueprintLibraryRecord(id, props.nameSpace);
444
449
  if (!record) {
445
450
  message.error(panelMessages().blueprintNotFound);
446
451
  void refreshBlueprintLibrary();
@@ -487,14 +492,14 @@ async function syncBlueprintToLibrary() {
487
492
  const libraryId = activeBlueprintLibraryId.value;
488
493
  if (!libraryId) return;
489
494
  await runBusyTask(t("common.syncingBlueprint"), async () => {
490
- const record = await getBlueprintLibraryRecord(libraryId);
495
+ const record = await getBlueprintLibraryRecord(libraryId, props.nameSpace);
491
496
  if (!record) return;
492
497
  await putBlueprintLibraryRecord({
493
498
  ...record,
494
499
  document: blueprintGraph.value.document,
495
500
  name: blueprintMeta.value.name,
496
501
  remark: blueprintMeta.value.remark,
497
- });
502
+ }, props.nameSpace);
498
503
  blueprintSyncedDocument.value = blueprintGraph.value.document;
499
504
  message.success(panelMessages().blueprintSyncedToLibrary);
500
505
  await refreshBlueprintLibrary();
@@ -510,7 +515,7 @@ async function saveBlueprintToLibrary(meta: BlueprintMetaDraft) {
510
515
  meta,
511
516
  source: "saved",
512
517
  });
513
- await putBlueprintLibraryRecord(record);
518
+ await putBlueprintLibraryRecord(record, props.nameSpace);
514
519
  activeBlueprintLibraryId.value = id;
515
520
  blueprintSyncedDocument.value = blueprintGraph.value.document;
516
521
  blueprintMeta.value = meta;
@@ -526,7 +531,7 @@ function handleExport() {
526
531
  const url = URL.createObjectURL(blob);
527
532
  const a = document.createElement("a");
528
533
  a.href = url;
529
- const safeName = (productName.value.trim() || "panel")
534
+ const safeName = ((productName.value ?? "").trim() || "panel")
530
535
  .replace(/[\\/:*?"<>|]/g, "-")
531
536
  .replace(/\s+/g, "-");
532
537
  a.download = `${safeName}-${Date.now()}.json`;
@@ -566,7 +571,7 @@ async function handleBlueprintImportFile(file: File) {
566
571
  const text = await file.text();
567
572
  const payload = parseBlueprintImportFile(await parseJsonText(text));
568
573
  const record = libraryRecordFromImport(payload);
569
- await putBlueprintLibraryRecord(record);
574
+ await putBlueprintLibraryRecord(record, props.nameSpace);
570
575
  await refreshBlueprintLibrary();
571
576
  if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
572
577
  await loadBlueprintFromLibrary(record.id);
@@ -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);
@@ -0,0 +1,60 @@
1
+ import type { WorkspaceProjectRecord } from "./workspace-project-db";
2
+
3
+ export const AbuilderEvents = {
4
+ /** 添加工作区 */
5
+ workspaceAdd: "workspace:add",
6
+ /** 同步工作区 */
7
+ workspaceSync: "workspace:sync",
8
+ } as const;
9
+
10
+ export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
11
+
12
+ /** 工作区完整数据(面板 + 蓝图),可用于外部持久化与再次加载 */
13
+ export type WorkspaceData = WorkspaceProjectRecord;
14
+
15
+ export type WorkspaceAddEventPayload = WorkspaceData;
16
+ export type WorkspaceSyncEventPayload = WorkspaceData;
17
+
18
+ export type AbuilderEventPayloadMap = {
19
+ [AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
20
+ [AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
21
+ };
22
+
23
+ type EventCallback = (payload: unknown) => void;
24
+
25
+ const listeners = new Map<string, Set<EventCallback>>();
26
+
27
+ export function emitAbuilderEvent<T extends AbuilderEventName>(
28
+ eventName: T,
29
+ payload: AbuilderEventPayloadMap[T]
30
+ ): void {
31
+ const set = listeners.get(eventName);
32
+ if (!set) return;
33
+ for (const callback of set) {
34
+ try {
35
+ callback(payload);
36
+ } catch (error) {
37
+ console.error(`[abuilder] event handler error (${eventName}):`, error);
38
+ }
39
+ }
40
+ }
41
+
42
+ export function addEventSubscription<T extends AbuilderEventName>(
43
+ eventName: T,
44
+ callback: (payload: AbuilderEventPayloadMap[T]) => void
45
+ ): { unsubscribe: () => void } {
46
+ let set = listeners.get(eventName);
47
+ if (!set) {
48
+ set = new Set();
49
+ listeners.set(eventName, set);
50
+ }
51
+ set.add(callback as EventCallback);
52
+ return {
53
+ unsubscribe: () => {
54
+ set.delete(callback as EventCallback);
55
+ if (set.size === 0) {
56
+ listeners.delete(eventName);
57
+ }
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ BlueprintGraph,
3
+ parseBlueprintData,
4
+ validateBlueprintData,
5
+ type BlueprintDocument,
6
+ type BlueprintMetaDraft,
7
+ type ParseCheckResult,
8
+ } from "@arronqzy/vue-blueprint";
9
+ import type { State } from "@arronqzy/rx-store";
10
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
11
+ import {
12
+ createWorkspaceProjectId,
13
+ type WorkspaceProjectRecord,
14
+ } from "./workspace-project-db";
15
+
16
+ export type { ParseCheckResult };
17
+
18
+ export type WorkspaceParseCheckResult = ParseCheckResult & {
19
+ view: ParseCheckResult;
20
+ blueprint: ParseCheckResult;
21
+ };
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+
27
+ function extractPanelState(data: unknown): unknown {
28
+ if (isRecord(data) && data.panelState !== undefined) return data.panelState;
29
+ return data;
30
+ }
31
+
32
+ function readBlueprintMeta(raw: unknown): BlueprintMetaDraft {
33
+ if (!isRecord(raw)) return { name: "", remark: "" };
34
+ return {
35
+ name: typeof raw.name === "string" ? raw.name : "",
36
+ remark: typeof raw.remark === "string" ? raw.remark : "",
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 校验数据能否被视图解析(工作区 `panelState` 或裸面板状态均可)。
42
+ */
43
+ export function parseViewData(
44
+ data: unknown
45
+ ): ParseCheckResult & { value?: State } {
46
+ if (data == null) {
47
+ return { ok: false, error: "empty" };
48
+ }
49
+ const normalized = normalizeImportedPanelState(extractPanelState(data));
50
+ if (!normalized) {
51
+ return { ok: false, error: "invalid-view-data" };
52
+ }
53
+ return { ok: true, value: normalized };
54
+ }
55
+
56
+ export function validateViewData(data: unknown): ParseCheckResult {
57
+ const parsed = parseViewData(data);
58
+ return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
59
+ }
60
+
61
+ export { parseBlueprintData, validateBlueprintData };
62
+
63
+ /**
64
+ * 构造一份可交给 `initialWorkspace` 的空工作区。
65
+ */
66
+ export function createEmptyWorkspace(
67
+ init?: Partial<WorkspaceProjectRecord>
68
+ ): WorkspaceProjectRecord {
69
+ const now = Date.now();
70
+ return {
71
+ id: init?.id || createWorkspaceProjectId(),
72
+ name: init?.name ?? "",
73
+ createdAt: init?.createdAt ?? now,
74
+ updatedAt: init?.updatedAt ?? now,
75
+ panelState: init?.panelState ?? createEmptyPanelState(),
76
+ blueprintDocument: init?.blueprintDocument ?? BlueprintGraph.empty().document,
77
+ blueprintMeta: init?.blueprintMeta ?? { name: "", remark: "" },
78
+ productName: init?.productName ?? "",
79
+ titleIconDataUrl: init?.titleIconDataUrl,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 校验完整工作区数据:视图 `panelState` 与蓝图 `blueprintDocument` 都要能解析。
85
+ */
86
+ export function validateWorkspaceData(data: unknown): WorkspaceParseCheckResult {
87
+ if (data == null) {
88
+ const empty = { ok: false, error: "empty" } satisfies ParseCheckResult;
89
+ return { ok: false, error: "empty", view: empty, blueprint: empty };
90
+ }
91
+ if (!isRecord(data)) {
92
+ const invalid = { ok: false, error: "invalid-workspace-data" } satisfies ParseCheckResult;
93
+ return {
94
+ ok: false,
95
+ error: "invalid-workspace-data",
96
+ view: invalid,
97
+ blueprint: invalid,
98
+ };
99
+ }
100
+ const view = validateViewData(data);
101
+ const blueprint = validateBlueprintData(data);
102
+ const ok = view.ok && blueprint.ok;
103
+ return {
104
+ ok,
105
+ error: ok ? undefined : "invalid-workspace-data",
106
+ view,
107
+ blueprint,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * 解析并规范化工作区。成功时 `value` 可直接作为 `initialWorkspace`。
113
+ */
114
+ export function parseWorkspaceData(
115
+ data: unknown
116
+ ): WorkspaceParseCheckResult & { value?: WorkspaceProjectRecord } {
117
+ const check = validateWorkspaceData(data);
118
+ if (!check.ok || !isRecord(data)) {
119
+ return check;
120
+ }
121
+ const view = parseViewData(data);
122
+ const blueprint = parseBlueprintData(data);
123
+ if (!view.value || !blueprint.value) {
124
+ return check;
125
+ }
126
+ const now = Date.now();
127
+ return {
128
+ ...check,
129
+ value: {
130
+ id: typeof data.id === "string" && data.id ? data.id : createWorkspaceProjectId(),
131
+ name: typeof data.name === "string" ? data.name : "",
132
+ createdAt: typeof data.createdAt === "number" ? data.createdAt : now,
133
+ updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : now,
134
+ panelState: view.value,
135
+ blueprintDocument: blueprint.value as BlueprintDocument,
136
+ blueprintMeta: readBlueprintMeta(data.blueprintMeta),
137
+ productName: typeof data.productName === "string" ? data.productName : "",
138
+ titleIconDataUrl:
139
+ typeof data.titleIconDataUrl === "string" ? data.titleIconDataUrl : undefined,
140
+ },
141
+ };
142
+ }
@@ -1,11 +1,19 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
1
2
  import type { WorkspaceProjectRecord } from "./workspace-project-db";
2
3
 
3
4
  const CACHE_PREFIX = "arronqzy-workspace-preview:";
4
5
 
5
- export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
6
+ function previewCacheKey(projectId: string, nameSpace?: string | null): string {
7
+ return appStorageKey(CACHE_PREFIX + projectId, nameSpace);
8
+ }
9
+
10
+ export function writeWorkspacePreviewCache(
11
+ record: WorkspaceProjectRecord,
12
+ nameSpace?: string | null
13
+ ): void {
6
14
  try {
7
15
  localStorage.setItem(
8
- CACHE_PREFIX + record.id,
16
+ previewCacheKey(record.id, nameSpace),
9
17
  JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
10
18
  );
11
19
  } catch {
@@ -14,10 +22,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
14
22
  }
15
23
 
16
24
  export function readWorkspacePreviewCache(
17
- projectId: string
25
+ projectId: string,
26
+ nameSpace?: string | null
18
27
  ): WorkspaceProjectRecord | null {
19
28
  try {
20
- const raw = localStorage.getItem(CACHE_PREFIX + projectId);
29
+ const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
21
30
  if (!raw) return null;
22
31
  const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
23
32
  if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
@@ -1,5 +1,6 @@
1
1
  import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/vue-blueprint";
2
2
  import type { State } from "@arronqzy/rx-store";
3
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
3
4
 
4
5
  const DB_NAME = "arronqzy-workspace-projects";
5
6
  const DB_VERSION = 1;
@@ -24,9 +25,9 @@ export type WorkspaceProjectRecord = {
24
25
  titleIconDataUrl?: string;
25
26
  };
26
27
 
27
- function openDb(): Promise<IDBDatabase> {
28
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
28
29
  return new Promise((resolve, reject) => {
29
- const request = indexedDB.open(DB_NAME, DB_VERSION);
30
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
30
31
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
31
32
  request.onupgradeneeded = () => {
32
33
  const db = request.result;
@@ -41,9 +42,10 @@ function openDb(): Promise<IDBDatabase> {
41
42
 
42
43
  function runTransaction<T>(
43
44
  mode: IDBTransactionMode,
44
- runner: (store: IDBObjectStore) => IDBRequest<T>
45
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
46
+ nameSpace?: string | null
45
47
  ): Promise<T> {
46
- return openDb().then(
48
+ return openDb(nameSpace).then(
47
49
  (db) =>
48
50
  new Promise<T>((resolve, reject) => {
49
51
  const tx = db.transaction(STORE_NAME, mode);
@@ -76,8 +78,10 @@ export function createWorkspaceProjectId(): string {
76
78
  return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
77
79
  }
78
80
 
79
- export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
80
- const db = await openDb();
81
+ export async function listWorkspaceProjects(
82
+ nameSpace?: string | null
83
+ ): Promise<WorkspaceProjectListItem[]> {
84
+ const db = await openDb(nameSpace);
81
85
  return new Promise((resolve, reject) => {
82
86
  const tx = db.transaction(STORE_NAME, "readonly");
83
87
  const store = tx.objectStore(STORE_NAME);
@@ -109,21 +113,28 @@ export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[
109
113
  }
110
114
 
111
115
  export async function getWorkspaceProject(
112
- id: string
116
+ id: string,
117
+ nameSpace?: string | null
113
118
  ): Promise<WorkspaceProjectRecord | null> {
114
- const record = await runTransaction<WorkspaceProjectRecord | undefined>("readonly", (store) =>
115
- store.get(id)
119
+ const record = await runTransaction<WorkspaceProjectRecord | undefined>(
120
+ "readonly",
121
+ (store) => store.get(id),
122
+ nameSpace
116
123
  );
117
124
  return record ?? null;
118
125
  }
119
126
 
120
127
  export async function putWorkspaceProject(
121
- record: WorkspaceProjectRecord
128
+ record: WorkspaceProjectRecord,
129
+ nameSpace?: string | null
122
130
  ): Promise<WorkspaceProjectRecord> {
123
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
131
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
124
132
  return record;
125
133
  }
126
134
 
127
- export async function deleteWorkspaceProject(id: string): Promise<void> {
128
- await runTransaction<undefined>("readwrite", (store) => store.delete(id));
135
+ export async function deleteWorkspaceProject(
136
+ id: string,
137
+ nameSpace?: string | null
138
+ ): Promise<void> {
139
+ await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
129
140
  }
@@ -1,3 +1,5 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+
1
3
  const CHANNEL_PREFIX = "arronqzy-workspace-project";
2
4
 
3
5
  export type WorkspaceProjectSyncMessage = {
@@ -6,16 +8,22 @@ export type WorkspaceProjectSyncMessage = {
6
8
  updatedAt: number;
7
9
  };
8
10
 
9
- export function getWorkspaceProjectChannelName(projectId: string): string {
10
- return `${CHANNEL_PREFIX}:${projectId}`;
11
+ export function getWorkspaceProjectChannelName(
12
+ projectId: string,
13
+ nameSpace?: string | null
14
+ ): string {
15
+ return appStorageKey(`${CHANNEL_PREFIX}:${projectId}`, nameSpace);
11
16
  }
12
17
 
13
18
  export function broadcastWorkspaceProjectUpdate(
14
19
  projectId: string,
15
- updatedAt: number
20
+ updatedAt: number,
21
+ nameSpace?: string | null
16
22
  ): void {
17
23
  if (typeof BroadcastChannel === "undefined") return;
18
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
24
+ const channel = new BroadcastChannel(
25
+ getWorkspaceProjectChannelName(projectId, nameSpace)
26
+ );
19
27
  channel.postMessage({
20
28
  type: "updated",
21
29
  projectId,
@@ -26,12 +34,15 @@ export function broadcastWorkspaceProjectUpdate(
26
34
 
27
35
  export function subscribeWorkspaceProjectUpdates(
28
36
  projectId: string,
29
- onUpdate: (message: WorkspaceProjectSyncMessage) => void
37
+ onUpdate: (message: WorkspaceProjectSyncMessage) => void,
38
+ nameSpace?: string | null
30
39
  ): () => void {
31
40
  if (typeof BroadcastChannel === "undefined") {
32
41
  return () => {};
33
42
  }
34
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
43
+ const channel = new BroadcastChannel(
44
+ getWorkspaceProjectChannelName(projectId, nameSpace)
45
+ );
35
46
  channel.onmessage = (event: MessageEvent<WorkspaceProjectSyncMessage>) => {
36
47
  const data = event.data;
37
48
  if (!data || data.type !== "updated" || data.projectId !== projectId) return;
@@ -1,13 +1,16 @@
1
1
  export function parseOnlinePreviewSearchParams(search: string): {
2
2
  projectId: string;
3
3
  previewInstanceId?: string;
4
+ nameSpace?: string;
4
5
  } | null {
5
6
  const params = new URLSearchParams(search);
6
7
  if (params.get("preview") !== "online") return null;
7
8
  const projectId = params.get("projectId") ?? params.get("id");
8
9
  if (!projectId) return null;
10
+ const nameSpace = (params.get("ns") ?? params.get("nameSpace") ?? "").trim();
9
11
  return {
10
12
  projectId,
11
13
  previewInstanceId: params.get("pid") ?? undefined,
14
+ nameSpace: nameSpace || undefined,
12
15
  };
13
16
  }
@@ -1,14 +1,27 @@
1
1
  /** 产物预览是否缩放内部内容(字体等)。默认 false:块仍按视口撑满,内容保持原尺寸以免失真。 */
2
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
3
+
2
4
  export const OUTPUT_SCALE_STORAGE_KEY = "panel:outputScale";
3
5
 
4
- export function readOutputScale(): boolean {
6
+ export function readOutputScale(nameSpace?: string | null): boolean {
5
7
  try {
6
- return localStorage.getItem(OUTPUT_SCALE_STORAGE_KEY) === "1";
8
+ return localStorage.getItem(appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace)) === "1";
7
9
  } catch {
8
10
  return false;
9
11
  }
10
12
  }
11
13
 
14
+ export function writeOutputScale(enabled: boolean, nameSpace?: string | null) {
15
+ try {
16
+ localStorage.setItem(
17
+ appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace),
18
+ enabled ? "1" : "0"
19
+ );
20
+ } catch {
21
+ // ignore storage errors
22
+ }
23
+ }
24
+
12
25
  export type PreviewFillScale = {
13
26
  vw: number;
14
27
  vh: number;
@@ -26,6 +26,24 @@ export function normalizeImportedPanelState(state: unknown): State | null {
26
26
  return next;
27
27
  }
28
28
 
29
+ export function createEmptyPanelState(): State {
30
+ return {
31
+ root: {
32
+ id: "root",
33
+ type: "root",
34
+ props: {},
35
+ children: [],
36
+ },
37
+ selectedIds: [],
38
+ variables: {
39
+ counter: 0,
40
+ isOpen: false,
41
+ layers: [DEFAULT_LAYER],
42
+ activeLayerId: DEFAULT_LAYER_ID,
43
+ },
44
+ };
45
+ }
46
+
29
47
  export function parseAllPanelElements(state: State): PanelElement[] {
30
48
  return (state.root.children ?? [])
31
49
  .filter((node) => {