@arronqzy/vue-view 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,17 +49,7 @@ import {
49
49
  } from "@arronqzy/vue-view";
50
50
  ```
51
51
 
52
- 创建 / 同步发出 `workspace:add` / `workspace:sync`,删除工作区发出 `workspace:delete`(参数 `{ ids, nameSpace }`)。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。
53
-
54
- 浏览器里的本地数据可以读取和清理,最后一个参数都是 `nameSpace`:
55
-
56
- - 工作区:`listWorkspaceProjects` / `getWorkspaceProject` / `deleteWorkspaceProject` / `clearWorkspaceProjects`
57
- - 蓝图库:`listBlueprintLibrary` / `getBlueprintLibraryRecord` / `deleteBlueprintLibraryRecord` / `clearBlueprintLibrary`
58
- - 蓝图执行日志:`listExecutionRunRecords` / `getExecutionRunRecord` / `deleteExecutionRunRecord` / `clearAllExecutionRunRecords` / `purgeExecutionRunsOlderThan` / `trimExecutionRunRecordsToMax`,保留策略用 `readExecutionLogSettings` / `writeExecutionLogSettings`
59
- - 接口集合:`listApiCollectionRecords` / `deleteApiCollection` / `clearApiCollections`
60
- - `clearAbuilderStorage`:一次清空以上四类,返回各自删除数量
61
-
62
- 工作区、蓝图库的写入接口不导出(由编辑器自己维护)。`getPreviewSnapshot` 只在 React 包实现。
52
+ 创建 / 同步发出 `workspace:add` / `workspace:sync`(载荷含 `nameSpace`),删除发出 `workspace:delete`。另有 `blueprint-library:changed` / `api-collection:changed`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`;带 `id` 时默认同步进 IndexedDB。也支持 `workspaceId` prop、`ref`(`AbuilderViewHandle`)以及 `put*` / `openWorkspace` / `getCurrentWorkspace` / `getPreviewSnapshot` 等与 React 对齐的公开 API。
63
53
 
64
54
  ## 和 React 版的差别
65
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,13 +23,13 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/i18n": "0.1.17",
27
- "@arronqzy/rx-store": "1.0.8",
28
26
  "@arronqzy/blueprint-dsl": "1.0.17",
27
+ "@arronqzy/i18n": "0.1.17",
29
28
  "@arronqzy/view-scene3d": "0.1.5",
30
- "@arronqzy/view-table": "0.1.8",
31
29
  "@arronqzy/vue-rx-store": "0.1.6",
32
- "@arronqzy/vue-blueprint": "0.1.18"
30
+ "@arronqzy/view-table": "0.1.8",
31
+ "@arronqzy/vue-blueprint": "0.1.19",
32
+ "@arronqzy/rx-store": "1.0.8"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "vue": "^3.4.0"
@@ -39,8 +39,8 @@
39
39
  "eslint": "^8.57.0",
40
40
  "typescript": "^5.5.4",
41
41
  "vue-tsc": "^2.2.0",
42
- "@arronqzy/eslint-config": "0.0.0",
43
- "@arronqzy/typescript-config": "1.0.0"
42
+ "@arronqzy/typescript-config": "1.0.0",
43
+ "@arronqzy/eslint-config": "0.0.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/index.ts CHANGED
@@ -16,17 +16,24 @@ export { createWorkspaceProjectId } from "./panel/library/workspace-project-db";
16
16
  export {
17
17
  listWorkspaceProjects,
18
18
  getWorkspaceProject,
19
+ putWorkspaceProject,
20
+ upsertWorkspaceProject,
19
21
  deleteWorkspaceProject,
20
22
  clearWorkspaceProjects,
21
23
  } from "./panel/library/workspace-storage";
