@arronqzy/vue-view 0.1.24 → 0.1.26

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.
@@ -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
  }
@@ -119,16 +119,20 @@ export class PanelRuler {
119
119
  ctx.moveTo(px + 0.5, height);
120
120
  ctx.lineTo(px + 0.5, height - lineLen);
121
121
  if (isMajor) {
122
- const label = String(Math.round((i * unit) / segment));
123
- ctx.fillText(label, px + 2, 2);
122
+ // 主刻度标签:世界坐标 = i * (unit/segment)
123
+ const label = String(Math.round(i * (unit / segment)));
124
+ ctx.fillText(label, px + 3, 1);
124
125
  }
125
126
  } else {
126
127
  ctx.moveTo(width, px + 0.5);
127
128
  ctx.lineTo(width - lineLen, px + 0.5);
128
129
  if (isMajor) {
129
- const label = String(Math.round((i * unit) / segment));
130
+ const label = String(Math.round(i * (unit / segment)));
130
131
  ctx.save();
131
- ctx.translate(2, px + 2);
132
+ ctx.translate(1, px + 3);
133
+ ctx.rotate(-Math.PI / 2);
134
+ ctx.textAlign = "right";
135
+ ctx.textBaseline = "top";
132
136
  ctx.fillText(label, 0, 0);
133
137
  ctx.restore();
134
138
  }
@@ -1,10 +1,22 @@
1
+ import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
1
2
  import {
2
3
  clearAllExecutionRunRecords,
3
- clearApiCollections,
4
+ clearApiCollections as clearApiCollectionsRaw,
4
5
  clearBlueprintLibrary as clearBlueprintLibraryRecords,
6
+ deleteApiCollection as deleteApiCollectionRaw,
5
7
  deleteBlueprintLibraryRecord as deleteBlueprintLibraryRecordRaw,
8
+ putApiCollection as putApiCollectionRaw,
9
+ putBlueprintLibraryRecord as putBlueprintLibraryRecordRaw,
10
+ putExecutionRunRecord as putExecutionRunRecordRaw,
11
+ upsertApiCollectionFromJson as upsertApiCollectionFromJsonRaw,
12
+ type BlueprintLibraryRecord,
13
+ type ExecutionRunRecord,
14
+ type UpsertApiCollectionInput,
15
+ type UpsertApiCollectionResult,
6
16
  } from "@arronqzy/vue-blueprint";
17
+ import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
7
18
  import { broadcastBlueprintLibraryChanged } from "./blueprint-library-sync";
19
+ import { AbuilderEvents, emitAbuilderEvent } from "./event-subscription";
8
20
  import { clearWorkspaceProjects } from "./workspace-storage";
9
21
 
10
22
  export {
@@ -19,17 +31,36 @@ export {
19
31
  readExecutionLogSettings,
20
32
  writeExecutionLogSettings,
21
33
  listApiCollectionRecords,
22
- deleteApiCollection,
23
- clearApiCollections,
24
34
  } from "@arronqzy/vue-blueprint";
25
35
  export type {
26
36
  BlueprintLibraryListItem,
27
37
  BlueprintLibraryRecord,
28
38
  ExecutionRunRecord,
29
39
  ExecutionLogSettings,
40
+ UpsertApiCollectionInput,
41
+ UpsertApiCollectionResult,
30
42
  } from "@arronqzy/vue-blueprint";
31
43
  export type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
32
44
 
45
+ function resolvedNameSpace(nameSpace?: string | null): string | null {
46
+ return normalizeAppNameSpace(nameSpace) || null;
47
+ }
48
+
49
+ /** 写入 / 覆盖蓝图库记录;已挂载的编辑器会刷新蓝图库列表 */
50
+ export async function putBlueprintLibraryRecord(
51
+ record: BlueprintLibraryRecord,
52
+ nameSpace?: string | null
53
+ ): Promise<BlueprintLibraryRecord> {
54
+ const saved = await putBlueprintLibraryRecordRaw(record, nameSpace);
55
+ broadcastBlueprintLibraryChanged(nameSpace);
56
+ emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
57
+ nameSpace: resolvedNameSpace(nameSpace),
58
+ action: "put",
59
+ ids: [saved.id],
60
+ });
61
+ return saved;
62
+ }
63
+
33
64
  /** 删除蓝图库中的一条蓝图;已挂载的编辑器会刷新蓝图库列表 */
