@arronqzy/react-view 1.0.38 → 1.0.40

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.
@@ -7,16 +7,16 @@ import {
7
7
  createWorkspaceProjectId,
8
8
  getWorkspaceProject,
9
9
  listWorkspaceProjects,
10
- putWorkspaceProject,
11
10
  type WorkspaceProjectListItem,
12
11
  type WorkspaceProjectRecord,
13
12
  } from "../library/workspace-project-db";
14
- import { deleteWorkspaceProject } from "../library/workspace-storage";
15
13
  import {
16
- broadcastWorkspaceProjectUpdate,
17
- subscribeWorkspaceProjectsChanged,
18
- } from "../library/workspace-project-sync";
14
+ deleteWorkspaceProject,
15
+ putWorkspaceProject,
16
+ } from "../library/workspace-storage";
17
+ import { subscribeWorkspaceProjectsChanged } from "../library/workspace-project-sync";
19
18
  import { writeWorkspacePreviewCache, readWorkspacePreviewCache } from "../library/workspace-project-cache";
19
+ import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
20
20
  import {
21
21
  cloneWorkspaceSnapshot,
22
22
  workspaceSnapshotsEqual,
@@ -45,6 +45,11 @@ export type UseWorkspaceProjectsOptions = {
45
45
  onProjectApplied?: (record: WorkspaceProjectRecord) => void;
46
46
  /** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
47
47
  initialWorkspace?: WorkspaceProjectRecord | null;
48
+ /**
49
+ * 按 IndexedDB id 打开工作区。与 `initialWorkspace` 同时存在时:
50
+ * 首次优先 `initialWorkspace`;之后 `workspaceId` 变化会从 IndexedDB 打开。
51
+ */
52
+ workspaceId?: string | null;
48
53
  /** 隔离 IndexedDB / 预览缓存 / 同步频道 */
49
54
  nameSpace?: string | null;
50
55
  };
@@ -103,17 +108,20 @@ export function useWorkspaceProjects({
103
108
  panelRevision,
104
109
  onProjectApplied,
105
110
  initialWorkspace,
111
+ workspaceId = null,
106
112
  nameSpace = null,
107
113
  }: UseWorkspaceProjectsOptions) {
108
114
  const { t } = useI18nOptional();
109
115
  const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
110
116
  const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
111
117
  const [activeProjectName, setActiveProjectName] = useState<string | null>(null);
118
+ const activeCreatedAtRef = useRef<number | null>(null);
112
119
  const syncedSnapshotRef = useRef<WorkspaceSnapshot | null>(null);
113
120
  const syncedPanelRevisionRef = useRef<unknown>(null);
114
121
  const pendingRevisionSyncRef = useRef(false);
115
122
  const previewWindowsRef = useRef(new Map<string, Window>());
116
123
  const [previewingProjectIds, setPreviewingProjectIds] = useState<string[]>([]);
124
+ const eventNameSpace = normalizeAppNameSpace(nameSpace) || null;
117
125
 
118
126
  const refreshPreviewing = useCallback(() => {
119
127
  const ids: string[] = [];
@@ -166,7 +174,9 @@ export function useWorkspaceProjects({
166
174
  return subscribeWorkspaceProjectsChanged((message) => {
167
175
  const removedActive =
168
176
  message.type === "cleared" ||
169
- (activeProjectId !== null && message.projectIds.includes(activeProjectId));
177
+ (message.type === "deleted" &&
178
+ activeProjectId !== null &&
179
+ message.projectIds.includes(activeProjectId));
170
180
  if (removedActive) {
171
181
  setActiveProjectId(null);
172
182
  setActiveProjectName(null);
@@ -197,8 +207,67 @@ export function useWorkspaceProjects({
197
207
  buildCurrentSnapshot,
198
208
  ]);
199
209
 
210
+ const ensureWorkspaceInIndexedDb = useCallback(
211
+ async (record: WorkspaceProjectRecord) => {
212
+ const id = typeof record.id === "string" ? record.id.trim() : "";
213
+ if (!id) return;
214
+ try {
215
+ const existing = await getWorkspaceProject(id, nameSpace);
216
+ const incomingUpdatedAt =
217
+ typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt)
218
+ ? record.updatedAt
219
+ : 0;
220
+ if (existing && existing.updatedAt >= incomingUpdatedAt) {
221
+ return;
222
+ }
223
+ const now = Date.now();
224
+ const name =
225
+ (typeof record.name === "string" && record.name.trim()) ||
226
+ (typeof record.productName === "string" && record.productName.trim()) ||
227
+ t("panel.defaults.unnamedProduct");
228
+ const toStore: WorkspaceProjectRecord = {
229
+ ...record,
230
+ id,
231
+ name,
232
+ createdAt: existing?.createdAt ?? record.createdAt ?? now,
233
+ updatedAt: incomingUpdatedAt || now,
234
+ panelState:
235
+ normalizeImportedPanelState(record.panelState) ??
236
+ existing?.panelState ??
237
+ createEmptyPanelState(),
238
+ blueprintDocument:
239
+ parseBlueprintData(record.blueprintDocument).value ??
240
+ existing?.blueprintDocument ??
241
+ BlueprintGraph.empty().document,
242
+ blueprintMeta: {
243
+ name:
244
+ record.blueprintMeta?.name ??
245
+ existing?.blueprintMeta?.name ??
246
+ t("panel.defaults.unnamedBlueprint"),
247
+ remark:
248
+ record.blueprintMeta?.remark ?? existing?.blueprintMeta?.remark ?? "",
249
+ },
250
+ productName:
251
+ typeof record.productName === "string" && record.productName.trim()
252
+ ? record.productName
253
+ : existing?.productName ?? t("panel.defaults.unnamedProduct"),
254
+ titleIconDataUrl:
255
+ record.titleIconDataUrl ?? existing?.titleIconDataUrl,
256
+ };
257
+ await putWorkspaceProject(toStore, nameSpace);
258
+ await refreshProjects();
259
+ } catch (error) {
260
+ console.warn("[abuilder] ensureWorkspaceInIndexedDb failed", error);
261
+ }
262
+ },
263
+ [nameSpace, refreshProjects, t]
264
+ );
265
+
200
266
  const applyProjectRecord = useCallback(
201
- (record: WorkspaceProjectRecord, options?: { notifyApplied?: boolean }) => {
267
+ (
268
+ record: WorkspaceProjectRecord,
269
+ options?: { notifyApplied?: boolean; persistToIndexedDb?: boolean }
270
+ ) => {
202
271
  const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
203
272
  const blueprintDocument =
204
273
  parsedBlueprint.value ?? BlueprintGraph.empty().document;
@@ -221,6 +290,10 @@ export function useWorkspaceProjects({
221
290
  setTitleIconDataUrl(titleIconDataUrl);
222
291
  setActiveProjectId(record.id || null);
223
292
  setActiveProjectName(record.name ?? null);
293
+ activeCreatedAtRef.current =
294
+ typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
295
+ ? record.createdAt
296
+ : Date.now();
224
297
  syncedSnapshotRef.current = cloneWorkspaceSnapshot({
225
298
  blueprintDocument,
226
299
  blueprintMeta,
@@ -229,18 +302,27 @@ export function useWorkspaceProjects({
229
302
  });
230
303
  syncedPanelRevisionRef.current = panelRevision;
231
304
  pendingRevisionSyncRef.current = true;
305
+ const normalized: WorkspaceProjectRecord = {
306
+ ...record,
307
+ id: record.id,
308
+ name: record.name ?? productName,
309
+ createdAt: activeCreatedAtRef.current,
310
+ updatedAt: record.updatedAt ?? Date.now(),
311
+ blueprintDocument,
312
+ blueprintMeta,
313
+ panelState,
314
+ productName,
315
+ titleIconDataUrl: titleIconDataUrl || undefined,
316
+ };
232
317
  if (options?.notifyApplied !== false) {
233
- onProjectApplied?.({
234
- ...record,
235
- blueprintDocument,
236
- blueprintMeta,
237
- panelState,
238
- productName,
239
- titleIconDataUrl: titleIconDataUrl || undefined,
240
- });
318
+ onProjectApplied?.(normalized);
319
+ }
320
+ if (options?.persistToIndexedDb) {
321
+ void ensureWorkspaceInIndexedDb(normalized);
241
322
  }
242
323
  },
243
324
  [
325
+ ensureWorkspaceInIndexedDb,
244
326
  importPanelData,
245
327
  onProjectApplied,
246
328
  setBlueprintGraph,
@@ -263,6 +345,7 @@ export function useWorkspaceProjects({
263
345
  setTitleIconDataUrl("");
264
346
  setActiveProjectId(null);
265
347
  setActiveProjectName(null);
348
+ activeCreatedAtRef.current = null;
266
349
  syncedSnapshotRef.current = null;
267
350
  }, [
268
351
  importPanelData,
@@ -275,23 +358,7 @@ export function useWorkspaceProjects({
275
358
 
276
359
  const initialWorkspaceAppliedKeyRef = useRef<string | null>(null);
277
360
  const bootstrappedRef = useRef(false);
278
- useEffect(() => {
279
- if (!bootstrappedRef.current) {
280
- bootstrappedRef.current = true;
281
- if (hasWorkspacePayload(initialWorkspace)) {
282
- applyProjectRecord(initialWorkspace, { notifyApplied: false });
283
- initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
284
- } else {
285
- applyEmptyWorkspace();
286
- }
287
- return;
288
- }
289
- if (!hasWorkspacePayload(initialWorkspace)) return;
290
- const key = workspaceApplyKey(initialWorkspace);
291
- if (initialWorkspaceAppliedKeyRef.current === key) return;
292
- initialWorkspaceAppliedKeyRef.current = key;
293
- applyProjectRecord(initialWorkspace, { notifyApplied: false });
294
- }, [applyEmptyWorkspace, applyProjectRecord, initialWorkspace]);
361
+ const workspaceIdAppliedRef = useRef<string | null>(null);
295
362
 
296
363
  const persistProject = useCallback(
297
364
  async (options: { id?: string; name: string; createdAt?: number }) => {
@@ -299,7 +366,7 @@ export function useWorkspaceProjects({
299
366
  const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
300
367
  const now = Date.now();
301
368
  const id = options.id ?? createWorkspaceProjectId();
302
- const createdAt = options.createdAt ?? now;
369
+ const createdAt = options.createdAt ?? activeCreatedAtRef.current ?? now;
303
370
  const name = (options.name ?? "").trim() || resolveProjectName();
304
371
 
305
372
  let panelState: State;
@@ -325,10 +392,9 @@ export function useWorkspaceProjects({
325
392
  };
326
393
 
327
394
  await putWorkspaceProject(record, nameSpace);
328
- writeWorkspacePreviewCache(record, nameSpace);
329
- broadcastWorkspaceProjectUpdate(id, now, nameSpace);
330
395
  setActiveProjectId(id);
331
396
  setActiveProjectName(name);
397
+ activeCreatedAtRef.current = createdAt;
332
398
  syncedSnapshotRef.current = snapshot;
333
399
  syncedPanelRevisionRef.current = panelRevision;
334
400
  pendingRevisionSyncRef.current = false;
@@ -349,14 +415,49 @@ export function useWorkspaceProjects({
349
415
  ]
350
416
  );
351
417
 
418
+ const getCurrentWorkspaceRecord = useCallback((): WorkspaceProjectRecord | null => {
419
+ if (!activeProjectId) return null;
420
+ try {
421
+ const snapshot = buildCurrentSnapshot();
422
+ const panelState = exportPanelData();
423
+ const now = Date.now();
424
+ return {
425
+ id: activeProjectId,
426
+ name: activeProjectName || resolveProjectName(),
427
+ createdAt: activeCreatedAtRef.current ?? now,
428
+ updatedAt: now,
429
+ panelState,
430
+ blueprintDocument: snapshot.blueprintDocument,
431
+ blueprintMeta: snapshot.blueprintMeta,
432
+ productName:
433
+ typeof snapshot.productName === "string"
434
+ ? snapshot.productName
435
+ : resolveProjectName(),
436
+ titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
437
+ };
438
+ } catch (error) {
439
+ console.warn("[abuilder] getCurrentWorkspace failed", error);
440
+ return null;
441
+ }
442
+ }, [
443
+ activeProjectId,
444
+ activeProjectName,
445
+ buildCurrentSnapshot,
446
+ exportPanelData,
447
+ resolveProjectName,
448
+ ]);
449
+
352
450
  const handleCreateProject = useCallback(async () => {
353
451
  return runBusyTask(t("common.savingWorkspace"), async () => {
354
452
  const name = resolveProjectName();
355
453
  const record = await persistProject({ name });
356
- emitAbuilderEvent(AbuilderEvents.workspaceAdd, record);
454
+ emitAbuilderEvent(AbuilderEvents.workspaceAdd, {
455
+ ...record,
456
+ nameSpace: eventNameSpace,
457
+ });
357
458
  return { name, id: record.id };
358
459
  });
359
- }, [persistProject, resolveProjectName, t]);
460
+ }, [eventNameSpace, persistProject, resolveProjectName, t]);
360
461
 
361
462
  const handleSaveProject = useCallback(async () => {
362
463
  if (!activeProjectId) {
@@ -368,17 +469,33 @@ export function useWorkspaceProjects({
368
469
  const record = await persistProject({
369
470
  id: activeProjectId,
370
471
  name,
371
- createdAt: existing?.createdAt,
472
+ createdAt: existing?.createdAt ?? activeCreatedAtRef.current ?? undefined,
473
+ });
474
+ emitAbuilderEvent(AbuilderEvents.workspaceSync, {
475
+ ...record,
476
+ nameSpace: eventNameSpace,
372
477
  });
373
478
  return { name, id: record.id };
374
479
  });
375
- }, [activeProjectId, handleCreateProject, nameSpace, persistProject, resolveProjectName, t]);
480
+ }, [
481
+ activeProjectId,
482
+ eventNameSpace,
483
+ handleCreateProject,
484
+ nameSpace,
485
+ persistProject,
486
+ resolveProjectName,
487
+ t,
488
+ ]);
376
489
 
377
490
  const handleOpenProject = useCallback(
378
- async (id: string) => {
491
+ async (idOrRecord: string | WorkspaceProjectRecord) => {
379
492
  try {
380
493
  await runBusyTask(t("common.loadingWorkspace"), async () => {
381
- const record = await getWorkspaceProject(id, nameSpace);
494
+ if (typeof idOrRecord !== "string") {
495
+ applyProjectRecord(idOrRecord, { persistToIndexedDb: true });
496
+ return;
497
+ }
498
+ const record = await getWorkspaceProject(idOrRecord, nameSpace);
382
499
  if (!record) {
383
500
  await refreshProjects();
384
501
  throw new Error(t("panel.messages.workspaceNotFound"));
@@ -397,24 +514,29 @@ export function useWorkspaceProjects({
397
514
  if (!activeProjectId) {
398
515
  throw new Error(t("panel.messages.saveWorkspaceFirst"));
399
516
  }
517
+ // 本地无记录时 upsert,避免宿主 initialWorkspace 未落盘后同步报错
400
518
  const existing = await getWorkspaceProject(activeProjectId, nameSpace);
401
- if (!existing) {
402
- setActiveProjectId(null);
403
- setActiveProjectName(null);
404
- syncedSnapshotRef.current = null;
405
- await refreshProjects();
406
- throw new Error(t("panel.messages.workspaceNotFound"));
407
- }
408
- const name = resolveProjectName() || existing.name;
519
+ const name = resolveProjectName() || existing?.name || activeProjectName || "";
409
520
  const record = await persistProject({
410
521
  id: activeProjectId,
411
522
  name,
412
- createdAt: existing.createdAt,
523
+ createdAt: existing?.createdAt ?? activeCreatedAtRef.current ?? undefined,
524
+ });
525
+ emitAbuilderEvent(AbuilderEvents.workspaceSync, {
526
+ ...record,
527
+ nameSpace: eventNameSpace,
413
528
  });
414
- emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
415
529
  return name;
416
530
  });
417
- }, [activeProjectId, nameSpace, persistProject, refreshProjects, resolveProjectName, t]);
531
+ }, [
532
+ activeProjectId,
533
+ activeProjectName,
534
+ eventNameSpace,
535
+ nameSpace,
536
+ persistProject,
537
+ resolveProjectName,
538
+ t,
539
+ ]);
418
540
 
419
541
  const handleDeleteProject = useCallback(
420
542
  async (id: string) => {
@@ -424,6 +546,7 @@ export function useWorkspaceProjects({
424
546
  if (activeProjectId === id) {
425
547
  setActiveProjectId(null);
426
548
  setActiveProjectName(null);
549
+ activeCreatedAtRef.current = null;
427
550
  syncedSnapshotRef.current = null;
428
551
  }
429
552
  previewWindowsRef.current.delete(id);
@@ -478,6 +601,58 @@ export function useWorkspaceProjects({
478
601
  [activeProjectId, dirty, handleSyncProject, nameSpace, refreshPreviewing, t]
479
602
  );
480
603
 
604
+ useEffect(() => {
605
+ if (!bootstrappedRef.current) {
606
+ bootstrappedRef.current = true;
607
+ if (hasWorkspacePayload(initialWorkspace)) {
608
+ applyProjectRecord(initialWorkspace, {
609
+ notifyApplied: false,
610
+ persistToIndexedDb: true,
611
+ });
612
+ initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
613
+ if (initialWorkspace.id) {
614
+ workspaceIdAppliedRef.current = initialWorkspace.id;
615
+ }
616
+ } else if (typeof workspaceId === "string" && workspaceId.trim()) {
617
+ workspaceIdAppliedRef.current = workspaceId.trim();
618
+ void handleOpenProject(workspaceId.trim());
619
+ } else {
620
+ applyEmptyWorkspace();
621
+ }
622
+ return;
623
+ }
624
+ if (!hasWorkspacePayload(initialWorkspace)) return;
625
+ const key = workspaceApplyKey(initialWorkspace);
626
+ if (initialWorkspaceAppliedKeyRef.current === key) return;
627
+ initialWorkspaceAppliedKeyRef.current = key;
628
+ if (initialWorkspace.id) {
629
+ workspaceIdAppliedRef.current = initialWorkspace.id;
630
+ }
631
+ applyProjectRecord(initialWorkspace, {
632
+ notifyApplied: false,
633
+ persistToIndexedDb: true,
634
+ });
635
+ }, [
636
+ applyEmptyWorkspace,
637
+ applyProjectRecord,
638
+ handleOpenProject,
639
+ initialWorkspace,
640
+ workspaceId,
641
+ ]);
642
+
643
+ useEffect(() => {
644
+ if (!bootstrappedRef.current) return;
645
+ const id = typeof workspaceId === "string" ? workspaceId.trim() : "";
646
+ if (!id) return;
647
+ if (workspaceIdAppliedRef.current === id) return;
648
+ if (activeProjectId === id) {
649
+ workspaceIdAppliedRef.current = id;
650
+ return;
651
+ }
652
+ workspaceIdAppliedRef.current = id;
653
+ void handleOpenProject(id);
654
+ }, [activeProjectId, handleOpenProject, workspaceId]);
655
+
481
656
  return {
482
657
  projects,
483
658
  activeProjectId,
@@ -490,6 +665,7 @@ export function useWorkspaceProjects({
490
665
  handleSyncProject,
491
666
  handleDeleteProject,
492
667
  handlePreviewProject,
668
+ getCurrentWorkspaceRecord,
493
669
  refreshProjects,
494
670
  };
495
671
  }
@@ -158,3 +158,120 @@ export function IconImage() {
158
158
  </svg>
159
159
  );
160
160
  }
161
+
162
+ export function IconFolder() {
163
+ return (
164
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
165
+ <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
166
+ </svg>
167
+ );
168
+ }
169
+
170
+ export function IconGrid() {
171
+ return (
172
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
173
+ <rect x="3" y="3" width="7" height="7" rx="1" />
174
+ <rect x="14" y="3" width="7" height="7" rx="1" />
175
+ <rect x="3" y="14" width="7" height="7" rx="1" />
176
+ <rect x="14" y="14" width="7" height="7" rx="1" />
177
+ </svg>
178
+ );
179
+ }
180
+
181
+ export function IconTree() {
182
+ return (
183
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
184
+ <path d="M12 4v8" />
185
+ <path d="M6 20v-4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v4" />
186
+ <circle cx="12" cy="4" r="2" />
187
+ <circle cx="6" cy="20" r="2" />
188
+ <circle cx="18" cy="20" r="2" />
189
+ </svg>
190
+ );
191
+ }
192
+
193
+ export function IconSearch() {
194
+ return (
195
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
196
+ <circle cx="11" cy="11" r="7" />
197
+ <path d="m20 20-3.5-3.5" />
198
+ </svg>
199
+ );
200
+ }
201
+
202
+ export function IconStar({ filled }: { filled?: boolean }) {
203
+ return (
204
+ <svg
205
+ viewBox="0 0 24 24"
206
+ className="h-3.5 w-3.5"
207
+ fill={filled ? "currentColor" : "none"}
208
+ stroke="currentColor"
209
+ strokeWidth="2"
210
+ >
211
+ <path d="m12 3 2.7 5.5 6 .9-4.4 4.3 1 6L12 16.8 6.7 19.7l1-6L3.3 9.4l6-.9L12 3Z" />
212
+ </svg>
213
+ );
214
+ }
215
+
216
+ export function IconEyeOff() {
217
+ return (
218
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
219
+ <path d="M3 3l18 18" />
220
+ <path d="M10.6 10.6a2 2 0 0 0 2.8 2.8" />
221
+ <path d="M9.9 5.1A10.6 10.6 0 0 1 12 5c5 0 9.3 3.1 11 7-.5 1.2-1.2 2.3-2.1 3.2" />
222
+ <path d="M6.1 6.1C4.2 7.3 2.7 9 1.9 11c1.7 3.9 6 7 10.1 7 1.4 0 2.7-.3 3.9-.7" />
223
+ </svg>
224
+ );
225
+ }
226
+
227
+ /** 仅看引用子树 */
228
+ export function IconRefFilter({ active }: { active?: boolean }) {
229
+ return (
230
+ <svg
231
+ viewBox="0 0 24 24"
232
+ className="h-3.5 w-3.5"
233
+ fill="none"
234
+ stroke="currentColor"
235
+ strokeWidth={active ? 2.2 : 2}
236
+ >
237
+ <path d="M4 5h16l-5.5 6.5V18l-5 2.5v-9L4 5Z" fill={active ? "currentColor" : "none"} fillOpacity={active ? 0.2 : 0} />
238
+ <circle cx="16.5" cy="16.5" r="2.2" />
239
+ <path d="M14.8 15.2 12 12.5" />
240
+ </svg>
241
+ );
242
+ }
243
+
244
+ export function IconBlueprint({ active }: { active?: boolean }) {
245
+ return (
246
+ <svg
247
+ viewBox="0 0 24 24"
248
+ className="h-3.5 w-3.5"
249
+ fill="none"
250
+ stroke="currentColor"
251
+ strokeWidth={active ? 2.2 : 2}
252
+ >
253
+ <rect x="3" y="4" width="18" height="16" rx="2" />
254
+ <circle cx="8" cy="10" r="1.5" fill={active ? "currentColor" : "none"} />
255
+ <circle cx="16" cy="10" r="1.5" fill={active ? "currentColor" : "none"} />
256
+ <circle cx="12" cy="16" r="1.5" fill={active ? "currentColor" : "none"} />
257
+ <path d="M9.2 10.8 10.8 14.2M14.8 10.8 13.2 14.2" />
258
+ </svg>
259
+ );
260
+ }
261
+
262
+ export function IconSun() {
263
+ return (
264
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
265
+ <circle cx="12" cy="12" r="4" />
266
+ <path d="M12 2v2.5M12 19.5V22M4.93 4.93l1.77 1.77M17.3 17.3l1.77 1.77M2 12h2.5M19.5 12H22M4.93 19.07 6.7 17.3M17.3 6.7l1.77-1.77" />
267
+ </svg>
268
+ );
269
+ }
270
+
271
+ export function IconMoon() {
272
+ return (
273
+ <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
274
+ <path d="M21 12.79A9 9 0 1 1 11.21 3c0 .66.08 1.3.22 1.92A7 7 0 0 0 19.08 12c.62.14 1.26.22 1.92.22Z" />
275
+ </svg>
276
+ );
277
+ }
@@ -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/react-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/react-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/react-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