22
- export type { WorkspaceProjectListItem } from "./panel/library/workspace-storage";
24
+ export type {
25
+ WorkspaceProjectListItem,
26
+ PutWorkspaceProjectOptions,
27
+ } from "./panel/library/workspace-storage";
23
28
  export {
24
29
  listBlueprintLibrary,
25
30
  getBlueprintLibraryRecord,
31
+ putBlueprintLibraryRecord,
26
32
  deleteBlueprintLibraryRecord,
27
33
  clearBlueprintLibrary,
28
34
  listExecutionRunRecords,
29
35
  getExecutionRunRecord,
36
+ putExecutionRunRecord,
30
37
  deleteExecutionRunRecord,
31
38
  clearAllExecutionRunRecords,
32
39
  purgeExecutionRunsOlderThan,
@@ -34,6 +41,8 @@ export {
34
41
  readExecutionLogSettings,
35
42
  writeExecutionLogSettings,
36
43
  listApiCollectionRecords,
44
+ putApiCollection,
45
+ upsertApiCollectionFromJson,
37
46
  deleteApiCollection,
38
47
  clearApiCollections,
39
48
  clearAbuilderStorage,
@@ -45,6 +54,8 @@ export type {
45
54
  ExecutionRunRecord,
46
55
  ExecutionLogSettings,
47
56
  ApiCollectionRecord,
57
+ UpsertApiCollectionInput,
58
+ UpsertApiCollectionResult,
48
59
  } from "./panel/library/abuilder-storage";
49
60
  export {
50
61
  addEventSubscription,
@@ -53,14 +64,27 @@ export {
53
64
  export type {
54
65
  AbuilderEventName,
55
66
  AbuilderEventPayloadMap,
67
+ AddEventSubscriptionOptions,
56
68
  WorkspaceAddEventPayload,
57
69
  WorkspaceSyncEventPayload,
58
70
  WorkspaceDeleteEventPayload,
71
+ BlueprintLibraryChangedEventPayload,
59
72
  ApiCollectionUploadStartEventPayload,
60
73
  ApiCollectionUploadSuccessEventPayload,
74
+ ApiCollectionChangedEventPayload,
61
75
  WorkspaceData,
62
76
  } from "./panel/library/event-subscription";
77
+ export {
78
+ getCurrentWorkspace,
79
+ openWorkspace,
80
+ createWorkspace,
81
+ saveWorkspace,
82
+ syncWorkspace,
83
+ } from "./panel/library/abuilder-view-handle";
84
+ export type { AbuilderViewHandle } from "./panel/library/abuilder-view-handle";
63
85
  export type {
64
86
  ParseCheckResult,
65
87
  WorkspaceParseCheckResult,
66
88
  } from "./panel/library/parse-workspace-data";
89
+ export { getPreviewSnapshot } from "./panel/library/preview-snapshot";
90
+ export type { GetPreviewSnapshotOptions } from "./panel/library/preview-snapshot";
@@ -40,6 +40,8 @@ import {
40
40
  resolvePreviewLayerElements,
41
41
  } from "./utils/panelStateIO";
42
42
  import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
43
+ import { registerPreviewSnapshotProvider } from "./library/preview-snapshot";
44
+ import { capturePreviewSceneElement } from "./library/capture-preview-snapshot";
43
45
 
44
46
  const { t, locale } = useI18n();
45
47
  useBlueprintDslLocaleSync();
@@ -292,6 +294,29 @@ onMounted(() => {
292
294
  onUnmounted(() => window.removeEventListener("resize", onResize));
293
295
  });
294
296
 
297
+ watch(
298
+ [layoutReady, sceneRef, sceneBounds, fillScale, outputScale],
299
+ () => {
300
+ if (!layoutReady.value || !sceneRef.value) {
301
+ registerPreviewSnapshotProvider(null);
302
+ return;
303
+ }
304
+ registerPreviewSnapshotProvider(async () => {
305
+ const scene = sceneRef.value;
306
+ if (!scene) return null;
307
+ const width = outputScale.value
308
+ ? sceneBounds.value.width
309
+ : sceneBounds.value.width * fillScale.value.scaleX;
310
+ const height = outputScale.value
311
+ ? sceneBounds.value.height
312
+ : sceneBounds.value.height * fillScale.value.scaleY;
313
+ return capturePreviewSceneElement(scene, width, height);
314
+ });
315
+ },
316
+ { immediate: true }
317
+ );
318
+ onUnmounted(() => registerPreviewSnapshotProvider(null));
319
+
295
320
  function noopUpdate() {}
296
321
  function noopSelect() {}
297
322
  </script>
@@ -1,5 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
3
+ import {
4
+ registerAbuilderViewHandle,
5
+ type AbuilderViewHandle,
6
+ } from "./library/abuilder-view-handle";
7
+ import {
8
+ captureEditorPreviewSnapshot,
9
+ registerPreviewSnapshotProvider,
10
+ } from "./library/preview-snapshot";
11
+ import { readOutputScale } from "./utils/outputScale";
3
12
  import type { State } from "@arronqzy/rx-store";
4
13
  import {
5
14
  useBlueprintDslLocaleSync,
@@ -90,6 +99,11 @@ const props = withDefaults(
90
99
  initialZoom?: number;
91
100
  nameSpace?: string | null;
92
101
  initialWorkspace?: WorkspaceProjectRecord | null;
102
+ /**
103
+ * 按 IndexedDB id 打开工作区(不 remount)。
104
+ * 与 initialWorkspace 同时存在时:首次优先完整记录;之后 id 变化从 IndexedDB 打开。
105
+ */
106
+ workspaceId?: string | null;
93
107
  /** 顶栏「设置 → 关于」展示的 npm 包名 */
94
108
  packageName?: string | null;
95
109
  /** 顶栏「设置 → 关于」展示的包版本号 */
@@ -99,6 +113,7 @@ const props = withDefaults(
99
113
  initialZoom: 1,
100
114
  nameSpace: null,
101
115
  initialWorkspace: null,
116
+ workspaceId: null,
102
117
  packageName: null,
103
118
  packageVersion: null,
104
119
  }
@@ -470,9 +485,39 @@ const workspaceProjects = useWorkspaceProjects({
470
485
  panelRevision: computed(() => `${historyCursor.value}|${allElements.value.length}`),
471
486
  onProjectApplied: handleWorkspaceProjectApplied,
472
487
  initialWorkspace: () => props.initialWorkspace,
488
+ workspaceId: () => props.workspaceId,
473
489
  nameSpace: () => props.nameSpace,
474
490
  });
475
491
 
492
+ const viewHandle: AbuilderViewHandle = {
493
+ getNameSpace: () => props.nameSpace ?? null,
494
+ getCurrentWorkspace: () => workspaceProjects.getCurrentWorkspaceRecord(),
495
+ openWorkspace: (idOrRecord) => workspaceProjects.handleOpenProject(idOrRecord),
496
+ createWorkspace: () => workspaceProjects.handleCreateProject(),
497
+ saveWorkspace: () => workspaceProjects.handleSaveProject(),
498
+ syncWorkspace: () => workspaceProjects.handleSyncProject(),
499
+ };
500
+
501
+ defineExpose(viewHandle);
502
+ onUnmounted(registerAbuilderViewHandle(viewHandle));
503
+
504
+ watch(
505
+ [canvasEl, allElements, layers, activeLayerId, () => props.nameSpace],
506
+ () => {
507
+ registerPreviewSnapshotProvider(async () =>
508
+ captureEditorPreviewSnapshot({
509
+ canvasRoot: canvasEl.value,
510
+ allElements: allElements.value,
511
+ layers: layers.value,
512
+ activeLayerId: activeLayerId.value,
513
+ outputScale: readOutputScale(props.nameSpace),
514
+ })
515
+ );
516
+ },
517
+ { immediate: true }
518
+ );
519
+ onUnmounted(() => registerPreviewSnapshotProvider(null));
520
+
476
521
  const blueprintLifecycleToken = computed(() => {
477
522
  const doc = blueprintGraph.value.document;
478
523
  const nodes = doc.nodes;
@@ -646,7 +691,10 @@ function handleApiCollectionUploadStart(info: {
646
691
  fileNames: string[];
647
692
  count: number;
648
693
  }) {
649
- emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, info);
694
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, {
695
+ ...info,
696
+ nameSpace: props.nameSpace ?? null,
697
+ });
650
698
  }
651
699
 
652
700
  function handleApiCollectionUploadSuccess(info: {
@@ -657,7 +705,10 @@ function handleApiCollectionUploadSuccess(info: {
657
705
  persistLocal: boolean;
658
706
  }>;
659
707
  }) {
660
- emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, info);
708
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, {
709
+ ...info,
710
+ nameSpace: props.nameSpace ?? null,
711
+ });
661
712
  }
662
713
 
663
714
  function handleApiCollectionSync(record: {
@@ -11,16 +11,16 @@ import {
11
11
  createWorkspaceProjectId,
12
12
  getWorkspaceProject,
13
13
  listWorkspaceProjects,
14
- putWorkspaceProject,
15
14
  type WorkspaceProjectListItem,
16
15
  type WorkspaceProjectRecord,
17
16
  } from "../library/workspace-project-db";
18
- import { deleteWorkspaceProject } from "../library/workspace-storage";
19
17
  import {
20
- broadcastWorkspaceProjectUpdate,
21
- subscribeWorkspaceProjectsChanged,
22
- } from "../library/workspace-project-sync";
18
+ deleteWorkspaceProject,
19
+ putWorkspaceProject,
20
+ } from "../library/workspace-storage";
21
+ import { subscribeWorkspaceProjectsChanged } from "../library/workspace-project-sync";
23
22
  import { writeWorkspacePreviewCache, readWorkspacePreviewCache } from "../library/workspace-project-cache";
23
+ import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
24
24
  import {
25
25
  cloneWorkspaceSnapshot,
26
26
  workspaceSnapshotsEqual,
@@ -48,6 +48,11 @@ export type UseWorkspaceProjectsOptions = {
48
48
  onProjectApplied?: (record: WorkspaceProjectRecord) => void;
49
49
  /** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
50
50
  initialWorkspace?: MaybeRefOrGetter<WorkspaceProjectRecord | null | undefined>;
51
+ /**
52
+ * 按 IndexedDB id 打开工作区。与 initialWorkspace 同时存在时:
53
+ * 首次优先 initialWorkspace;之后 workspaceId 变化会从 IndexedDB 打开。
54
+ */
55
+ workspaceId?: MaybeRefOrGetter<string | null | undefined>;
51
56
  nameSpace?: MaybeRefOrGetter<string | null | undefined>;
52
57
  };
53
58
 
@@ -94,9 +99,11 @@ function workspaceApplyKey(record: WorkspaceProjectRecord): string {
94
99
  export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
95
100
  const { t, locale } = useI18nOptional();
96
101
  const nameSpace = () => toValue(options.nameSpace) ?? null;
102
+ const eventNameSpace = () => normalizeAppNameSpace(nameSpace()) || null;
97
103
  const projects = ref<WorkspaceProjectListItem[]>([]);
98
104
  const activeProjectId = ref<string | null>(null);
99
105
  const activeProjectName = ref<string | null>(null);
106
+ let activeCreatedAt: number | null = null;
100
107
  const syncedSnapshotRef = shallowRef<WorkspaceSnapshot | null>(null);
101
108
  let syncedPanelRevision: unknown = null;
102
109
  let pendingRevisionSync = false;
@@ -150,7 +157,9 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
150
157
  const active = activeProjectId.value;
151
158
  const removedActive =
152
159
  message.type === "cleared" ||
153
- (active !== null && message.projectIds.includes(active));
160
+ (message.type === "deleted" &&
161
+ active !== null &&
162
+ message.projectIds.includes(active));
154
163
  if (removedActive) {
155
164
  activeProjectId.value = null;
156
165
  activeProjectName.value = null;
@@ -187,9 +196,61 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
187
196
  return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.value);
188
197
  });
189
198
 
199
+ async function ensureWorkspaceInIndexedDb(record: WorkspaceProjectRecord) {
200
+ const id = typeof record.id === "string" ? record.id.trim() : "";
201
+ if (!id) return;
202
+ try {
203
+ const existing = await getWorkspaceProject(id, nameSpace());
204
+ const incomingUpdatedAt =
205
+ typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt)
206
+ ? record.updatedAt
207
+ : 0;
208
+ if (existing && existing.updatedAt >= incomingUpdatedAt) {
209
+ return;
210
+ }
211
+ const now = Date.now();
212
+ const name =
213
+ (typeof record.name === "string" && record.name.trim()) ||
214
+ (typeof record.productName === "string" && record.productName.trim()) ||
215
+ t("panel.defaults.unnamedProduct");
216
+ const toStore: WorkspaceProjectRecord = {
217
+ ...record,
218
+ id,
219
+ name,
220
+ createdAt: existing?.createdAt ?? record.createdAt ?? now,
221
+ updatedAt: incomingUpdatedAt || now,
222
+ panelState:
223
+ normalizeImportedPanelState(record.panelState) ??
224
+ existing?.panelState ??
225
+ createEmptyPanelState(),
226
+ blueprintDocument:
227
+ parseBlueprintData(record.blueprintDocument).value ??
228
+ existing?.blueprintDocument ??
229
+ BlueprintGraph.empty().document,
230
+ blueprintMeta: {
231
+ name:
232
+ record.blueprintMeta?.name ??
233
+ existing?.blueprintMeta?.name ??
234
+ t("panel.defaults.unnamedBlueprint"),
235
+ remark:
236
+ record.blueprintMeta?.remark ?? existing?.blueprintMeta?.remark ?? "",
237
+ },
238
+ productName:
239
+ typeof record.productName === "string" && record.productName.trim()
240
+ ? record.productName
241
+ : existing?.productName ?? t("panel.defaults.unnamedProduct"),
242
+ titleIconDataUrl: record.titleIconDataUrl ?? existing?.titleIconDataUrl,
243
+ };
244
+ await putWorkspaceProject(toStore, nameSpace());
245
+ await refreshProjects();
246
+ } catch (error) {
247
+ console.warn("[abuilder] ensureWorkspaceInIndexedDb failed", error);
248
+ }
249
+ }
250
+
190
251
  function applyProjectRecord(
191
252
  record: WorkspaceProjectRecord,
192
- applyOptions?: { notifyApplied?: boolean }
253
+ applyOptions?: { notifyApplied?: boolean; persistToIndexedDb?: boolean }
193
254
  ) {
194
255
  const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
195
256
  const blueprintDocument =
@@ -213,6 +274,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
213
274
  options.setTitleIconDataUrl(titleIconDataUrl);
214
275
  activeProjectId.value = record.id || null;
215
276
  activeProjectName.value = record.name ?? null;
277
+ activeCreatedAt =
278
+ typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
279
+ ? record.createdAt
280
+ : Date.now();
216
281
  syncedSnapshotRef.value = cloneWorkspaceSnapshot({
217
282
  blueprintDocument,
218
283
  blueprintMeta,
@@ -221,15 +286,23 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
221
286
  });
222
287
  syncedPanelRevision = toValue(options.panelRevision);
223
288
  pendingRevisionSync = true;
289
+ const normalized: WorkspaceProjectRecord = {
290
+ ...record,
291
+ id: record.id,
292
+ name: record.name ?? productName,
293
+ createdAt: activeCreatedAt,
294
+ updatedAt: record.updatedAt ?? Date.now(),
295
+ blueprintDocument,
296
+ blueprintMeta,
297
+ panelState,
298
+ productName,
299
+ titleIconDataUrl: titleIconDataUrl || undefined,
300
+ };
224
301
  if (applyOptions?.notifyApplied !== false) {
225
- options.onProjectApplied?.({
226
- ...record,
227
- blueprintDocument,
228
- blueprintMeta,
229
- panelState,
230
- productName,
231
- titleIconDataUrl: titleIconDataUrl || undefined,
232
- });
302
+ options.onProjectApplied?.(normalized);
303
+ }
304
+ if (applyOptions?.persistToIndexedDb) {
305
+ void ensureWorkspaceInIndexedDb(normalized);
233
306
  }
234
307
  }
235
308
 
@@ -244,46 +317,13 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
244
317
  options.setTitleIconDataUrl("");
245
318
  activeProjectId.value = null;
246
319
  activeProjectName.value = null;
320
+ activeCreatedAt = null;
247
321
  syncedSnapshotRef.value = null;
248
322
  }
249
323
 
250
324
  let bootstrapped = false;
251
325
  let appliedWorkspaceKey: string | null = null;
252
-
253
- function bootstrapInitialWorkspace() {
254
- const initialWorkspace = toValue(options.initialWorkspace) ?? null;
255
- if (!bootstrapped) {
256
- bootstrapped = true;
257
- if (hasWorkspacePayload(initialWorkspace)) {
258
- applyProjectRecord(initialWorkspace, { notifyApplied: false });
259
- appliedWorkspaceKey = workspaceApplyKey(initialWorkspace);
260
- } else {
261
- applyEmptyWorkspace();
262
- }
263
- return;
264
- }
265
- if (!hasWorkspacePayload(initialWorkspace)) return;
266
- const key = workspaceApplyKey(initialWorkspace);
267
- if (appliedWorkspaceKey === key) return;
268
- appliedWorkspaceKey = key;
269
- applyProjectRecord(initialWorkspace, { notifyApplied: false });
270
- }
271
-
272
- onMounted(() => {
273
- bootstrapInitialWorkspace();
274
- void refreshProjects();
275
- previewPollTimer = window.setInterval(refreshPreviewing, 2000);
276
- });
277
-
278
- watch(
279
- () => {
280
- const record = toValue(options.initialWorkspace);
281
- return record ? workspaceApplyKey(record) : "";
282
- },
283
- () => {
284
- bootstrapInitialWorkspace();
285
- }
286
- );
326
+ let workspaceIdApplied: string | null = null;
287
327
 
288
328
  async function persistProject(persistOptions: {
289
329
  id?: string;
@@ -294,7 +334,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
294
334
  const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
295
335
  const now = Date.now();
296
336
  const id = persistOptions.id ?? createWorkspaceProjectId();
297
- const createdAt = persistOptions.createdAt ?? now;
337
+ const createdAt = persistOptions.createdAt ?? activeCreatedAt ?? now;
298
338
  const name = (persistOptions.name ?? "").trim() || resolveProjectName();
299
339
 
300
340
  let panelState: State;
@@ -320,10 +360,9 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
320
360
  };
321
361
 
322
362
  await putWorkspaceProject(record, nameSpace());
323
- writeWorkspacePreviewCache(record, nameSpace());
324
- broadcastWorkspaceProjectUpdate(id, now, nameSpace());
325
363
  activeProjectId.value = id;
326
364
  activeProjectName.value = name;
365
+ activeCreatedAt = createdAt;
327
366
  syncedSnapshotRef.value = snapshot;
328
367
  syncedPanelRevision = toValue(options.panelRevision);
329
368
  pendingRevisionSync = false;
@@ -334,11 +373,40 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
334
373
  }
335
374
  }
336
375
 
376
+ function getCurrentWorkspaceRecord(): WorkspaceProjectRecord | null {
377
+ if (!activeProjectId.value) return null;
378
+ try {
379
+ const snapshot = buildCurrentSnapshot();
380
+ const panelState = options.exportPanelData();
381
+ const now = Date.now();
382
+ return {
383
+ id: activeProjectId.value,
384
+ name: activeProjectName.value || resolveProjectName(),
385
+ createdAt: activeCreatedAt ?? now,
386
+ updatedAt: now,
387
+ panelState,
388
+ blueprintDocument: snapshot.blueprintDocument,
389
+ blueprintMeta: snapshot.blueprintMeta,
390
+ productName:
391
+ typeof snapshot.productName === "string"
392
+ ? snapshot.productName
393
+ : resolveProjectName(),
394
+ titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
395
+ };
396
+ } catch (error) {
397
+ console.warn("[abuilder] getCurrentWorkspace failed", error);
398
+ return null;
399
+ }
400
+ }
401
+
337
402
  async function handleCreateProject() {
338
403
  return runBusyTask(t("common.savingWorkspace"), async () => {
339
404
  const name = resolveProjectName();
340
405
  const record = await persistProject({ name });
341
- emitAbuilderEvent(AbuilderEvents.workspaceAdd, record);
406
+ emitAbuilderEvent(AbuilderEvents.workspaceAdd, {
407
+ ...record,
408
+ nameSpace: eventNameSpace(),
409
+ });
342
410
  return { name, id: record.id };
343
411
  });
344
412
  }
@@ -354,16 +422,24 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
354
422
  const record = await persistProject({
355
423
  id: projectId,
356
424
  name,
357
- createdAt: existing?.createdAt,
425
+ createdAt: existing?.createdAt ?? activeCreatedAt ?? undefined,
426
+ });
427
+ emitAbuilderEvent(AbuilderEvents.workspaceSync, {
428
+ ...record,
429
+ nameSpace: eventNameSpace(),
358
430
  });
359
431
  return { name, id: record.id };
360
432
  });
361
433
  }