34
65
  export async function deleteBlueprintLibraryRecord(
35
66
  id: string,
@@ -37,12 +68,79 @@ export async function deleteBlueprintLibraryRecord(
37
68
  ): Promise<void> {
38
69
  await deleteBlueprintLibraryRecordRaw(id, nameSpace);
39
70
  broadcastBlueprintLibraryChanged(nameSpace);
71
+ emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
72
+ nameSpace: resolvedNameSpace(nameSpace),
73
+ action: "delete",
74
+ ids: [id],
75
+ });
40
76
  }
41
77
 
42
78
  /** 清空该 nameSpace 下的蓝图库,返回删除数量;已挂载的编辑器会刷新蓝图库列表 */
43
79
  export async function clearBlueprintLibrary(nameSpace?: string | null): Promise<number> {
44
80
  const count = await clearBlueprintLibraryRecords(nameSpace);
45
81
  broadcastBlueprintLibraryChanged(nameSpace);
82
+ emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
83
+ nameSpace: resolvedNameSpace(nameSpace),
84
+ action: "clear",
85
+ });
86
+ return count;
87
+ }
88
+
89
+ /** 写入一条蓝图执行日志 */
90
+ export async function putExecutionRunRecord(
91
+ record: ExecutionRunRecord,
92
+ nameSpace?: string | null
93
+ ): Promise<ExecutionRunRecord> {
94
+ return putExecutionRunRecordRaw(record, nameSpace);
95
+ }
96
+
97
+ /** 写入接口集合;persistLocal 时落 IndexedDB,并同步已挂载编辑器的内存列表 */
98
+ export async function putApiCollection(
99
+ record: ApiCollectionRecord,
100
+ nameSpace?: string | null
101
+ ): Promise<ApiCollectionRecord> {
102
+ const saved = await putApiCollectionRaw(record, nameSpace);
103
+ emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
104
+ nameSpace: resolvedNameSpace(nameSpace),
105
+ action: "put",
106
+ ids: [saved.id],
107
+ });
108
+ return saved;
109
+ }
110
+
111
+ /** 从 JSON 文本 upsert 接口集合;可传 nameSpace */
112
+ export async function upsertApiCollectionFromJson(
113
+ input: UpsertApiCollectionInput
114
+ ): Promise<UpsertApiCollectionResult> {
115
+ const result = await upsertApiCollectionFromJsonRaw(input);
116
+ if (result.ok) {
117
+ emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
118
+ nameSpace: resolvedNameSpace(input.nameSpace),
119
+ action: "put",
120
+ ids: [result.record.id],
121
+ });
122
+ }
123
+ return result;
124
+ }
125
+
126
+ export async function deleteApiCollection(
127
+ id: string,
128
+ nameSpace?: string | null
129
+ ): Promise<void> {
130
+ await deleteApiCollectionRaw(id, nameSpace);
131
+ emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
132
+ nameSpace: resolvedNameSpace(nameSpace),
133
+ action: "delete",
134
+ ids: [id],
135
+ });
136
+ }
137
+
138
+ export async function clearApiCollections(nameSpace?: string | null): Promise<number> {
139
+ const count = await clearApiCollectionsRaw(nameSpace);
140
+ emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
141
+ nameSpace: resolvedNameSpace(nameSpace),
142
+ action: "clear",
143
+ });
46
144
  return count;
47
145
  }
48
146
 
