@arronqzy/react-view 1.0.21 → 1.0.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -30,14 +30,14 @@
30
30
  "@codemirror/lang-json": "^6.0.1",
31
31
  "@codemirror/theme-one-dark": "^6.1.2",
32
32
  "@codemirror/view": "^6.36.2",
33
- "@arronqzy/ui": "1.0.6",
34
- "@arronqzy/blueprint-dsl": "1.0.10",
35
33
  "@arronqzy/i18n": "0.1.8",
36
- "@arronqzy/react-blueprint": "1.0.12",
34
+ "@arronqzy/ui": "1.0.6",
37
35
  "@arronqzy/rx-store": "1.0.5",
38
- "@arronqzy/webllm-assistant": "0.1.10",
36
+ "@arronqzy/react-blueprint": "1.0.13",
37
+ "@arronqzy/blueprint-dsl": "1.0.11",
38
+ "@arronqzy/view-scene3d": "0.1.2",
39
39
  "@arronqzy/view-table": "0.1.5",
40
- "@arronqzy/view-scene3d": "0.1.2"
40
+ "@arronqzy/webllm-assistant": "0.1.10"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "^19.0.1",
package/readme.md CHANGED
@@ -147,7 +147,12 @@ const dataUrl = await getPreviewSnapshot({
147
147
  | `parseOnlinePreviewSearchParams` | 解析预览 URL 参数 |
148
148
  | `addEventSubscription` | 订阅工作区事件,返回 `{ unsubscribe }` |
149
149
  | `AbuilderEvents` | 工作区事件名常量 |
150
- | `getPreviewSnapshot` | 获取预览画面 base64 缩略图 |
150
+ | `getPreviewSnapshot` | 获取预览画面 base64 缩略图(仅本包 / `@arronqzy/abuilder`) |
151
+ | `parseWorkspaceData` / `validateWorkspaceData` | 解析或校验完整工作区;`parse` 的 `value` 可作 `initialWorkspace` |
152
+ | `parseViewData` / `validateViewData` | 解析或校验视图 `panelState` |
153
+ | `parseBlueprintData` / `validateBlueprintData` | 解析或校验蓝图文档 |
154
+ | `createEmptyWorkspace` | 构造空工作区记录 |
155
+ | `createWorkspaceProjectId` | 生成工作区 ID |
151
156
  | `WorkspaceData` / `WorkspaceProjectRecord` | 完整工作区数据类型 |
152
157
  | `createView` / `createAction` 等 | 视图 DSL 与指令扩展(`src/core`) |
153
158
 
@@ -1,4 +1,6 @@
1
1
  import { I18nProvider } from "@arronqzy/i18n/react";
2
+ import { LOCALE_STORAGE_KEY } from "@arronqzy/i18n";
3
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
4
  import type { Locale } from "@arronqzy/i18n";
3
5
  import type { ReactNode } from "react";
4
6
 
@@ -6,12 +8,22 @@ export type I18nRootProps = {
6
8
  children: ReactNode;
7
9
  locale?: Locale | null;
8
10
  onLocaleChange?: (locale: Locale) => void;
11
+ nameSpace?: string | null;
9
12
  };
10
13
 
11
14
  /** Ensures panel / preview subtree always has i18n context (host apps, duplicate package graphs). */
12
- export function I18nRoot({ children, locale, onLocaleChange }: I18nRootProps) {
15
+ export function I18nRoot({
16
+ children,
17
+ locale,
18
+ onLocaleChange,
19
+ nameSpace,
20
+ }: I18nRootProps) {
13
21
  return (
14
- <I18nProvider locale={locale} onLocaleChange={onLocaleChange}>
22
+ <I18nProvider
23
+ locale={locale}
24
+ onLocaleChange={onLocaleChange}
25
+ storageKey={appStorageKey(LOCALE_STORAGE_KEY, nameSpace)}
26
+ >
15
27
  {children}
16
28
  </I18nProvider>
17
29
  );
@@ -54,6 +54,7 @@ export type ReactViewOnlinePreviewProps = {
54
54
  workspace?: WorkspaceProjectRecord | null;
55
55
  locale?: Locale | null;
56
56
  onLocaleChange?: (locale: Locale) => void;
57
+ nameSpace?: string | null;
57
58
  };
58
59
 
59
60
  function applyTitleIcon(titleIconDataUrl?: string) {
@@ -68,20 +69,24 @@ function applyTitleIcon(titleIconDataUrl?: string) {
68
69
  }
69
70
  }
70
71
 
71
- async function loadWorkspaceRecord(projectId: string): Promise<WorkspaceProjectRecord | null> {
72
- const fromDb = await getWorkspaceProject(projectId);
72
+ async function loadWorkspaceRecord(
73
+ projectId: string,
74
+ nameSpace?: string | null
75
+ ): Promise<WorkspaceProjectRecord | null> {
76
+ const fromDb = await getWorkspaceProject(projectId, nameSpace);
73
77
  if (fromDb) return fromDb;
74
- return readWorkspacePreviewCache(projectId);
78
+ return readWorkspacePreviewCache(projectId, nameSpace);
75
79
  }
76
80
 
77
81
  export function ReactViewOnlinePreview({
78
82
  locale,
79
83
  onLocaleChange,
84
+ nameSpace,
80
85
  ...rest
81
86
  }: ReactViewOnlinePreviewProps) {
82
87
  return (
83
- <I18nRoot locale={locale} onLocaleChange={onLocaleChange}>
84
- <ReactViewOnlinePreviewInner {...rest} />
88
+ <I18nRoot locale={locale} onLocaleChange={onLocaleChange} nameSpace={nameSpace}>
89
+ <ReactViewOnlinePreviewInner nameSpace={nameSpace} {...rest} />
85
90
  </I18nRoot>
86
91
  );
87
92
  }
@@ -90,6 +95,7 @@ function ReactViewOnlinePreviewInner({
90
95
  projectId,
91
96
  previewInstanceId,
92
97
  workspace = null,
98
+ nameSpace = null,
93
99
  }: ReactViewOnlinePreviewProps) {
94
100
  const { t } = useI18n();
95
101
 
@@ -102,7 +108,7 @@ function ReactViewOnlinePreviewInner({
102
108
  >([]);
103
109
  const [layoutRevision, setLayoutRevision] = useState(0);
104
110
  const [layoutReady, setLayoutReady] = useState(false);
105
- const [outputScale, setOutputScale] = useState(() => readOutputScale());
111
+ const [outputScale, setOutputScale] = useState(() => readOutputScale(nameSpace));
106
112
  const [fillScale, setFillScale] = useState({ scaleX: 1, scaleY: 1 });
107
113
 
108
114
  const sceneRef = useRef<HTMLDivElement | null>(null);
@@ -122,7 +128,9 @@ function ReactViewOnlinePreviewInner({
122
128
  setPanelState(normalized);
123
129
  setBlueprintGraph(BlueprintGraph.fromDocument(record.blueprintDocument));
124
130
  document.title =
125
- record.productName.trim() || record.name || t("panel.workspace.previewDocTitle");
131
+ (typeof record.productName === "string" ? record.productName : "").trim() ||
132
+ record.name ||
133
+ t("panel.workspace.previewDocTitle");
126
134
  applyTitleIcon(record.titleIconDataUrl);
127
135
  setProjectRevision((v) => v + 1);
128
136
  return true;
@@ -141,7 +149,7 @@ function ReactViewOnlinePreviewInner({
141
149
  return;
142
150
  }
143
151
 
144
- const record = await loadWorkspaceRecord(projectId);
152
+ const record = await loadWorkspaceRecord(projectId, nameSpace);
145
153
  if (!record) {
146
154
  setLoadError(t("panel.messages.workspaceNotFound"));
147
155
  setPanelState(null);
@@ -149,7 +157,7 @@ function ReactViewOnlinePreviewInner({
149
157
  }
150
158
 
151
159
  applyWorkspaceRecord(record);
152
- }, [applyWorkspaceRecord, projectId, t, workspace]);
160
+ }, [applyWorkspaceRecord, nameSpace, projectId, t, workspace]);
153
161
 
154
162
  useEffect(() => {
155
163
  void loadProject();
@@ -159,8 +167,8 @@ function ReactViewOnlinePreviewInner({
159
167
  if (workspace || !projectId) return;
160
168
  return subscribeWorkspaceProjectUpdates(projectId, () => {
161
169
  void loadProject();
162
- });
163
- }, [loadProject, projectId, workspace]);
170
+ }, nameSpace);
171
+ }, [loadProject, nameSpace, projectId, workspace]);
164
172
 
165
173
  const layers = useMemo(
166
174
  () => (panelState ? parsePanelLayers(panelState) : []),
@@ -177,13 +185,13 @@ function ReactViewOnlinePreviewInner({
177
185
 
178
186
  useEffect(() => {
179
187
  let cancelled = false;
180
- void listBlueprintLibrary().then((items) => {
188
+ void listBlueprintLibrary(nameSpace).then((items) => {
181
189
  if (!cancelled) setBlueprintLibraryItems(items);
182
190
  });
183
191
  return () => {
184
192
  cancelled = true;
185
193
  };
186
- }, [projectRevision]);
194
+ }, [nameSpace, projectRevision]);
187
195
 
188
196
  const blueprintLibraryNameById = useMemo(
189
197
  () => new Map(blueprintLibraryItems.map((item) => [item.id, item.name])),
@@ -192,13 +200,13 @@ function ReactViewOnlinePreviewInner({
192
200
 
193
201
  const resolveLibraryBlueprint = useCallback<LibraryBlueprintResolver>(
194
202
  async (libraryBlueprintId) => {
195
- const record = await getBlueprintLibraryRecord(libraryBlueprintId);
203
+ const record = await getBlueprintLibraryRecord(libraryBlueprintId, nameSpace);
196
204
  if (!record) return null;
197
- const items = await listBlueprintLibrary();
205
+ const items = await listBlueprintLibrary(nameSpace);
198
206
  const nameById = new Map(items.map((item) => [item.id, item.name]));
199
207
  return documentToRunnableGraph(record.document, { libraryNameById: nameById });
200
208
  },
201
- []
209
+ [nameSpace]
202
210
  );
203
211
 
204
212
  const handleViewScopeUpdate = useCallback(
@@ -251,7 +259,7 @@ function ReactViewOnlinePreviewInner({
251
259
  );
252
260
 
253
261
  const applySceneFit = useCallback(() => {
254
- const enabled = readOutputScale();
262
+ const enabled = readOutputScale(nameSpace);
255
263
  setOutputScale(enabled);
256
264
  const fill = applyPreviewSceneFill(
257
265
  sceneRef.current,
@@ -262,7 +270,7 @@ function ReactViewOnlinePreviewInner({
262
270
  setFillScale({ scaleX: fill.scaleX, scaleY: fill.scaleY });
263
271
  setLayoutRevision((v) => v + 1);
264
272
  notifyPreviewLayoutChanged();
265
- }, [sceneBounds.height, sceneBounds.width]);
273
+ }, [nameSpace, sceneBounds.height, sceneBounds.width]);
266
274
 
267
275
  useLayoutEffect(() => {
268
276
  if (!panelState || layerElements.length === 0) return;
@@ -451,13 +459,16 @@ function ReactViewOnlinePreviewInner({
451
459
  export function parseOnlinePreviewSearchParams(search: string): {
452
460
  projectId: string;
453
461
  previewInstanceId?: string;
462
+ nameSpace?: string;
454
463
  } | null {
455
464
  const params = new URLSearchParams(search);
456
465
  if (params.get("preview") !== "online") return null;
457
466
  const projectId = params.get("projectId") ?? params.get("id");
458
467
  if (!projectId) return null;
468
+ const nameSpace = (params.get("ns") ?? params.get("nameSpace") ?? "").trim();
459
469
  return {
460
470
  projectId,
461
471
  previewInstanceId: params.get("pid") ?? undefined,
472
+ nameSpace: nameSpace || undefined,
462
473
  };
463
474
  }
@@ -65,7 +65,7 @@ import {
65
65
  type BlueprintMetaDraft,
66
66
  } from "@arronqzy/react-blueprint";
67
67
  import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
68
- import { abortClockNode, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
68
+ import { abortClockNode, appStorageKey, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
69
69
  import { WorkspaceStageSplit } from "./components/WorkspaceStageSplit";
70
70
  import {
71
71
  WorkspaceConfigSidebar,
@@ -362,22 +362,30 @@ function shouldClearSelectionOnBlank(target: HTMLElement | null) {
362
362
  export type ReactViewPanelProps = {
363
363
  initialZoom?: number;
364
364
  className?: string;
365
- /** 外部传入的完整工作区数据,挂载后自动渲染 */
365
+ /** 外部传入的完整工作区数据,挂载后按这份数据完整渲染;空则显示空白,不自动打开 IndexedDB 记录 */
366
366
  initialWorkspace?: WorkspaceProjectRecord | null;
367
367
  /** 界面语言;省略时按 localStorage / 浏览器语言解析 */
368
368
  locale?: Locale | null;
369
369
  onLocaleChange?: (locale: Locale) => void;
370
+ /**
371
+ * 隔离本实例的 IndexedDB / localStorage / BroadcastChannel。
372
+ * 同一页面挂多个 App 时传入不同值,避免工作区、蓝图库、预览缓存互相覆盖。
373
+ * 省略或空字符串保持历史全局库名。
374
+ */
375
+ nameSpace?: string | null;
370
376
  };
371
377
 
372
378
  function ReactViewPanelInner({
373
379
  initialZoom = 1,
374
380
  className,
375
381
  initialWorkspace = null,
382
+ nameSpace = null,
376
383
  }: ReactViewPanelProps) {
377
384
  const { t, locale, setLocale } = useI18n();
378
385
  const messages = useMemo(() => getPanelMessages(t), [t]);
379
- const THEME_STORAGE_KEY = "panel:theme";
380
- const TITLE_ICON_STORAGE_KEY = "panel:titleIconDataUrl";
386
+ const THEME_STORAGE_KEY = appStorageKey("panel:theme", nameSpace);
387
+ const TITLE_ICON_STORAGE_KEY = appStorageKey("panel:titleIconDataUrl", nameSpace);
388
+ const FONT_SIZE_STORAGE_KEY = appStorageKey("panel:fontSize", nameSpace);
381
389
  const { setTheme } = useTheme();
382
390
  const themedScrollbarClass =
383
391
  "scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
@@ -483,7 +491,7 @@ function ReactViewPanelInner({
483
491
  const [titleIconZoom, setTitleIconZoom] = useState(1.6);
484
492
  const [pendingPreviewLayerId, setPendingPreviewLayerId] = useState<string | null>(null);
485
493
  const [panelFontSize, setPanelFontSize] = useState<"sm" | "md" | "lg">("md");
486
- const [outputScale, setOutputScale] = useState(() => readOutputScale());
494
+ const [outputScale, setOutputScale] = useState(() => readOutputScale(nameSpace));
487
495
  const [blueprintOpen, setBlueprintOpen] = useState(false);
488
496
  const [assistantOpen, setAssistantOpen] = useState(false);
489
497
  const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
@@ -577,35 +585,26 @@ function ReactViewPanelInner({
577
585
 
578
586
  useEffect(() => {
579
587
  try {
580
- const stored = localStorage.getItem("panel:fontSize");
588
+ const stored = localStorage.getItem(FONT_SIZE_STORAGE_KEY);
581
589
  if (stored === "sm" || stored === "md" || stored === "lg") {
582
590
  setPanelFontSize(stored);
583
591
  }
584
592
  } catch {
585
593
  // ignore storage errors
586
594
  }
587
- }, []);
595
+ }, [FONT_SIZE_STORAGE_KEY]);
588
596
 
589
597
  useEffect(() => {
590
598
  try {
591
- localStorage.setItem("panel:fontSize", panelFontSize);
599
+ localStorage.setItem(FONT_SIZE_STORAGE_KEY, panelFontSize);
592
600
  } catch {
593
601
  // ignore storage errors
594
602
  }
595
- }, [panelFontSize]);
603
+ }, [FONT_SIZE_STORAGE_KEY, panelFontSize]);
596
604
 
597
605
  useEffect(() => {
598
- writeOutputScale(outputScale);
599
- }, [outputScale]);
600
-
601
- useEffect(() => {
602
- try {
603
- const stored = localStorage.getItem(TITLE_ICON_STORAGE_KEY) ?? "";
604
- if (stored) setTitleIconDataUrl(stored);
605
- } catch {
606
- // ignore storage errors
607
- }
608
- }, [TITLE_ICON_STORAGE_KEY]);
606
+ writeOutputScale(outputScale, nameSpace);
607
+ }, [nameSpace, outputScale]);
609
608
 
610
609
  useEffect(() => {
611
610
  try {
@@ -865,13 +864,13 @@ function ReactViewPanelInner({
865
864
 
866
865
  const resolveLibraryBlueprint = useCallback<LibraryBlueprintResolver>(
867
866
  async (libraryBlueprintId) => {
868
- const record = await getBlueprintLibraryRecord(libraryBlueprintId);
867
+ const record = await getBlueprintLibraryRecord(libraryBlueprintId, nameSpace);
869
868
  if (!record) return null;
870
- const items = await listBlueprintLibrary();
869
+ const items = await listBlueprintLibrary(nameSpace);
871
870
  const nameById = new Map(items.map((item) => [item.id, item.name]));
872
871
  return documentToRunnableGraph(record.document, { libraryNameById: nameById });
873
872
  },
874
- []
873
+ [nameSpace]
875
874
  );
876
875
 
877
876
  const handleBlueprintExecutionBlocked = useCallback((message: string) => {
@@ -893,6 +892,7 @@ function ReactViewPanelInner({
893
892
  libraryNameById: blueprintLibraryNameById,
894
893
  onExecutionBlocked: handleBlueprintExecutionBlocked,
895
894
  onViewScopeUpdate: handleViewScopeUpdate,
895
+ nameSpace,
896
896
  });
897
897
 
898
898
  const { selectLifecycleNode, selectedLifecycleNodeId: debugLifecycleNodeId } =
@@ -994,9 +994,9 @@ function ReactViewPanelInner({
994
994
  );
995
995
 
996
996
  const refreshBlueprintLibrary = useCallback(async () => {
997
- const items = await listBlueprintLibrary();
997
+ const items = await listBlueprintLibrary(nameSpace);
998
998
  setBlueprintLibraryItems(items);
999
- }, []);
999
+ }, [nameSpace]);
1000
1000
 
1001
1001
  useEffect(() => {
1002
1002
  void refreshBlueprintLibrary();
@@ -1004,7 +1004,7 @@ function ReactViewPanelInner({
1004
1004
 
1005
1005
  const loadBlueprintFromLibrary = useCallback(
1006
1006
  async (id: string) => {
1007
- const record = await getBlueprintLibraryRecord(id);
1007
+ const record = await getBlueprintLibraryRecord(id, nameSpace);
1008
1008
  if (!record) {
1009
1009
  toast({ title: t("panel.messages.blueprintNotFound") });
1010
1010
  void refreshBlueprintLibrary();
@@ -1023,7 +1023,7 @@ function ReactViewPanelInner({
1023
1023
  setBlueprintOpen(true);
1024
1024
  });
1025
1025
  },
1026
- [refreshBlueprintLibrary, t]
1026
+ [nameSpace, refreshBlueprintLibrary, t]
1027
1027
  );
1028
1028
 
1029
1029
  const snapshotWorkspaceBlueprint = useCallback(() => {
@@ -1078,7 +1078,7 @@ function ReactViewPanelInner({
1078
1078
  const syncBlueprintToLibrary = useCallback(async () => {
1079
1079
  if (!activeBlueprintLibraryId) return;
1080
1080
  await runBusyTask(t("common.syncingBlueprint"), async () => {
1081
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1081
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId, nameSpace);
1082
1082
  if (!existing) {
1083
1083
  toast({ title: t("panel.messages.blueprintNotFound") });
1084
1084
  void refreshBlueprintLibrary();
@@ -1094,7 +1094,7 @@ function ReactViewPanelInner({
1094
1094
  meta: blueprintMeta,
1095
1095
  source: existing.source,
1096
1096
  });
1097
- await putBlueprintLibraryRecord(record);
1097
+ await putBlueprintLibraryRecord(record, nameSpace);
1098
1098
  setBlueprintSyncedDocument(blueprintGraph.document);
1099
1099
  await refreshBlueprintLibrary();
1100
1100
  toast({ title: messages.blueprintSynced(record.name) });
@@ -1105,6 +1105,7 @@ function ReactViewPanelInner({
1105
1105
  blueprintMeta,
1106
1106
  refreshBlueprintLibrary,
1107
1107
  t,
1108
+ nameSpace,
1108
1109
  ]);
1109
1110
 
1110
1111
  const saveBlueprintToLibrary = useCallback(
@@ -1115,7 +1116,7 @@ function ReactViewPanelInner({
1115
1116
  let recordId = activeBlueprintLibraryId ?? undefined;
1116
1117
  let createdAt: number | undefined;
1117
1118
  if (activeBlueprintLibraryId) {
1118
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1119
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId, nameSpace);
1119
1120
  if (existing?.source === "saved") {
1120
1121
  recordId = existing.id;
1121
1122
  createdAt = existing.createdAt;
@@ -1133,14 +1134,14 @@ function ReactViewPanelInner({
1133
1134
  meta,
1134
1135
  source: "saved",
1135
1136
  });
1136
- await putBlueprintLibraryRecord(record);
1137
+ await putBlueprintLibraryRecord(record, nameSpace);
1137
1138
  setActiveBlueprintLibraryId(record.id);
1138
1139
  setBlueprintSyncedDocument(blueprintGraph.document);
1139
1140
  await refreshBlueprintLibrary();
1140
1141
  toast({ title: t("panel.messages.blueprintSavedLocal") });
1141
1142
  });
1142
1143
  },
1143
- [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary, t]
1144
+ [activeBlueprintLibraryId, blueprintGraph.document, nameSpace, refreshBlueprintLibrary, t]
1144
1145
  );
1145
1146
 
1146
1147
  const openBlueprintMetaDialog = useCallback((mode: "export" | "save") => {
@@ -1167,7 +1168,7 @@ function ReactViewPanelInner({
1167
1168
 
1168
1169
  const handleRenameBlueprintLibraryItem = useCallback(
1169
1170
  async (id: string, name: string) => {
1170
- const updated = await updateBlueprintLibraryMeta(id, { name });
1171
+ const updated = await updateBlueprintLibraryMeta(id, { name }, nameSpace);
1171
1172
  if (!updated) {
1172
1173
  toast({ title: t("panel.messages.renameFailed") });
1173
1174
  void refreshBlueprintLibrary();
@@ -1179,12 +1180,12 @@ function ReactViewPanelInner({
1179
1180
  }
1180
1181
  toast({ title: t("panel.messages.blueprintRenamed") });
1181
1182
  },
1182
- [activeBlueprintLibraryId, refreshBlueprintLibrary]
1183
+ [activeBlueprintLibraryId, nameSpace, refreshBlueprintLibrary]
1183
1184
  );
1184
1185
 
1185
1186
  const handleDeleteBlueprintLibraryItem = useCallback(
1186
1187
  async (id: string) => {
1187
- await deleteBlueprintLibraryRecord(id);
1188
+ await deleteBlueprintLibraryRecord(id, nameSpace);
1188
1189
  await refreshBlueprintLibrary();
1189
1190
  if (activeBlueprintLibraryId === id) {
1190
1191
  setActiveBlueprintLibraryId(null);
@@ -1192,7 +1193,7 @@ function ReactViewPanelInner({
1192
1193
  }
1193
1194
  toast({ title: t("panel.messages.blueprintDeletedFromLibrary") });
1194
1195
  },
1195
- [activeBlueprintLibraryId, refreshBlueprintLibrary]
1196
+ [activeBlueprintLibraryId, nameSpace, refreshBlueprintLibrary]
1196
1197
  );
1197
1198
 
1198
1199
  const openBlueprintExportDialog = useCallback(() => {
@@ -1207,7 +1208,7 @@ function ReactViewPanelInner({
1207
1208
  const text = await file.text();
1208
1209
  const payload = parseBlueprintImportFile(await parseJsonText(text));
1209
1210
  const record = libraryRecordFromImport(payload);
1210
- await putBlueprintLibraryRecord(record);
1211
+ await putBlueprintLibraryRecord(record, nameSpace);
1211
1212
  await refreshBlueprintLibrary();
1212
1213
  if (!activeBlueprintLibraryId) {
1213
1214
  snapshotWorkspaceBlueprint();
@@ -1224,6 +1225,7 @@ function ReactViewPanelInner({
1224
1225
  [
1225
1226
  activeBlueprintLibraryId,
1226
1227
  loadBlueprintFromLibrary,
1228
+ nameSpace,
1227
1229
  refreshBlueprintLibrary,
1228
1230
  snapshotWorkspaceBlueprint,
1229
1231
  t,
@@ -1267,6 +1269,7 @@ function ReactViewPanelInner({
1267
1269
  panelRevision: `${historyCursor}|${allElements.length}`,
1268
1270
  onProjectApplied: handleWorkspaceProjectApplied,
1269
1271
  initialWorkspace,
1272
+ nameSpace,
1270
1273
  });
1271
1274
 
1272
1275
  useEffect(() => {
@@ -1276,11 +1279,11 @@ function ReactViewPanelInner({
1276
1279
  allElements,
1277
1280
  layers,
1278
1281
  activeLayerId,
1279
- outputScale: readOutputScale(),
1282
+ outputScale: readOutputScale(nameSpace),
1280
1283
  });
1281
1284
  });
1282
1285
  return () => registerPreviewSnapshotProvider(null);
1283
- }, [activeLayerId, allElements, canvasEl, layers]);
1286
+ }, [activeLayerId, allElements, canvasEl, layers, nameSpace]);
1284
1287
 
1285
1288
  const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
1286
1289
  graph: blueprintGraph,
@@ -3589,11 +3592,12 @@ function ReactViewPanelInner({
3589
3592
  export function ReactViewPanel({
3590
3593
  locale,
3591
3594
  onLocaleChange,
3595
+ nameSpace,
3592
3596
  ...rest
3593
3597
  }: ReactViewPanelProps) {
3594
3598
  return (
3595
- <I18nRoot locale={locale} onLocaleChange={onLocaleChange}>
3596
- <ReactViewPanelInner {...rest} />
3599
+ <I18nRoot locale={locale} onLocaleChange={onLocaleChange} nameSpace={nameSpace}>
3600
+ <ReactViewPanelInner nameSpace={nameSpace} {...rest} />
3597
3601
  </I18nRoot>
3598
3602
  );
3599
3603
  }
@@ -21,6 +21,8 @@ import {
21
21
  } from "../library/workspace-snapshot";
22
22
  import { runBusyTask } from "../utils/async-work";
23
23
  import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
24
+ import { parseBlueprintData } from "@arronqzy/react-blueprint";
25
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
24
26
 
25
27
  export type UseWorkspaceProjectsOptions = {
26
28
  exportPanelData: () => State;
@@ -39,13 +41,20 @@ export type UseWorkspaceProjectsOptions = {
39
41
  onProjectApplied?: (record: WorkspaceProjectRecord) => void;
40
42
  /** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
41
43
  initialWorkspace?: WorkspaceProjectRecord | null;
44
+ /** 隔离 IndexedDB / 预览缓存 / 同步频道 */
45
+ nameSpace?: string | null;
42
46
  };
43
47
 
44
- export function buildOnlinePreviewUrl(projectId: string): string {
48
+ export function buildOnlinePreviewUrl(
49
+ projectId: string,
50
+ nameSpace?: string | null
51
+ ): string {
45
52
  const url = new URL(window.location.href);
46
53
  url.search = "";
47
54
  url.searchParams.set("preview", "online");
48
55
  url.searchParams.set("projectId", projectId);
56
+ const ns = (nameSpace ?? "").trim();
57
+ if (ns) url.searchParams.set("ns", ns);
49
58
  const previewInstanceId =
50
59
  typeof crypto !== "undefined" && "randomUUID" in crypto
51
60
  ? crypto.randomUUID()
@@ -54,6 +63,28 @@ export function buildOnlinePreviewUrl(projectId: string): string {
54
63
  return url.toString();
55
64
  }
56
65
 
66
+ function previewWindowName(projectId: string, nameSpace?: string | null): string {
67
+ const ns = (nameSpace ?? "").trim();
68
+ return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
69
+ }
70
+
71
+ function hasWorkspacePayload(
72
+ record?: WorkspaceProjectRecord | null
73
+ ): record is WorkspaceProjectRecord {
74
+ if (!record || typeof record !== "object") return false;
75
+ return Boolean(
76
+ record.panelState ||
77
+ record.blueprintDocument ||
78
+ record.id ||
79
+ record.productName ||
80
+ record.titleIconDataUrl
81
+ );
82
+ }
83
+
84
+ function workspaceApplyKey(record: WorkspaceProjectRecord): string {
85
+ return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
86
+ }
87
+
57
88
  export function useWorkspaceProjects({
58
89
  exportPanelData,
59
90
  importPanelData,
@@ -68,6 +99,7 @@ export function useWorkspaceProjects({
68
99
  panelRevision,
69
100
  onProjectApplied,
70
101
  initialWorkspace,
102
+ nameSpace = null,
71
103
  }: UseWorkspaceProjectsOptions) {
72
104
  const { t } = useI18nOptional();
73
105
  const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
@@ -99,7 +131,8 @@ export function useWorkspaceProjects({
99
131
  }, [refreshPreviewing]);
100
132
 
101
133
  const resolveProjectName = useCallback(() => {
102
- return productName.trim() || t("panel.defaults.unnamedProduct");
134
+ return (typeof productName === "string" ? productName : "").trim() ||
135
+ t("panel.defaults.unnamedProduct");
103
136
  }, [productName, t]);
104
137
 
105
138
  const buildCurrentSnapshot = useCallback((): WorkspaceSnapshot => {
@@ -112,9 +145,9 @@ export function useWorkspaceProjects({
112
145
  }, [blueprintDocument, blueprintMeta, productName, titleIconDataUrl]);
113
146
 
114
147
  const refreshProjects = useCallback(async () => {
115
- const items = await listWorkspaceProjects();
148
+ const items = await listWorkspaceProjects(nameSpace);
116
149
  setProjects(items);
117
- }, []);
150
+ }, [nameSpace]);
118
151
 
119
152
  useEffect(() => {
120
153
  void refreshProjects();
@@ -143,25 +176,33 @@ export function useWorkspaceProjects({
143
176
 
144
177
  const applyProjectRecord = useCallback(
145
178
  (record: WorkspaceProjectRecord, options?: { notifyApplied?: boolean }) => {
179
+ const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
146
180
  const blueprintDocument =
147
- record.blueprintDocument ?? BlueprintGraph.empty().document;
181
+ parsedBlueprint.value ?? BlueprintGraph.empty().document;
148
182
  const blueprintMeta: BlueprintMetaDraft = {
149
183
  name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
150
184
  remark: record.blueprintMeta?.remark ?? "",
151
185
  };
186
+ const panelState =
187
+ normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
188
+ const productName =
189
+ typeof record.productName === "string" && record.productName.trim()
190
+ ? record.productName
191
+ : t("panel.defaults.unnamedProduct");
192
+ const titleIconDataUrl = record.titleIconDataUrl ?? "";
152
193
 
153
- importPanelData(record.panelState);
194
+ importPanelData(panelState);
154
195
  setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
155
196
  setBlueprintMeta(blueprintMeta);
156
- setProductName(record.productName);
157
- setTitleIconDataUrl(record.titleIconDataUrl ?? "");
158
- setActiveProjectId(record.id);
159
- setActiveProjectName(record.name);
197
+ setProductName(productName);
198
+ setTitleIconDataUrl(titleIconDataUrl);
199
+ setActiveProjectId(record.id || null);
200
+ setActiveProjectName(record.name ?? null);
160
201
  syncedSnapshotRef.current = cloneWorkspaceSnapshot({
161
202
  blueprintDocument,
162
203
  blueprintMeta,
163
- productName: record.productName,
164
- titleIconDataUrl: record.titleIconDataUrl ?? "",
204
+ productName,
205
+ titleIconDataUrl,
165
206
  });
166
207
  syncedPanelRevisionRef.current = panelRevision;
167
208
  pendingRevisionSyncRef.current = true;
@@ -170,6 +211,9 @@ export function useWorkspaceProjects({
170
211
  ...record,
171
212
  blueprintDocument,
172
213
  blueprintMeta,
214
+ panelState,
215
+ productName,
216
+ titleIconDataUrl: titleIconDataUrl || undefined,
173
217
  });
174
218
  }
175
219
  },
@@ -185,14 +229,46 @@ export function useWorkspaceProjects({
185
229
  ]
186
230
  );
187
231
 
232
+ const applyEmptyWorkspace = useCallback(() => {
233
+ importPanelData(createEmptyPanelState());
234
+ setBlueprintGraph(BlueprintGraph.empty());
235
+ setBlueprintMeta({
236
+ name: t("panel.defaults.unnamedBlueprint"),
237
+ remark: "",
238
+ });
239
+ setProductName(t("panel.defaults.unnamedProduct"));
240
+ setTitleIconDataUrl("");
241
+ setActiveProjectId(null);
242
+ setActiveProjectName(null);
243
+ syncedSnapshotRef.current = null;
244
+ }, [
245
+ importPanelData,
246
+ setBlueprintGraph,
247
+ setBlueprintMeta,
248
+ setProductName,
249
+ setTitleIconDataUrl,
250
+ t,
251
+ ]);
252
+
188
253
  const initialWorkspaceAppliedKeyRef = useRef<string | null>(null);
254
+ const bootstrappedRef = useRef(false);
189
255
  useEffect(() => {
190
- if (!initialWorkspace?.id) return;
191
- const key = `${initialWorkspace.id}:${initialWorkspace.updatedAt}`;
256
+ if (!bootstrappedRef.current) {
257
+ bootstrappedRef.current = true;
258
+ if (hasWorkspacePayload(initialWorkspace)) {
259
+ applyProjectRecord(initialWorkspace, { notifyApplied: false });
260
+ initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
261
+ } else {
262
+ applyEmptyWorkspace();
263
+ }
264
+ return;
265
+ }
266
+ if (!hasWorkspacePayload(initialWorkspace)) return;
267
+ const key = workspaceApplyKey(initialWorkspace);
192
268
  if (initialWorkspaceAppliedKeyRef.current === key) return;
193
269
  initialWorkspaceAppliedKeyRef.current = key;
194
270
  applyProjectRecord(initialWorkspace, { notifyApplied: false });
195
- }, [applyProjectRecord, initialWorkspace]);
271
+ }, [applyEmptyWorkspace, applyProjectRecord, initialWorkspace]);
196
272
 
197
273
  const persistProject = useCallback(
198
274
  async (options: { id?: string; name: string; createdAt?: number }) => {
@@ -200,7 +276,7 @@ export function useWorkspaceProjects({
200
276
  const now = Date.now();
201
277
  const id = options.id ?? createWorkspaceProjectId();
202
278
  const createdAt = options.createdAt ?? now;
203
- const name = options.name.trim() || resolveProjectName();
279
+ const name = (options.name ?? "").trim() || resolveProjectName();
204
280
 
205
281
  const record: WorkspaceProjectRecord = {
206
282
  id,
@@ -210,13 +286,14 @@ export function useWorkspaceProjects({
210
286
  panelState: exportPanelData(),
211
287
  blueprintDocument: snapshot.blueprintDocument,
212
288
  blueprintMeta: snapshot.blueprintMeta,
213
- productName: snapshot.productName,
289
+ productName:
290
+ typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
214
291
  titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
215
292
  };
216
293
 
217
- await putWorkspaceProject(record);
218
- writeWorkspacePreviewCache(record);
219
- broadcastWorkspaceProjectUpdate(id, now);
294
+ await putWorkspaceProject(record, nameSpace);
295
+ writeWorkspacePreviewCache(record, nameSpace);
296
+ broadcastWorkspaceProjectUpdate(id, now, nameSpace);
220
297
  setActiveProjectId(id);
221
298
  setActiveProjectName(name);
222
299
  syncedSnapshotRef.current = snapshot;
@@ -225,7 +302,7 @@ export function useWorkspaceProjects({
225
302
  await refreshProjects();
226
303
  return record;
227
304
  },
228
- [buildCurrentSnapshot, exportPanelData, panelRevision, resolveProjectName, refreshProjects]
305
+ [buildCurrentSnapshot, exportPanelData, nameSpace, panelRevision, resolveProjectName, refreshProjects]
229
306
  );
230
307
 
231
308
  const handleCreateProject = useCallback(async () => {
@@ -242,7 +319,7 @@ export function useWorkspaceProjects({
242
319
  return handleCreateProject();
243
320
  }
244
321
  return runBusyTask(t("common.savingWorkspace"), async () => {
245
- const existing = await getWorkspaceProject(activeProjectId);
322
+ const existing = await getWorkspaceProject(activeProjectId, nameSpace);
246
323
  const name = resolveProjectName();
247
324
  const record = await persistProject({
248
325
  id: activeProjectId,
@@ -251,12 +328,12 @@ export function useWorkspaceProjects({
251
328
  });
252
329
  return { name, id: record.id };
253
330
  });
254
- }, [activeProjectId, handleCreateProject, persistProject, resolveProjectName, t]);
331
+ }, [activeProjectId, handleCreateProject, nameSpace, persistProject, resolveProjectName, t]);
255
332
 
256
333
  const handleOpenProject = useCallback(
257
334
  async (id: string) => {
258
335
  await runBusyTask(t("common.loadingWorkspace"), async () => {
259
- const record = await getWorkspaceProject(id);
336
+ const record = await getWorkspaceProject(id, nameSpace);
260
337
  if (!record) {
261
338
  await refreshProjects();
262
339
  throw new Error(t("panel.messages.workspaceNotFound"));
@@ -264,7 +341,7 @@ export function useWorkspaceProjects({
264
341
  applyProjectRecord(record);
265
342
  });
266
343
  },
267
- [applyProjectRecord, refreshProjects, t]
344
+ [applyProjectRecord, nameSpace, refreshProjects, t]
268
345
  );
269
346
 
270
347
  const handleSyncProject = useCallback(async () => {
@@ -272,7 +349,7 @@ export function useWorkspaceProjects({
272
349
  if (!activeProjectId) {
273
350
  throw new Error(t("panel.messages.saveWorkspaceFirst"));
274
351
  }
275
- const existing = await getWorkspaceProject(activeProjectId);
352
+ const existing = await getWorkspaceProject(activeProjectId, nameSpace);
276
353
  if (!existing) {
277
354
  setActiveProjectId(null);
278
355
  setActiveProjectName(null);
@@ -289,12 +366,12 @@ export function useWorkspaceProjects({
289
366
  emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
290
367
  return name;
291
368
  });
292
- }, [activeProjectId, persistProject, refreshProjects, resolveProjectName, t]);
369
+ }, [activeProjectId, nameSpace, persistProject, refreshProjects, resolveProjectName, t]);
293
370
 
294
371
  const handleDeleteProject = useCallback(
295
372
  async (id: string) => {
296
373
  await runBusyTask(t("common.deletingWorkspace"), async () => {
297
- await deleteWorkspaceProject(id);
374
+ await deleteWorkspaceProject(id, nameSpace);
298
375
  if (activeProjectId === id) {
299
376
  setActiveProjectId(null);
300
377
  setActiveProjectName(null);
@@ -305,7 +382,7 @@ export function useWorkspaceProjects({
305
382
  await refreshProjects();
306
383
  });
307
384
  },
308
- [activeProjectId, refreshPreviewing, refreshProjects, t]
385
+ [activeProjectId, nameSpace, refreshPreviewing, refreshProjects, t]
309
386
  );
310
387
 
311
388
  const handlePreviewProject = useCallback(
@@ -315,9 +392,10 @@ export function useWorkspaceProjects({
315
392
  await handleSyncProject();
316
393
  }
317
394
  const record =
318
- (await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
395
+ (await getWorkspaceProject(id, nameSpace)) ??
396
+ readWorkspacePreviewCache(id, nameSpace);
319
397
  if (record) {
320
- writeWorkspacePreviewCache(record);
398
+ writeWorkspacePreviewCache(record, nameSpace);
321
399
  }
322
400
  const existing = previewWindowsRef.current.get(id);
323
401
  if (existing && !existing.closed) {
@@ -330,8 +408,8 @@ export function useWorkspaceProjects({
330
408
  return;
331
409
  }
332
410
  const win = window.open(
333
- buildOnlinePreviewUrl(id),
334
- `abuilder-preview-${id}`
411
+ buildOnlinePreviewUrl(id, nameSpace),
412
+ previewWindowName(id, nameSpace)
335
413
  );
336
414
  if (win) {
337
415
  previewWindowsRef.current.set(id, win);
@@ -345,7 +423,7 @@ export function useWorkspaceProjects({
345
423
  refreshPreviewing();
346
424
  });
347
425
  },
348
- [activeProjectId, dirty, handleSyncProject, refreshPreviewing, t]
426
+ [activeProjectId, dirty, handleSyncProject, nameSpace, refreshPreviewing, t]
349
427
  );
350
428
 
351
429
  return {
@@ -2,4 +2,18 @@ export * from "./ReactViewPanel";
2
2
  export * from "./ReactViewOnlinePreview";
3
3
  export { I18nRoot, I18nProvider } from "./I18nRoot";
4
4
  export type { I18nRootProps, Locale } from "./I18nRoot";
5
- export type * from "./types";
5
+ export type * from "./types";
6
+ export {
7
+ parseViewData,
8
+ validateViewData,
9
+ parseBlueprintData,
10
+ validateBlueprintData,
11
+ validateWorkspaceData,
12
+ parseWorkspaceData,
13
+ createEmptyWorkspace,
14
+ } from "./library/parse-workspace-data";
15
+ export { createWorkspaceProjectId } from "./library/workspace-project-db";
16
+ export type {
17
+ ParseCheckResult,
18
+ WorkspaceParseCheckResult,
19
+ } from "./library/parse-workspace-data";
@@ -0,0 +1,142 @@
1
+ import {
2
+ BlueprintGraph,
3
+ parseBlueprintData,
4
+ validateBlueprintData,
5
+ type BlueprintDocument,
6
+ type BlueprintMetaDraft,
7
+ type ParseCheckResult,
8
+ } from "@arronqzy/react-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,12 +1,20 @@
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
  // 只写轻量标记,避免把含 data URL 的整包再塞进 localStorage(配额小、内存翻倍)
8
16
  localStorage.setItem(
9
- CACHE_PREFIX + record.id,
17
+ previewCacheKey(record.id, nameSpace),
10
18
  JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
11
19
  );
12
20
  } catch {
@@ -15,10 +23,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
15
23
  }
16
24
 
17
25
  export function readWorkspacePreviewCache(
18
- projectId: string
26
+ projectId: string,
27
+ nameSpace?: string | null
19
28
  ): WorkspaceProjectRecord | null {
20
29
  try {
21
- const raw = localStorage.getItem(CACHE_PREFIX + projectId);
30
+ const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
22
31
  if (!raw) return null;
23
32
  const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
24
33
  if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
@@ -1,6 +1,7 @@
1
1
  import type { BlueprintDocument } from "@arronqzy/react-blueprint";
2
2
  import type { BlueprintMetaDraft } from "@arronqzy/react-blueprint";
3
3
  import type { State } from "@arronqzy/rx-store";
4
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
4
5
 
5
6
  const DB_NAME = "arronqzy-workspace-projects";
6
7
  const DB_VERSION = 1;
@@ -25,9 +26,9 @@ export type WorkspaceProjectRecord = {
25
26
  titleIconDataUrl?: string;
26
27
  };
27
28
 
28
- function openDb(): Promise<IDBDatabase> {
29
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
29
30
  return new Promise((resolve, reject) => {
30
- const request = indexedDB.open(DB_NAME, DB_VERSION);
31
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
31
32
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
32
33
  request.onupgradeneeded = () => {
33
34
  const db = request.result;
@@ -42,9 +43,10 @@ function openDb(): Promise<IDBDatabase> {
42
43
 
43
44
  function runTransaction<T>(
44
45
  mode: IDBTransactionMode,
45
- runner: (store: IDBObjectStore) => IDBRequest<T>
46
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
47
+ nameSpace?: string | null
46
48
  ): Promise<T> {
47
- return openDb().then(
49
+ return openDb(nameSpace).then(
48
50
  (db) =>
49
51
  new Promise<T>((resolve, reject) => {
50
52
  const tx = db.transaction(STORE_NAME, mode);
@@ -77,8 +79,10 @@ export function createWorkspaceProjectId(): string {
77
79
  return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
78
80
  }
79
81
 
80
- export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
81
- const db = await openDb();
82
+ export async function listWorkspaceProjects(
83
+ nameSpace?: string | null
84
+ ): Promise<WorkspaceProjectListItem[]> {
85
+ const db = await openDb(nameSpace);
82
86
  return new Promise((resolve, reject) => {
83
87
  const tx = db.transaction(STORE_NAME, "readonly");
84
88
  const store = tx.objectStore(STORE_NAME);
@@ -110,21 +114,28 @@ export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[
110
114
  }
111
115
 
112
116
  export async function getWorkspaceProject(
113
- id: string
117
+ id: string,
118
+ nameSpace?: string | null
114
119
  ): Promise<WorkspaceProjectRecord | null> {
115
- const record = await runTransaction<WorkspaceProjectRecord | undefined>("readonly", (store) =>
116
- store.get(id)
120
+ const record = await runTransaction<WorkspaceProjectRecord | undefined>(
121
+ "readonly",
122
+ (store) => store.get(id),
123
+ nameSpace
117
124
  );
118
125
  return record ?? null;
119
126
  }
120
127
 
121
128
  export async function putWorkspaceProject(
122
- record: WorkspaceProjectRecord
129
+ record: WorkspaceProjectRecord,
130
+ nameSpace?: string | null
123
131
  ): Promise<WorkspaceProjectRecord> {
124
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
132
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
125
133
  return record;
126
134
  }
127
135
 
128
- export async function deleteWorkspaceProject(id: string): Promise<void> {
129
- await runTransaction<undefined>("readwrite", (store) => store.delete(id));
136
+ export async function deleteWorkspaceProject(
137
+ id: string,
138
+ nameSpace?: string | null
139
+ ): Promise<void> {
140
+ await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
130
141
  }
@@ -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,17 +1,22 @@
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
 
12
- export function writeOutputScale(enabled: boolean) {
14
+ export function writeOutputScale(enabled: boolean, nameSpace?: string | null) {
13
15
  try {
14
- localStorage.setItem(OUTPUT_SCALE_STORAGE_KEY, enabled ? "1" : "0");
16
+ localStorage.setItem(
17
+ appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace),
18
+ enabled ? "1" : "0"
19
+ );
15
20
  } catch {
16
21
  // ignore storage errors
17
22
  }
@@ -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) => {