362
434
 
363
- async function handleOpenProject(id: string) {
435
+ async function handleOpenProject(idOrRecord: string | WorkspaceProjectRecord) {
364
436
  try {
365
437
  await runBusyTask(t("common.loadingWorkspace"), async () => {
366
- const record = await getWorkspaceProject(id, nameSpace());
438
+ if (typeof idOrRecord !== "string") {
439
+ applyProjectRecord(idOrRecord, { persistToIndexedDb: true });
440
+ return;
441
+ }
442
+ const record = await getWorkspaceProject(idOrRecord, nameSpace());
367
443
  if (!record) {
368
444
  await refreshProjects();
369
445
  throw new Error(t("panel.messages.workspaceNotFound"));
@@ -381,20 +457,17 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
381
457
  throw new Error(t("panel.messages.saveWorkspaceFirst"));
382
458
  }
383
459
  const existing = await getWorkspaceProject(activeProjectId.value, nameSpace());
384
- if (!existing) {
385
- activeProjectId.value = null;
386
- activeProjectName.value = null;
387
- syncedSnapshotRef.value = null;
388
- await refreshProjects();
389
- throw new Error(t("panel.messages.workspaceNotFound"));
390
- }
391
- const name = resolveProjectName() || existing.name;
460
+ const name =
461
+ resolveProjectName() || existing?.name || activeProjectName.value || "";
392
462
  const record = await persistProject({
393
463
  id: activeProjectId.value,
394
464
  name,
395
- createdAt: existing.createdAt,
465
+ createdAt: existing?.createdAt ?? activeCreatedAt ?? undefined,
466
+ });
467
+ emitAbuilderEvent(AbuilderEvents.workspaceSync, {
468
+ ...record,
469
+ nameSpace: eventNameSpace(),
396
470
  });
397
- emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
398
471
  return name;
399
472
  });
400
473
  }
@@ -406,6 +479,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
406
479
  if (activeProjectId.value === id) {
407
480
  activeProjectId.value = null;
408
481
  activeProjectName.value = null;
482
+ activeCreatedAt = null;
409
483
  syncedSnapshotRef.value = null;
410
484
  }
411
485
  previewWindows.delete(id);
@@ -456,6 +530,73 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
456
530
  });