@@ -0,0 +1,100 @@
1
+ import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
2
+ import type { WorkspaceProjectRecord } from "./workspace-project-db";
3
+
4
+ /** 已挂载编辑面板的命令式句柄(也可通过 `ref` 拿到) */
5
+ export type AbuilderViewHandle = {
6
+ /** 当前 nameSpace */
7
+ getNameSpace: () => string | null;
8
+ /** 导出当前编辑器中的完整工作区;尚无关联 id 时返回 null */
9
+ getCurrentWorkspace: () => WorkspaceProjectRecord | null;
10
+ /** 按 id 从 IndexedDB 打开,或直接应用完整记录 */
11
+ openWorkspace: (idOrRecord: string | WorkspaceProjectRecord) => Promise<void>;
12
+ /** 创建新工作区并写入 IndexedDB,发出 workspace:add */
13
+ createWorkspace: () => Promise<{ name: string; id: string }>;
14
+ /** 保存当前工作区到 IndexedDB,发出 workspace:sync;无 active 时等同创建 */
15
+ saveWorkspace: () => Promise<{ name: string; id: string }>;
16
+ /** 同步当前工作区到 IndexedDB,发出 workspace:sync */
17
+ syncWorkspace: () => Promise<string>;
18
+ };
19
+
20
+ const handles = new Set<AbuilderViewHandle>();
21
+
22
+ export function registerAbuilderViewHandle(handle: AbuilderViewHandle): () => void {
23
+ handles.add(handle);
24
+ return () => {
25
+ handles.delete(handle);
26
+ };
27
+ }
28
+
29
+ function findHandle(nameSpace?: string | null): AbuilderViewHandle | null {
30
+ if (handles.size === 0) return null;
31
+ if (nameSpace === undefined) {
32
+ return handles.values().next().value ?? null;
33
+ }
34
+ const wanted = normalizeAppNameSpace(nameSpace);
35
+ for (const handle of handles) {
36
+ if (normalizeAppNameSpace(handle.getNameSpace()) === wanted) return handle;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * 读取当前已挂载编辑器中的工作区快照。
43
+ * 同页多个实例时请传入 `nameSpace`;未挂载或尚无 active 工作区时返回 null。
44
+ */
45
+ export function getCurrentWorkspace(
46
+ nameSpace?: string | null
47
+ ): WorkspaceProjectRecord | null {
48
+ return findHandle(nameSpace)?.getCurrentWorkspace() ?? null;
49
+ }
50
+
51
+ /**
52
+ * 命令式打开工作区(不 remount)。
53
+ * 同页多个实例时请传入 `nameSpace`。
54
+ */
55
+ export async function openWorkspace(
56
+ idOrRecord: string | WorkspaceProjectRecord,
57
+ nameSpace?: string | null
58
+ ): Promise<void> {
59
+ const handle = findHandle(nameSpace);
60
+ if (!handle) {
61
+ throw new Error(
62
+ "No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
63
+ );
64
+ }
65
+ await handle.openWorkspace(idOrRecord);
66
+ }
67
+
68
+ export async function createWorkspace(
69
+ nameSpace?: string | null
70
+ ): Promise<{ name: string; id: string }> {
71
+ const handle = findHandle(nameSpace);
72
+ if (!handle) {
73
+ throw new Error(
74
+ "No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
75
+ );
76
+ }
77
+ return handle.createWorkspace();
78
+ }
79
+
80
+ export async function saveWorkspace(
81
+ nameSpace?: string | null
82
+ ): Promise<{ name: string; id: string }> {
83
+ const handle = findHandle(nameSpace);
84
+ if (!handle) {
85
+ throw new Error(
86
+ "No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
87
+ );
88
+ }
89
+ return handle.saveWorkspace();
90
+ }
91
+
92
+ export async function syncWorkspace(nameSpace?: string | null): Promise<string> {
93
+ const handle = findHandle(nameSpace);
94
+ if (!handle) {
95
+ throw new Error(
96
+ "No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
97
+ );
98
+ }
99
+ return handle.syncWorkspace();
100
+ }