@arronqzy/react-view 1.0.20 → 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/dist/styles.css +45 -0
- package/package.json +5 -5
- package/readme.md +6 -1
- package/src/panel/I18nRoot.tsx +14 -2
- package/src/panel/ReactViewOnlinePreview.tsx +29 -18
- package/src/panel/ReactViewPanel.tsx +65 -59
- package/src/panel/ai/applyAssistantAction.ts +1 -0
- package/src/panel/components/ElementsLayer.tsx +67 -16
- package/src/panel/components/MaterialSidebar.tsx +18 -11
- package/src/panel/components/PanelConfigSidebar.tsx +139 -1
- package/src/panel/components/WorkspaceConfigSidebar.tsx +1 -0
- package/src/panel/hooks/usePanelElements.ts +10 -4
- package/src/panel/hooks/useWorkspaceProjects.ts +112 -34
- package/src/panel/index.tsx +15 -1
- package/src/panel/library/capture-preview-snapshot.ts +4 -2
- 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/types.ts +4 -0
- package/src/panel/utils/outputScale.ts +9 -4
- package/src/panel/utils/panelElementDefaults.ts +10 -0
- package/src/panel/utils/panelElementNodes.ts +5 -3
- package/src/panel/utils/panelStateIO.ts +18 -0
- package/src/panel/utils/viewportPlacement.ts +73 -0
|
@@ -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";
|
|
@@ -620,7 +620,8 @@ async function paintPanelElementOntoCanvas(
|
|
|
620
620
|
liveNode &&
|
|
621
621
|
(el.materialType === "reference" ||
|
|
622
622
|
el.materialType === "table" ||
|
|
623
|
-
el.materialType === "grid"
|
|
623
|
+
el.materialType === "grid" ||
|
|
624
|
+
el.materialType === "viewport")
|
|
624
625
|
) {
|
|
625
626
|
try {
|
|
626
627
|
await drawNodeOntoCanvas(ctx, liveNode, 0, 0, w, h, 0);
|
|
@@ -681,7 +682,8 @@ export async function capturePreviewLayerSnapshot(
|
|
|
681
682
|
ctx.fillStyle = options.backgroundColor ?? "#ffffff";
|
|
682
683
|
ctx.fillRect(0, 0, width, height);
|
|
683
684
|
|
|
684
|
-
const sorted = [...elements]
|
|
685
|
+
const sorted = [...elements]
|
|
686
|
+
.sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1));
|
|
685
687
|
for (const el of sorted) {
|
|
686
688
|
const liveNode =
|
|
687
689
|
options.canvasRoot?.querySelector<HTMLElement>(
|
|
@@ -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;
|