@arronqzy/vue-view 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +5 -5
- package/src/index.ts +26 -0
- package/src/panel/VueViewOnlinePreview.vue +68 -26
- package/src/panel/VueViewPanel.vue +15 -10
- package/src/panel/hooks/useWorkspaceProjects.ts +144 -34
- package/src/panel/library/event-subscription.ts +60 -0
- 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/parseOnlinePreviewSearchParams.ts +3 -0
- package/src/panel/utils/outputScale.ts +15 -2
- package/src/panel/utils/panelStateIO.ts +18 -0
package/README.md
CHANGED
|
@@ -29,7 +29,21 @@ createApp(VueViewPanel).use(Antd).mount("#app");
|
|
|
29
29
|
import { VueViewOnlinePreview, parseOnlinePreviewSearchParams } from "@arronqzy/vue-view";
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
URL 参数:`?preview=online&projectId=<id>&pid=<instanceId
|
|
32
|
+
URL 参数:`?preview=online&projectId=<id>&pid=<instanceId>`。也可传入 `workspace` 直接预览宿主数据(优先于 `projectId`)。
|
|
33
|
+
|
|
34
|
+
### 宿主集成
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
addEventSubscription,
|
|
39
|
+
AbuilderEvents,
|
|
40
|
+
parseWorkspaceData,
|
|
41
|
+
createEmptyWorkspace,
|
|
42
|
+
createWorkspaceProjectId,
|
|
43
|
+
} from "@arronqzy/vue-view";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
创建 / 同步工作区会发出 `workspace:add` / `workspace:sync`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。IndexedDB CRUD 与 `getPreviewSnapshot` **不导出**(后者仅 React 实现)。
|
|
33
47
|
|
|
34
48
|
## 依赖
|
|
35
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"selecto": "^1.26.3",
|
|
24
24
|
"uuid": "^13.0.0",
|
|
25
25
|
"vue": "^3.5.13",
|
|
26
|
-
"@arronqzy/blueprint-dsl": "1.0.10",
|
|
27
|
-
"@arronqzy/rx-store": "1.0.5",
|
|
28
|
-
"@arronqzy/view-table": "0.1.5",
|
|
29
|
-
"@arronqzy/vue-blueprint": "0.1.8",
|
|
30
26
|
"@arronqzy/i18n": "0.1.8",
|
|
27
|
+
"@arronqzy/blueprint-dsl": "1.0.11",
|
|
28
|
+
"@arronqzy/rx-store": "1.0.5",
|
|
31
29
|
"@arronqzy/view-scene3d": "0.1.2",
|
|
30
|
+
"@arronqzy/view-table": "0.1.5",
|
|
31
|
+
"@arronqzy/vue-blueprint": "0.1.9",
|
|
32
32
|
"@arronqzy/vue-rx-store": "0.1.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
export { default as VueViewPanel } from "./panel/VueViewPanel.vue";
|
|
2
2
|
export { default as VueViewOnlinePreview } from "./panel/VueViewOnlinePreview.vue";
|
|
3
3
|
export { parseOnlinePreviewSearchParams } from "./panel/parseOnlinePreviewSearchParams";
|
|
4
|
+
export type { WorkspaceProjectRecord } from "./panel/library/workspace-project-db";
|
|
4
5
|
export type * from "./panel/types";
|
|
6
|
+
export {
|
|
7
|
+
parseViewData,
|
|
8
|
+
validateViewData,
|
|
9
|
+
parseBlueprintData,
|
|
10
|
+
validateBlueprintData,
|
|
11
|
+
validateWorkspaceData,
|
|
12
|
+
parseWorkspaceData,
|
|
13
|
+
createEmptyWorkspace,
|
|
14
|
+
} from "./panel/library/parse-workspace-data";
|
|
15
|
+
export { createWorkspaceProjectId } from "./panel/library/workspace-project-db";
|
|
16
|
+
export {
|
|
17
|
+
addEventSubscription,
|
|
18
|
+
AbuilderEvents,
|
|
19
|
+
} from "./panel/library/event-subscription";
|
|
20
|
+
export type {
|
|
21
|
+
AbuilderEventName,
|
|
22
|
+
AbuilderEventPayloadMap,
|
|
23
|
+
WorkspaceAddEventPayload,
|
|
24
|
+
WorkspaceSyncEventPayload,
|
|
25
|
+
WorkspaceData,
|
|
26
|
+
} from "./panel/library/event-subscription";
|
|
27
|
+
export type {
|
|
28
|
+
ParseCheckResult,
|
|
29
|
+
WorkspaceParseCheckResult,
|
|
30
|
+
} from "./panel/library/parse-workspace-data";
|
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
useViewScopeStoreVersion,
|
|
23
23
|
} from "./scope/view-scope-store";
|
|
24
24
|
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getWorkspaceProject,
|
|
27
|
+
type WorkspaceProjectRecord,
|
|
28
|
+
} from "./library/workspace-project-db";
|
|
26
29
|
import { readWorkspacePreviewCache } from "./library/workspace-project-cache";
|
|
27
30
|
import { subscribeWorkspaceProjectUpdates } from "./library/workspace-project-sync";
|
|
28
31
|
import {
|
|
@@ -41,8 +44,12 @@ const { t, locale } = useI18n();
|
|
|
41
44
|
const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
|
|
42
45
|
|
|
43
46
|
const props = defineProps<{
|
|
44
|
-
|
|
47
|
+
/** 从 IndexedDB / 缓存按 ID 加载;与 `workspace` 二选一,优先 `workspace` */
|
|
48
|
+
projectId?: string;
|
|
45
49
|
previewInstanceId?: string;
|
|
50
|
+
nameSpace?: string | null;
|
|
51
|
+
/** 直接传入完整工作区,用于宿主后端加载后预览 */
|
|
52
|
+
workspace?: WorkspaceProjectRecord | null;
|
|
46
53
|
}>();
|
|
47
54
|
|
|
48
55
|
const loadError = ref<string | null>(null);
|
|
@@ -53,7 +60,7 @@ const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
|
|
|
53
60
|
const layoutRevision = ref(0);
|
|
54
61
|
const layoutReady = ref(false);
|
|
55
62
|
const sceneRef = ref<HTMLDivElement | null>(null);
|
|
56
|
-
const outputScale = ref(readOutputScale());
|
|
63
|
+
const outputScale = ref(readOutputScale(props.nameSpace));
|
|
57
64
|
const fillScale = ref({ scaleX: 1, scaleY: 1 });
|
|
58
65
|
|
|
59
66
|
const layers = computed(() => (panelState.value ? parsePanelLayers(panelState.value) : []));
|
|
@@ -64,10 +71,19 @@ const allElements = computed(() =>
|
|
|
64
71
|
panelState.value ? parseAllPanelElements(panelState.value) : []
|
|
65
72
|
);
|
|
66
73
|
|
|
74
|
+
const resolvedProjectId = computed(
|
|
75
|
+
() => props.workspace?.id ?? props.projectId ?? ""
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
function workspaceTitle(record: WorkspaceProjectRecord) {
|
|
79
|
+
const product = typeof record.productName === "string" ? record.productName : "";
|
|
80
|
+
return product.trim() || record.name || t("panel.workspace.previewDocTitle");
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
async function loadWorkspaceRecord(projectId: string) {
|
|
68
|
-
const fromDb = await getWorkspaceProject(projectId);
|
|
84
|
+
const fromDb = await getWorkspaceProject(projectId, props.nameSpace);
|
|
69
85
|
if (fromDb) return fromDb;
|
|
70
|
-
return readWorkspacePreviewCache(projectId);
|
|
86
|
+
return readWorkspacePreviewCache(projectId, props.nameSpace);
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
function applyTitleIcon(titleIconDataUrl?: string) {
|
|
@@ -82,13 +98,7 @@ function applyTitleIcon(titleIconDataUrl?: string) {
|
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
const record = await loadWorkspaceRecord(props.projectId);
|
|
87
|
-
if (!record) {
|
|
88
|
-
loadError.value = t("panel.messages.workspaceNotFound");
|
|
89
|
-
panelState.value = null;
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
101
|
+
function applyWorkspaceRecord(record: WorkspaceProjectRecord) {
|
|
92
102
|
const normalized = normalizeImportedPanelState(record.panelState);
|
|
93
103
|
if (!normalized) {
|
|
94
104
|
loadError.value = t("panel.messages.workspaceDataInvalid");
|
|
@@ -99,24 +109,56 @@ async function loadProject() {
|
|
|
99
109
|
clearViewElementScopes();
|
|
100
110
|
panelState.value = normalized;
|
|
101
111
|
blueprintGraph.value = BlueprintGraph.fromDocument(record.blueprintDocument);
|
|
102
|
-
document.title =
|
|
112
|
+
document.title = workspaceTitle(record);
|
|
103
113
|
applyTitleIcon(record.titleIconDataUrl);
|
|
104
114
|
projectRevision.value += 1;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
async function loadProject() {
|
|
118
|
+
if (props.workspace) {
|
|
119
|
+
applyWorkspaceRecord(props.workspace);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!props.projectId) {
|
|
123
|
+
loadError.value = t("panel.messages.workspaceNotFound");
|
|
124
|
+
panelState.value = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const record = await loadWorkspaceRecord(props.projectId);
|
|
128
|
+
if (!record) {
|
|
129
|
+
loadError.value = t("panel.messages.workspaceNotFound");
|
|
130
|
+
panelState.value = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
applyWorkspaceRecord(record);
|
|
134
|
+
}
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
watch(
|
|
137
|
+
() => [props.workspace, props.projectId, props.nameSpace] as const,
|
|
138
|
+
() => {
|
|
113
139
|
void loadProject();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
},
|
|
141
|
+
{ immediate: true }
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
watch(
|
|
145
|
+
() => [props.workspace, props.projectId, props.nameSpace] as const,
|
|
146
|
+
([workspace, projectId, nameSpace], _prev, onCleanup) => {
|
|
147
|
+
if (workspace || !projectId) return;
|
|
148
|
+
const unsub = subscribeWorkspaceProjectUpdates(
|
|
149
|
+
projectId,
|
|
150
|
+
() => {
|
|
151
|
+
void loadProject();
|
|
152
|
+
},
|
|
153
|
+
nameSpace
|
|
154
|
+
);
|
|
155
|
+
onCleanup(unsub);
|
|
156
|
+
},
|
|
157
|
+
{ immediate: true }
|
|
158
|
+
);
|
|
117
159
|
|
|
118
160
|
watch(projectRevision, () => {
|
|
119
|
-
void listBlueprintLibrary().then((items) => {
|
|
161
|
+
void listBlueprintLibrary(props.nameSpace).then((items) => {
|
|
120
162
|
blueprintLibraryItems.value = items;
|
|
121
163
|
});
|
|
122
164
|
});
|
|
@@ -126,9 +168,9 @@ const blueprintLibraryNameById = computed(
|
|
|
126
168
|
);
|
|
127
169
|
|
|
128
170
|
const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
|
|
129
|
-
const record = await getBlueprintLibraryRecord(libraryBlueprintId);
|
|
171
|
+
const record = await getBlueprintLibraryRecord(libraryBlueprintId, props.nameSpace);
|
|
130
172
|
if (!record) return null;
|
|
131
|
-
const items = await listBlueprintLibrary();
|
|
173
|
+
const items = await listBlueprintLibrary(props.nameSpace);
|
|
132
174
|
const nameById = new Map(items.map((item) => [item.id, item.name]));
|
|
133
175
|
return documentToRunnableGraph(record.document, { libraryNameById: nameById });
|
|
134
176
|
};
|
|
@@ -167,7 +209,7 @@ const displayElements = computed(() =>
|
|
|
167
209
|
);
|
|
168
210
|
|
|
169
211
|
function applySceneFit() {
|
|
170
|
-
const enabled = readOutputScale();
|
|
212
|
+
const enabled = readOutputScale(props.nameSpace);
|
|
171
213
|
outputScale.value = enabled;
|
|
172
214
|
const fill = applyPreviewSceneFill(
|
|
173
215
|
sceneRef.value,
|
|
@@ -277,7 +319,7 @@ function noopSelect() {}
|
|
|
277
319
|
v-else
|
|
278
320
|
class="min-h-screen w-full overflow-hidden bg-white text-gray-900"
|
|
279
321
|
data-preview-mode="online"
|
|
280
|
-
:data-project-id="
|
|
322
|
+
:data-project-id="resolvedProjectId"
|
|
281
323
|
:data-preview-instance-id="previewInstanceId ?? ''"
|
|
282
324
|
:data-preview-node-count="String(displayElements.length)"
|
|
283
325
|
>
|
|
@@ -78,8 +78,10 @@ const props = withDefaults(
|
|
|
78
78
|
defineProps<{
|
|
79
79
|
class?: string;
|
|
80
80
|
initialZoom?: number;
|
|
81
|
+
nameSpace?: string | null;
|
|
82
|
+
initialWorkspace?: WorkspaceProjectRecord | null;
|
|
81
83
|
}>(),
|
|
82
|
-
{ initialZoom: 1 }
|
|
84
|
+
{ initialZoom: 1, nameSpace: null, initialWorkspace: null }
|
|
83
85
|
);
|
|
84
86
|
|
|
85
87
|
const {
|
|
@@ -296,9 +298,9 @@ const {
|
|
|
296
298
|
} = useBlueprintNodeSelectionGuard(selectedBlueprintNodeId, applyBlueprintNodeSelection);
|
|
297
299
|
|
|
298
300
|
const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
|
|
299
|
-
const record = await getBlueprintLibraryRecord(libraryBlueprintId);
|
|
301
|
+
const record = await getBlueprintLibraryRecord(libraryBlueprintId, props.nameSpace);
|
|
300
302
|
if (!record) return null;
|
|
301
|
-
const items = await listBlueprintLibrary();
|
|
303
|
+
const items = await listBlueprintLibrary(props.nameSpace);
|
|
302
304
|
const nameById = new Map(items.map((item) => [item.id, item.name]));
|
|
303
305
|
return documentToRunnableGraph(record.document, { libraryNameById: nameById });
|
|
304
306
|
};
|
|
@@ -319,6 +321,7 @@ const blueprintDebugSession = useBlueprintDebugSession({
|
|
|
319
321
|
libraryNameById: blueprintLibraryNameById,
|
|
320
322
|
onExecutionBlocked: handleBlueprintExecutionBlocked,
|
|
321
323
|
onViewScopeUpdate: handleViewScopeUpdate,
|
|
324
|
+
nameSpace: () => props.nameSpace,
|
|
322
325
|
});
|
|
323
326
|
|
|
324
327
|
const blueprintDebugToolbar = computed(() => ({
|
|
@@ -359,7 +362,7 @@ function handleUpdateAllowFalseSignalPropagation(value: boolean) {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
async function refreshBlueprintLibrary() {
|
|
362
|
-
blueprintLibraryItems.value = await listBlueprintLibrary();
|
|
365
|
+
blueprintLibraryItems.value = await listBlueprintLibrary(props.nameSpace);
|
|
363
366
|
}
|
|
364
367
|
|
|
365
368
|
onMounted(() => {
|
|
@@ -402,6 +405,8 @@ const workspaceProjects = useWorkspaceProjects({
|
|
|
402
405
|
},
|
|
403
406
|
panelRevision: computed(() => `${historyCursor.value}|${allElements.value.length}`),
|
|
404
407
|
onProjectApplied: handleWorkspaceProjectApplied,
|
|
408
|
+
initialWorkspace: () => props.initialWorkspace,
|
|
409
|
+
nameSpace: () => props.nameSpace,
|
|
405
410
|
});
|
|
406
411
|
|
|
407
412
|
const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
|
|
@@ -440,7 +445,7 @@ const blueprintNodeOptions = computed(() =>
|
|
|
440
445
|
);
|
|
441
446
|
async function loadBlueprintFromLibrary(id: string) {
|
|
442
447
|
await runBusyTask(t("common.loadingBlueprint"), async () => {
|
|
443
|
-
const record = await getBlueprintLibraryRecord(id);
|
|
448
|
+
const record = await getBlueprintLibraryRecord(id, props.nameSpace);
|
|
444
449
|
if (!record) {
|
|
445
450
|
message.error(panelMessages().blueprintNotFound);
|
|
446
451
|
void refreshBlueprintLibrary();
|
|
@@ -487,14 +492,14 @@ async function syncBlueprintToLibrary() {
|
|
|
487
492
|
const libraryId = activeBlueprintLibraryId.value;
|
|
488
493
|
if (!libraryId) return;
|
|
489
494
|
await runBusyTask(t("common.syncingBlueprint"), async () => {
|
|
490
|
-
const record = await getBlueprintLibraryRecord(libraryId);
|
|
495
|
+
const record = await getBlueprintLibraryRecord(libraryId, props.nameSpace);
|
|
491
496
|
if (!record) return;
|
|
492
497
|
await putBlueprintLibraryRecord({
|
|
493
498
|
...record,
|
|
494
499
|
document: blueprintGraph.value.document,
|
|
495
500
|
name: blueprintMeta.value.name,
|
|
496
501
|
remark: blueprintMeta.value.remark,
|
|
497
|
-
});
|
|
502
|
+
}, props.nameSpace);
|
|
498
503
|
blueprintSyncedDocument.value = blueprintGraph.value.document;
|
|
499
504
|
message.success(panelMessages().blueprintSyncedToLibrary);
|
|
500
505
|
await refreshBlueprintLibrary();
|
|
@@ -510,7 +515,7 @@ async function saveBlueprintToLibrary(meta: BlueprintMetaDraft) {
|
|
|
510
515
|
meta,
|
|
511
516
|
source: "saved",
|
|
512
517
|
});
|
|
513
|
-
await putBlueprintLibraryRecord(record);
|
|
518
|
+
await putBlueprintLibraryRecord(record, props.nameSpace);
|
|
514
519
|
activeBlueprintLibraryId.value = id;
|
|
515
520
|
blueprintSyncedDocument.value = blueprintGraph.value.document;
|
|
516
521
|
blueprintMeta.value = meta;
|
|
@@ -526,7 +531,7 @@ function handleExport() {
|
|
|
526
531
|
const url = URL.createObjectURL(blob);
|
|
527
532
|
const a = document.createElement("a");
|
|
528
533
|
a.href = url;
|
|
529
|
-
const safeName = (productName.value.trim() || "panel")
|
|
534
|
+
const safeName = ((productName.value ?? "").trim() || "panel")
|
|
530
535
|
.replace(/[\\/:*?"<>|]/g, "-")
|
|
531
536
|
.replace(/\s+/g, "-");
|
|
532
537
|
a.download = `${safeName}-${Date.now()}.json`;
|
|
@@ -566,7 +571,7 @@ async function handleBlueprintImportFile(file: File) {
|
|
|
566
571
|
const text = await file.text();
|
|
567
572
|
const payload = parseBlueprintImportFile(await parseJsonText(text));
|
|
568
573
|
const record = libraryRecordFromImport(payload);
|
|
569
|
-
await putBlueprintLibraryRecord(record);
|
|
574
|
+
await putBlueprintLibraryRecord(record, props.nameSpace);
|
|
570
575
|
await refreshBlueprintLibrary();
|
|
571
576
|
if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
|
|
572
577
|
await loadBlueprintFromLibrary(record.id);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, type MaybeRefOrGetter } from "vue";
|
|
2
2
|
import {
|
|
3
3
|
BlueprintGraph,
|
|
4
|
+
parseBlueprintData,
|
|
4
5
|
type BlueprintDocument,
|
|
5
6
|
type BlueprintMetaDraft,
|
|
6
7
|
} from "@arronqzy/vue-blueprint";
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
type WorkspaceSnapshot,
|
|
24
25
|
} from "../library/workspace-snapshot";
|
|
25
26
|
import { runBusyTask } from "../utils/async-work";
|
|
27
|
+
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
28
|
+
import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
|
|
26
29
|
|
|
27
30
|
export type UseWorkspaceProjectsOptions = {
|
|
28
31
|
exportPanelData: () => State;
|
|
@@ -39,13 +42,21 @@ export type UseWorkspaceProjectsOptions = {
|
|
|
39
42
|
panelRevision: MaybeRefOrGetter<unknown>;
|
|
40
43
|
/** 工作区记录应用到编辑器后的回调(用于恢复蓝图面板等 UI 状态) */
|
|
41
44
|
onProjectApplied?: (record: WorkspaceProjectRecord) => void;
|
|
45
|
+
/** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
|
|
46
|
+
initialWorkspace?: MaybeRefOrGetter<WorkspaceProjectRecord | null | undefined>;
|
|
47
|
+
nameSpace?: MaybeRefOrGetter<string | null | undefined>;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
|
-
export function buildOnlinePreviewUrl(
|
|
50
|
+
export function buildOnlinePreviewUrl(
|
|
51
|
+
projectId: string,
|
|
52
|
+
nameSpace?: string | null
|
|
53
|
+
): string {
|
|
45
54
|
const url = new URL(window.location.href);
|
|
46
55
|
url.search = "";
|
|
47
56
|
url.searchParams.set("preview", "online");
|
|
48
57
|
url.searchParams.set("projectId", projectId);
|
|
58
|
+
const ns = (nameSpace ?? "").trim();
|
|
59
|
+
if (ns) url.searchParams.set("ns", ns);
|
|
49
60
|
const previewInstanceId =
|
|
50
61
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
51
62
|
? crypto.randomUUID()
|
|
@@ -54,8 +65,31 @@ export function buildOnlinePreviewUrl(projectId: string): string {
|
|
|
54
65
|
return url.toString();
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
function previewWindowName(projectId: string, nameSpace?: string | null): string {
|
|
69
|
+
const ns = (nameSpace ?? "").trim();
|
|
70
|
+
return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasWorkspacePayload(
|
|
74
|
+
record?: WorkspaceProjectRecord | null
|
|
75
|
+
): record is WorkspaceProjectRecord {
|
|
76
|
+
if (!record || typeof record !== "object") return false;
|
|
77
|
+
return Boolean(
|
|
78
|
+
record.panelState ||
|
|
79
|
+
record.blueprintDocument ||
|
|
80
|
+
record.id ||
|
|
81
|
+
record.productName ||
|
|
82
|
+
record.titleIconDataUrl
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function workspaceApplyKey(record: WorkspaceProjectRecord): string {
|
|
87
|
+
return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
58
91
|
const { t, locale } = useI18nOptional();
|
|
92
|
+
const nameSpace = () => toValue(options.nameSpace) ?? null;
|
|
59
93
|
const projects = ref<WorkspaceProjectListItem[]>([]);
|
|
60
94
|
const activeProjectId = ref<string | null>(null);
|
|
61
95
|
const activeProjectName = ref<string | null>(null);
|
|
@@ -77,7 +111,8 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
77
111
|
|
|
78
112
|
function resolveProjectName() {
|
|
79
113
|
void locale.value;
|
|
80
|
-
|
|
114
|
+
const name = toValue(options.productName);
|
|
115
|
+
return (typeof name === "string" ? name : "").trim() || t("panel.defaults.unnamedProduct");
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
function buildCurrentSnapshot(): WorkspaceSnapshot {
|
|
@@ -90,13 +125,15 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
async function refreshProjects() {
|
|
93
|
-
projects.value = await listWorkspaceProjects();
|
|
128
|
+
projects.value = await listWorkspaceProjects(nameSpace());
|
|
94
129
|
}
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
watch(
|
|
132
|
+
() => nameSpace(),
|
|
133
|
+
() => {
|
|
134
|
+
void refreshProjects();
|
|
135
|
+
}
|
|
136
|
+
);
|
|
100
137
|
|
|
101
138
|
onUnmounted(() => {
|
|
102
139
|
if (previewPollTimer !== undefined) window.clearInterval(previewPollTimer);
|
|
@@ -122,36 +159,104 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
122
159
|
return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.value);
|
|
123
160
|
});
|
|
124
161
|
|
|
125
|
-
function applyProjectRecord(
|
|
162
|
+
function applyProjectRecord(
|
|
163
|
+
record: WorkspaceProjectRecord,
|
|
164
|
+
applyOptions?: { notifyApplied?: boolean }
|
|
165
|
+
) {
|
|
166
|
+
const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
|
|
126
167
|
const blueprintDocument =
|
|
127
|
-
|
|
168
|
+
parsedBlueprint.value ?? BlueprintGraph.empty().document;
|
|
128
169
|
const blueprintMeta: BlueprintMetaDraft = {
|
|
129
170
|
name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
|
|
130
171
|
remark: record.blueprintMeta?.remark ?? "",
|
|
131
172
|
};
|
|
173
|
+
const panelState =
|
|
174
|
+
normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
|
|
175
|
+
const productName =
|
|
176
|
+
typeof record.productName === "string" && record.productName.trim()
|
|
177
|
+
? record.productName
|
|
178
|
+
: t("panel.defaults.unnamedProduct");
|
|
179
|
+
const titleIconDataUrl = record.titleIconDataUrl ?? "";
|
|
132
180
|
|
|
133
|
-
options.importPanelData(
|
|
181
|
+
options.importPanelData(panelState);
|
|
134
182
|
options.setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
|
|
135
183
|
options.setBlueprintMeta(blueprintMeta);
|
|
136
|
-
options.setProductName(
|
|
137
|
-
options.setTitleIconDataUrl(
|
|
138
|
-
activeProjectId.value = record.id;
|
|
139
|
-
activeProjectName.value = record.name;
|
|
184
|
+
options.setProductName(productName);
|
|
185
|
+
options.setTitleIconDataUrl(titleIconDataUrl);
|
|
186
|
+
activeProjectId.value = record.id || null;
|
|
187
|
+
activeProjectName.value = record.name ?? null;
|
|
140
188
|
syncedSnapshotRef.value = cloneWorkspaceSnapshot({
|
|
141
189
|
blueprintDocument,
|
|
142
190
|
blueprintMeta,
|
|
143
|
-
productName
|
|
144
|
-
titleIconDataUrl
|
|
191
|
+
productName,
|
|
192
|
+
titleIconDataUrl,
|
|
145
193
|
});
|
|
146
194
|
syncedPanelRevision = toValue(options.panelRevision);
|
|
147
195
|
pendingRevisionSync = true;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
196
|
+
if (applyOptions?.notifyApplied !== false) {
|
|
197
|
+
options.onProjectApplied?.({
|
|
198
|
+
...record,
|
|
199
|
+
blueprintDocument,
|
|
200
|
+
blueprintMeta,
|
|
201
|
+
panelState,
|
|
202
|
+
productName,
|
|
203
|
+
titleIconDataUrl: titleIconDataUrl || undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyEmptyWorkspace() {
|
|
209
|
+
options.importPanelData(createEmptyPanelState());
|
|
210
|
+
options.setBlueprintGraph(BlueprintGraph.empty());
|
|
211
|
+
options.setBlueprintMeta({
|
|
212
|
+
name: t("panel.defaults.unnamedBlueprint"),
|
|
213
|
+
remark: "",
|
|
152
214
|
});
|
|
215
|
+
options.setProductName(t("panel.defaults.unnamedProduct"));
|
|
216
|
+
options.setTitleIconDataUrl("");
|
|
217
|
+
activeProjectId.value = null;
|
|
218
|
+
activeProjectName.value = null;
|
|
219
|
+
syncedSnapshotRef.value = null;
|
|
153
220
|
}
|
|
154
221
|
|
|
222
|
+
let bootstrapped = false;
|
|
223
|
+
let appliedWorkspaceKey: string | null = null;
|
|
224
|
+
|
|
225
|
+
function bootstrapInitialWorkspace() {
|
|
226
|
+
const initialWorkspace = toValue(options.initialWorkspace) ?? null;
|
|
227
|
+
if (!bootstrapped) {
|
|
228
|
+
bootstrapped = true;
|
|
229
|
+
if (hasWorkspacePayload(initialWorkspace)) {
|
|
230
|
+
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
231
|
+
appliedWorkspaceKey = workspaceApplyKey(initialWorkspace);
|
|
232
|
+
} else {
|
|
233
|
+
applyEmptyWorkspace();
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!hasWorkspacePayload(initialWorkspace)) return;
|
|
238
|
+
const key = workspaceApplyKey(initialWorkspace);
|
|
239
|
+
if (appliedWorkspaceKey === key) return;
|
|
240
|
+
appliedWorkspaceKey = key;
|
|
241
|
+
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onMounted(() => {
|
|
245
|
+
bootstrapInitialWorkspace();
|
|
246
|
+
void refreshProjects();
|
|
247
|
+
previewPollTimer = window.setInterval(refreshPreviewing, 2000);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
watch(
|
|
251
|
+
() => {
|
|
252
|
+
const record = toValue(options.initialWorkspace);
|
|
253
|
+
return record ? workspaceApplyKey(record) : "";
|
|
254
|
+
},
|
|
255
|
+
() => {
|
|
256
|
+
bootstrapInitialWorkspace();
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
155
260
|
async function persistProject(persistOptions: {
|
|
156
261
|
id?: string;
|
|
157
262
|
name: string;
|
|
@@ -161,7 +266,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
161
266
|
const now = Date.now();
|
|
162
267
|
const id = persistOptions.id ?? createWorkspaceProjectId();
|
|
163
268
|
const createdAt = persistOptions.createdAt ?? now;
|
|
164
|
-
const name = persistOptions.name.trim() || resolveProjectName();
|
|
269
|
+
const name = (persistOptions.name ?? "").trim() || resolveProjectName();
|
|
165
270
|
|
|
166
271
|
const record: WorkspaceProjectRecord = {
|
|
167
272
|
id,
|
|
@@ -171,13 +276,14 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
171
276
|
panelState: options.exportPanelData(),
|
|
172
277
|
blueprintDocument: snapshot.blueprintDocument,
|
|
173
278
|
blueprintMeta: snapshot.blueprintMeta,
|
|
174
|
-
productName:
|
|
279
|
+
productName:
|
|
280
|
+
typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
|
|
175
281
|
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
176
282
|
};
|
|
177
283
|
|
|
178
|
-
await putWorkspaceProject(record);
|
|
179
|
-
writeWorkspacePreviewCache(record);
|
|
180
|
-
broadcastWorkspaceProjectUpdate(id, now);
|
|
284
|
+
await putWorkspaceProject(record, nameSpace());
|
|
285
|
+
writeWorkspacePreviewCache(record, nameSpace());
|
|
286
|
+
broadcastWorkspaceProjectUpdate(id, now, nameSpace());
|
|
181
287
|
activeProjectId.value = id;
|
|
182
288
|
activeProjectName.value = name;
|
|
183
289
|
syncedSnapshotRef.value = snapshot;
|
|
@@ -191,6 +297,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
191
297
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
192
298
|
const name = resolveProjectName();
|
|
193
299
|
const record = await persistProject({ name });
|
|
300
|
+
emitAbuilderEvent(AbuilderEvents.workspaceAdd, record);
|
|
194
301
|
return { name, id: record.id };
|
|
195
302
|
});
|
|
196
303
|
}
|
|
@@ -201,7 +308,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
201
308
|
return handleCreateProject();
|
|
202
309
|
}
|
|
203
310
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
204
|
-
const existing = await getWorkspaceProject(projectId);
|
|
311
|
+
const existing = await getWorkspaceProject(projectId, nameSpace());
|
|
205
312
|
const name = resolveProjectName();
|
|
206
313
|
const record = await persistProject({
|
|
207
314
|
id: projectId,
|
|
@@ -214,7 +321,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
214
321
|
|
|
215
322
|
async function handleOpenProject(id: string) {
|
|
216
323
|
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
217
|
-
const record = await getWorkspaceProject(id);
|
|
324
|
+
const record = await getWorkspaceProject(id, nameSpace());
|
|
218
325
|
if (!record) {
|
|
219
326
|
await refreshProjects();
|
|
220
327
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
@@ -228,7 +335,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
228
335
|
if (!activeProjectId.value) {
|
|
229
336
|
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
230
337
|
}
|
|
231
|
-
const existing = await getWorkspaceProject(activeProjectId.value);
|
|
338
|
+
const existing = await getWorkspaceProject(activeProjectId.value, nameSpace());
|
|
232
339
|
if (!existing) {
|
|
233
340
|
activeProjectId.value = null;
|
|
234
341
|
activeProjectName.value = null;
|
|
@@ -237,18 +344,19 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
237
344
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
238
345
|
}
|
|
239
346
|
const name = resolveProjectName() || existing.name;
|
|
240
|
-
await persistProject({
|
|
347
|
+
const record = await persistProject({
|
|
241
348
|
id: activeProjectId.value,
|
|
242
349
|
name,
|
|
243
350
|
createdAt: existing.createdAt,
|
|
244
351
|
});
|
|
352
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
|
|
245
353
|
return name;
|
|
246
354
|
});
|
|
247
355
|
}
|
|
248
356
|
|
|
249
357
|
async function handleDeleteProject(id: string) {
|
|
250
358
|
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
251
|
-
await deleteWorkspaceProject(id);
|
|
359
|
+
await deleteWorkspaceProject(id, nameSpace());
|
|
252
360
|
if (activeProjectId.value === id) {
|
|
253
361
|
activeProjectId.value = null;
|
|
254
362
|
activeProjectName.value = null;
|
|
@@ -266,9 +374,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
266
374
|
await handleSyncProject();
|
|
267
375
|
}
|
|
268
376
|
const record =
|
|
269
|
-
(await getWorkspaceProject(id)) ??
|
|
377
|
+
(await getWorkspaceProject(id, nameSpace())) ??
|
|
378
|
+
readWorkspacePreviewCache(id, nameSpace());
|
|
270
379
|
if (record) {
|
|
271
|
-
writeWorkspacePreviewCache(record);
|
|
380
|
+
writeWorkspacePreviewCache(record, nameSpace());
|
|
272
381
|
}
|
|
273
382
|
const existing = previewWindows.get(id);
|
|
274
383
|
if (existing && !existing.closed) {
|
|
@@ -280,9 +389,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
280
389
|
refreshPreviewing();
|
|
281
390
|
return;
|
|
282
391
|
}
|
|
392
|
+
const ns = nameSpace();
|
|
283
393
|
const win = window.open(
|
|
284
|
-
buildOnlinePreviewUrl(id),
|
|
285
|
-
|
|
394
|
+
buildOnlinePreviewUrl(id, ns),
|
|
395
|
+
previewWindowName(id, ns)
|
|
286
396
|
);
|
|
287
397
|
if (win) {
|
|
288
398
|
previewWindows.set(id, win);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
|
+
|
|
3
|
+
export const AbuilderEvents = {
|
|
4
|
+
/** 添加工作区 */
|
|
5
|
+
workspaceAdd: "workspace:add",
|
|
6
|
+
/** 同步工作区 */
|
|
7
|
+
workspaceSync: "workspace:sync",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
|
|
11
|
+
|
|
12
|
+
/** 工作区完整数据(面板 + 蓝图),可用于外部持久化与再次加载 */
|
|
13
|
+
export type WorkspaceData = WorkspaceProjectRecord;
|
|
14
|
+
|
|
15
|
+
export type WorkspaceAddEventPayload = WorkspaceData;
|
|
16
|
+
export type WorkspaceSyncEventPayload = WorkspaceData;
|
|
17
|
+
|
|
18
|
+
export type AbuilderEventPayloadMap = {
|
|
19
|
+
[AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
|
|
20
|
+
[AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type EventCallback = (payload: unknown) => void;
|
|
24
|
+
|
|
25
|
+
const listeners = new Map<string, Set<EventCallback>>();
|
|
26
|
+
|
|
27
|
+
export function emitAbuilderEvent<T extends AbuilderEventName>(
|
|
28
|
+
eventName: T,
|
|
29
|
+
payload: AbuilderEventPayloadMap[T]
|
|
30
|
+
): void {
|
|
31
|
+
const set = listeners.get(eventName);
|
|
32
|
+
if (!set) return;
|
|
33
|
+
for (const callback of set) {
|
|
34
|
+
try {
|
|
35
|
+
callback(payload);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`[abuilder] event handler error (${eventName}):`, error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function addEventSubscription<T extends AbuilderEventName>(
|
|
43
|
+
eventName: T,
|
|
44
|
+
callback: (payload: AbuilderEventPayloadMap[T]) => void
|
|
45
|
+
): { unsubscribe: () => void } {
|
|
46
|
+
let set = listeners.get(eventName);
|
|
47
|
+
if (!set) {
|
|
48
|
+
set = new Set();
|
|
49
|
+
listeners.set(eventName, set);
|
|
50
|
+
}
|
|
51
|
+
set.add(callback as EventCallback);
|
|
52
|
+
return {
|
|
53
|
+
unsubscribe: () => {
|
|
54
|
+
set.delete(callback as EventCallback);
|
|
55
|
+
if (set.size === 0) {
|
|
56
|
+
listeners.delete(eventName);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlueprintGraph,
|
|
3
|
+
parseBlueprintData,
|
|
4
|
+
validateBlueprintData,
|
|
5
|
+
type BlueprintDocument,
|
|
6
|
+
type BlueprintMetaDraft,
|
|
7
|
+
type ParseCheckResult,
|
|
8
|
+
} from "@arronqzy/vue-blueprint";
|
|
9
|
+
import type { State } from "@arronqzy/rx-store";
|
|
10
|
+
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
11
|
+
import {
|
|
12
|
+
createWorkspaceProjectId,
|
|
13
|
+
type WorkspaceProjectRecord,
|
|
14
|
+
} from "./workspace-project-db";
|
|
15
|
+
|
|
16
|
+
export type { ParseCheckResult };
|
|
17
|
+
|
|
18
|
+
export type WorkspaceParseCheckResult = ParseCheckResult & {
|
|
19
|
+
view: ParseCheckResult;
|
|
20
|
+
blueprint: ParseCheckResult;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractPanelState(data: unknown): unknown {
|
|
28
|
+
if (isRecord(data) && data.panelState !== undefined) return data.panelState;
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readBlueprintMeta(raw: unknown): BlueprintMetaDraft {
|
|
33
|
+
if (!isRecord(raw)) return { name: "", remark: "" };
|
|
34
|
+
return {
|
|
35
|
+
name: typeof raw.name === "string" ? raw.name : "",
|
|
36
|
+
remark: typeof raw.remark === "string" ? raw.remark : "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 校验数据能否被视图解析(工作区 `panelState` 或裸面板状态均可)。
|
|
42
|
+
*/
|
|
43
|
+
export function parseViewData(
|
|
44
|
+
data: unknown
|
|
45
|
+
): ParseCheckResult & { value?: State } {
|
|
46
|
+
if (data == null) {
|
|
47
|
+
return { ok: false, error: "empty" };
|
|
48
|
+
}
|
|
49
|
+
const normalized = normalizeImportedPanelState(extractPanelState(data));
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
return { ok: false, error: "invalid-view-data" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, value: normalized };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateViewData(data: unknown): ParseCheckResult {
|
|
57
|
+
const parsed = parseViewData(data);
|
|
58
|
+
return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { parseBlueprintData, validateBlueprintData };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 构造一份可交给 `initialWorkspace` 的空工作区。
|
|
65
|
+
*/
|
|
66
|
+
export function createEmptyWorkspace(
|
|
67
|
+
init?: Partial<WorkspaceProjectRecord>
|
|
68
|
+
): WorkspaceProjectRecord {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
return {
|
|
71
|
+
id: init?.id || createWorkspaceProjectId(),
|
|
72
|
+
name: init?.name ?? "",
|
|
73
|
+
createdAt: init?.createdAt ?? now,
|
|
74
|
+
updatedAt: init?.updatedAt ?? now,
|
|
75
|
+
panelState: init?.panelState ?? createEmptyPanelState(),
|
|
76
|
+
blueprintDocument: init?.blueprintDocument ?? BlueprintGraph.empty().document,
|
|
77
|
+
blueprintMeta: init?.blueprintMeta ?? { name: "", remark: "" },
|
|
78
|
+
productName: init?.productName ?? "",
|
|
79
|
+
titleIconDataUrl: init?.titleIconDataUrl,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 校验完整工作区数据:视图 `panelState` 与蓝图 `blueprintDocument` 都要能解析。
|
|
85
|
+
*/
|
|
86
|
+
export function validateWorkspaceData(data: unknown): WorkspaceParseCheckResult {
|
|
87
|
+
if (data == null) {
|
|
88
|
+
const empty = { ok: false, error: "empty" } satisfies ParseCheckResult;
|
|
89
|
+
return { ok: false, error: "empty", view: empty, blueprint: empty };
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(data)) {
|
|
92
|
+
const invalid = { ok: false, error: "invalid-workspace-data" } satisfies ParseCheckResult;
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: "invalid-workspace-data",
|
|
96
|
+
view: invalid,
|
|
97
|
+
blueprint: invalid,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const view = validateViewData(data);
|
|
101
|
+
const blueprint = validateBlueprintData(data);
|
|
102
|
+
const ok = view.ok && blueprint.ok;
|
|
103
|
+
return {
|
|
104
|
+
ok,
|
|
105
|
+
error: ok ? undefined : "invalid-workspace-data",
|
|
106
|
+
view,
|
|
107
|
+
blueprint,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 解析并规范化工作区。成功时 `value` 可直接作为 `initialWorkspace`。
|
|
113
|
+
*/
|
|
114
|
+
export function parseWorkspaceData(
|
|
115
|
+
data: unknown
|
|
116
|
+
): WorkspaceParseCheckResult & { value?: WorkspaceProjectRecord } {
|
|
117
|
+
const check = validateWorkspaceData(data);
|
|
118
|
+
if (!check.ok || !isRecord(data)) {
|
|
119
|
+
return check;
|
|
120
|
+
}
|
|
121
|
+
const view = parseViewData(data);
|
|
122
|
+
const blueprint = parseBlueprintData(data);
|
|
123
|
+
if (!view.value || !blueprint.value) {
|
|
124
|
+
return check;
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
return {
|
|
128
|
+
...check,
|
|
129
|
+
value: {
|
|
130
|
+
id: typeof data.id === "string" && data.id ? data.id : createWorkspaceProjectId(),
|
|
131
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
132
|
+
createdAt: typeof data.createdAt === "number" ? data.createdAt : now,
|
|
133
|
+
updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : now,
|
|
134
|
+
panelState: view.value,
|
|
135
|
+
blueprintDocument: blueprint.value as BlueprintDocument,
|
|
136
|
+
blueprintMeta: readBlueprintMeta(data.blueprintMeta),
|
|
137
|
+
productName: typeof data.productName === "string" ? data.productName : "",
|
|
138
|
+
titleIconDataUrl:
|
|
139
|
+
typeof data.titleIconDataUrl === "string" ? data.titleIconDataUrl : undefined,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
1
2
|
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
3
|
|
|
3
4
|
const CACHE_PREFIX = "arronqzy-workspace-preview:";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
function previewCacheKey(projectId: string, nameSpace?: string | null): string {
|
|
7
|
+
return appStorageKey(CACHE_PREFIX + projectId, nameSpace);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function writeWorkspacePreviewCache(
|
|
11
|
+
record: WorkspaceProjectRecord,
|
|
12
|
+
nameSpace?: string | null
|
|
13
|
+
): void {
|
|
6
14
|
try {
|
|
7
15
|
localStorage.setItem(
|
|
8
|
-
|
|
16
|
+
previewCacheKey(record.id, nameSpace),
|
|
9
17
|
JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
|
|
10
18
|
);
|
|
11
19
|
} catch {
|
|
@@ -14,10 +22,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
|
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export function readWorkspacePreviewCache(
|
|
17
|
-
projectId: string
|
|
25
|
+
projectId: string,
|
|
26
|
+
nameSpace?: string | null
|
|
18
27
|
): WorkspaceProjectRecord | null {
|
|
19
28
|
try {
|
|
20
|
-
const raw = localStorage.getItem(
|
|
29
|
+
const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
|
|
21
30
|
if (!raw) return null;
|
|
22
31
|
const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
|
|
23
32
|
if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/vue-blueprint";
|
|
2
2
|
import type { State } from "@arronqzy/rx-store";
|
|
3
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
3
4
|
|
|
4
5
|
const DB_NAME = "arronqzy-workspace-projects";
|
|
5
6
|
const DB_VERSION = 1;
|
|
@@ -24,9 +25,9 @@ export type WorkspaceProjectRecord = {
|
|
|
24
25
|
titleIconDataUrl?: string;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
function openDb(): Promise<IDBDatabase> {
|
|
28
|
+
function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
|
|
28
29
|
return new Promise((resolve, reject) => {
|
|
29
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
30
|
+
const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
|
|
30
31
|
request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
|
|
31
32
|
request.onupgradeneeded = () => {
|
|
32
33
|
const db = request.result;
|
|
@@ -41,9 +42,10 @@ function openDb(): Promise<IDBDatabase> {
|
|
|
41
42
|
|
|
42
43
|
function runTransaction<T>(
|
|
43
44
|
mode: IDBTransactionMode,
|
|
44
|
-
runner: (store: IDBObjectStore) => IDBRequest<T
|
|
45
|
+
runner: (store: IDBObjectStore) => IDBRequest<T>,
|
|
46
|
+
nameSpace?: string | null
|
|
45
47
|
): Promise<T> {
|
|
46
|
-
return openDb().then(
|
|
48
|
+
return openDb(nameSpace).then(
|
|
47
49
|
(db) =>
|
|
48
50
|
new Promise<T>((resolve, reject) => {
|
|
49
51
|
const tx = db.transaction(STORE_NAME, mode);
|
|
@@ -76,8 +78,10 @@ export function createWorkspaceProjectId(): string {
|
|
|
76
78
|
return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
export async function listWorkspaceProjects(
|
|
80
|
-
|
|
81
|
+
export async function listWorkspaceProjects(
|
|
82
|
+
nameSpace?: string | null
|
|
83
|
+
): Promise<WorkspaceProjectListItem[]> {
|
|
84
|
+
const db = await openDb(nameSpace);
|
|
81
85
|
return new Promise((resolve, reject) => {
|
|
82
86
|
const tx = db.transaction(STORE_NAME, "readonly");
|
|
83
87
|
const store = tx.objectStore(STORE_NAME);
|
|
@@ -109,21 +113,28 @@ export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
export async function getWorkspaceProject(
|
|
112
|
-
id: string
|
|
116
|
+
id: string,
|
|
117
|
+
nameSpace?: string | null
|
|
113
118
|
): Promise<WorkspaceProjectRecord | null> {
|
|
114
|
-
const record = await runTransaction<WorkspaceProjectRecord | undefined>(
|
|
115
|
-
|
|
119
|
+
const record = await runTransaction<WorkspaceProjectRecord | undefined>(
|
|
120
|
+
"readonly",
|
|
121
|
+
(store) => store.get(id),
|
|
122
|
+
nameSpace
|
|
116
123
|
);
|
|
117
124
|
return record ?? null;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
export async function putWorkspaceProject(
|
|
121
|
-
record: WorkspaceProjectRecord
|
|
128
|
+
record: WorkspaceProjectRecord,
|
|
129
|
+
nameSpace?: string | null
|
|
122
130
|
): Promise<WorkspaceProjectRecord> {
|
|
123
|
-
await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
|
|
131
|
+
await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
|
|
124
132
|
return record;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
export async function deleteWorkspaceProject(
|
|
128
|
-
|
|
135
|
+
export async function deleteWorkspaceProject(
|
|
136
|
+
id: string,
|
|
137
|
+
nameSpace?: string | null
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
|
|
129
140
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
|
|
1
3
|
const CHANNEL_PREFIX = "arronqzy-workspace-project";
|
|
2
4
|
|
|
3
5
|
export type WorkspaceProjectSyncMessage = {
|
|
@@ -6,16 +8,22 @@ export type WorkspaceProjectSyncMessage = {
|
|
|
6
8
|
updatedAt: number;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
|
-
export function getWorkspaceProjectChannelName(
|
|
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,13 +1,16 @@
|
|
|
1
1
|
export function parseOnlinePreviewSearchParams(search: string): {
|
|
2
2
|
projectId: string;
|
|
3
3
|
previewInstanceId?: string;
|
|
4
|
+
nameSpace?: string;
|
|
4
5
|
} | null {
|
|
5
6
|
const params = new URLSearchParams(search);
|
|
6
7
|
if (params.get("preview") !== "online") return null;
|
|
7
8
|
const projectId = params.get("projectId") ?? params.get("id");
|
|
8
9
|
if (!projectId) return null;
|
|
10
|
+
const nameSpace = (params.get("ns") ?? params.get("nameSpace") ?? "").trim();
|
|
9
11
|
return {
|
|
10
12
|
projectId,
|
|
11
13
|
previewInstanceId: params.get("pid") ?? undefined,
|
|
14
|
+
nameSpace: nameSpace || undefined,
|
|
12
15
|
};
|
|
13
16
|
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/** 产物预览是否缩放内部内容(字体等)。默认 false:块仍按视口撑满,内容保持原尺寸以免失真。 */
|
|
2
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
3
|
+
|
|
2
4
|
export const OUTPUT_SCALE_STORAGE_KEY = "panel:outputScale";
|
|
3
5
|
|
|
4
|
-
export function readOutputScale(): boolean {
|
|
6
|
+
export function readOutputScale(nameSpace?: string | null): boolean {
|
|
5
7
|
try {
|
|
6
|
-
return localStorage.getItem(OUTPUT_SCALE_STORAGE_KEY) === "1";
|
|
8
|
+
return localStorage.getItem(appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace)) === "1";
|
|
7
9
|
} catch {
|
|
8
10
|
return false;
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
export function writeOutputScale(enabled: boolean, nameSpace?: string | null) {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(
|
|
17
|
+
appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace),
|
|
18
|
+
enabled ? "1" : "0"
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore storage errors
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export type PreviewFillScale = {
|
|
13
26
|
vw: number;
|
|
14
27
|
vh: number;
|
|
@@ -26,6 +26,24 @@ export function normalizeImportedPanelState(state: unknown): State | null {
|
|
|
26
26
|
return next;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function createEmptyPanelState(): State {
|
|
30
|
+
return {
|
|
31
|
+
root: {
|
|
32
|
+
id: "root",
|
|
33
|
+
type: "root",
|
|
34
|
+
props: {},
|
|
35
|
+
children: [],
|
|
36
|
+
},
|
|
37
|
+
selectedIds: [],
|
|
38
|
+
variables: {
|
|
39
|
+
counter: 0,
|
|
40
|
+
isOpen: false,
|
|
41
|
+
layers: [DEFAULT_LAYER],
|
|
42
|
+
activeLayerId: DEFAULT_LAYER_ID,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
export function parseAllPanelElements(state: State): PanelElement[] {
|
|
30
48
|
return (state.root.children ?? [])
|
|
31
49
|
.filter((node) => {
|