@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 +6 -6
- package/readme.md +6 -1
- package/src/panel/I18nRoot.tsx +14 -2
- package/src/panel/ReactViewOnlinePreview.tsx +29 -18
- package/src/panel/ReactViewPanel.tsx +45 -41
- package/src/panel/hooks/useWorkspaceProjects.ts +112 -34
- package/src/panel/index.tsx +15 -1
- package/src/panel/library/parse-workspace-data.ts +142 -0
- package/src/panel/library/workspace-project-cache.ts +13 -4
- package/src/panel/library/workspace-project-db.ts +24 -13
- package/src/panel/library/workspace-project-sync.ts +17 -6
- package/src/panel/utils/outputScale.ts +9 -4
- package/src/panel/utils/panelStateIO.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/react-view",
|
|
3
|
-
"version": "1.0.
|
|
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/
|
|
34
|
+
"@arronqzy/ui": "1.0.6",
|
|
37
35
|
"@arronqzy/rx-store": "1.0.5",
|
|
38
|
-
"@arronqzy/
|
|
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/
|
|
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
|
|
package/src/panel/I18nRoot.tsx
CHANGED
|
@@ -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({
|
|
15
|
+
export function I18nRoot({
|
|
16
|
+
children,
|
|
17
|
+
locale,
|
|
18
|
+
onLocaleChange,
|
|
19
|
+
nameSpace,
|
|
20
|
+
}: I18nRootProps) {
|
|
13
21
|
return (
|
|
14
|
-
<I18nProvider
|
|
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(
|
|
72
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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() ||
|
|
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
|
-
|
|
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(
|
|
194
|
+
importPanelData(panelState);
|
|
154
195
|
setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
|
|
155
196
|
setBlueprintMeta(blueprintMeta);
|
|
156
|
-
setProductName(
|
|
157
|
-
setTitleIconDataUrl(
|
|
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
|
|
164
|
-
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 (!
|
|
191
|
-
|
|
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:
|
|
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)) ??
|
|
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
|
-
|
|
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 {
|
package/src/panel/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
81
|
-
|
|
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>(
|
|
116
|
-
|
|
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(
|
|
129
|
-
|
|
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(
|
|
10
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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) => {
|