457
531
  }
458
532
 
533
+ function bootstrapInitialWorkspace() {
534
+ const initialWorkspace = toValue(options.initialWorkspace) ?? null;
535
+ const workspaceIdRaw = toValue(options.workspaceId);
536
+ const workspaceId =
537
+ typeof workspaceIdRaw === "string" ? workspaceIdRaw.trim() : "";
538
+ if (!bootstrapped) {
539
+ bootstrapped = true;
540
+ if (hasWorkspacePayload(initialWorkspace)) {
541
+ applyProjectRecord(initialWorkspace, {
542
+ notifyApplied: false,
543
+ persistToIndexedDb: true,
544
+ });
545
+ appliedWorkspaceKey = workspaceApplyKey(initialWorkspace);
546
+ if (initialWorkspace.id) workspaceIdApplied = initialWorkspace.id;
547
+ } else if (workspaceId) {
548
+ workspaceIdApplied = workspaceId;
549
+ void handleOpenProject(workspaceId);
550
+ } else {
551
+ applyEmptyWorkspace();
552
+ }
553
+ return;
554
+ }
555
+ if (!hasWorkspacePayload(initialWorkspace)) return;
556
+ const key = workspaceApplyKey(initialWorkspace);
557
+ if (appliedWorkspaceKey === key) return;
558
+ appliedWorkspaceKey = key;
559
+ if (initialWorkspace.id) workspaceIdApplied = initialWorkspace.id;
560
+ applyProjectRecord(initialWorkspace, {
561
+ notifyApplied: false,
562
+ persistToIndexedDb: true,
563
+ });
564
+ }
565
+
566
+ onMounted(() => {
567
+ bootstrapInitialWorkspace();
568
+ void refreshProjects();
569
+ previewPollTimer = window.setInterval(refreshPreviewing, 2000);
570
+ });
571
+
572
+ watch(
573
+ () => {
574
+ const record = toValue(options.initialWorkspace);
575
+ return record ? workspaceApplyKey(record) : "";
576
+ },
577
+ () => {
578
+ bootstrapInitialWorkspace();
579
+ }
580
+ );
581
+
582
+ watch(
583
+ () => {
584
+ const raw = toValue(options.workspaceId);
585
+ return typeof raw === "string" ? raw.trim() : "";
586
+ },
587
+ (id) => {
588
+ if (!bootstrapped) return;
589
+ if (!id) return;
590
+ if (workspaceIdApplied === id) return;
591
+ if (activeProjectId.value === id) {
592
+ workspaceIdApplied = id;
593
+ return;
594
+ }
595
+ workspaceIdApplied = id;
596
+ void handleOpenProject(id);
597
+ }
598
+ );
599
+
459
600
  return {
460
601
  projects,
461
602
  activeProjectId,
@@ -468,6 +609,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
468
609
  handleSyncProject,
469
610
  handleDeleteProject,
470
611
  handlePreviewProject,
612
+ getCurrentWorkspaceRecord,
471
613
  refreshProjects,
472
614
  };
473
615
  }