@arronqzy/vue-blueprint 0.1.13 → 0.1.16

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.
@@ -0,0 +1,397 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import { useI18n } from "@arronqzy/i18n/vue";
4
+ import { Button, Checkbox, Input, Modal } from "ant-design-vue";
5
+ import {
6
+ downloadApiCollectionTemplate,
7
+ getApiCollectionTemplateJson,
8
+ type ApiCollectionRecord,
9
+ type ApiCollectionSyncResult,
10
+ } from "@arronqzy/blueprint-dsl";
11
+
12
+ import {
13
+ getApiCollectionRecords,
14
+ removeApiCollection,
15
+ subscribeApiCollections,
16
+ upsertApiCollectionFromJson,
17
+ } from "../library/api-collection-store";
18
+
19
+ export type ApiCollectionUploadStartInfo = {
20
+ fileNames: string[];
21
+ count: number;
22
+ };
23
+
24
+ export type ApiCollectionUploadSuccessInfo = {
25
+ collections: Array<{
26
+ id: string;
27
+ name: string;
28
+ apiCount: number;
29
+ persistLocal: boolean;
30
+ }>;
31
+ };
32
+
33
+ export type ApiCollectionManagerDialogProps = {
34
+ open: boolean;
35
+ onOpenChange: (open: boolean) => void;
36
+ onUploadStart?: (info: ApiCollectionUploadStartInfo) => void;
37
+ onUploadSuccess?: (info: ApiCollectionUploadSuccessInfo) => void;
38
+ onSyncCollection?: (
39
+ record: ApiCollectionRecord
40
+ ) => ApiCollectionSyncResult | Promise<ApiCollectionSyncResult>;
41
+ };
42
+
43
+ const props = defineProps<ApiCollectionManagerDialogProps>();
44
+
45
+ const emit = defineEmits<{
46
+ uploadStart: [info: ApiCollectionUploadStartInfo];
47
+ uploadSuccess: [info: ApiCollectionUploadSuccessInfo];
48
+ }>();
49
+
50
+ const { t } = useI18n();
51
+
52
+ function formatSyncResultMessage(result: ApiCollectionSyncResult): string {
53
+ if (result.updatedCount <= 0) {
54
+ return t("blueprint.apiCollection.syncNone");
55
+ }
56
+ const names = result.nodes.map((node) => node.label).join("、");
57
+ return t("blueprint.apiCollection.syncSuccessDetail", {
58
+ count: result.updatedCount,
59
+ names,
60
+ });
61
+ }
62
+ const fileInputRef = ref<HTMLInputElement | null>(null);
63
+ const records = ref<ApiCollectionRecord[]>([...getApiCollectionRecords()]);
64
+ const persistLocalDefault = ref(true);
65
+ const error = ref<string | null>(null);
66
+ const busy = ref(false);
67
+
68
+ const editorOpen = ref(false);
69
+ const editingId = ref<string | null>(null);
70
+ const editorJson = ref("");
71
+ const editorPersistLocal = ref(true);
72
+ const editorError = ref<string | null>(null);
73
+
74
+ watch(
75
+ () => props.open,
76
+ (open, _prev, onCleanup) => {
77
+ if (!open) return;
78
+ records.value = [...getApiCollectionRecords()];
79
+ const unsubscribe = subscribeApiCollections(() => {
80
+ records.value = [...getApiCollectionRecords()];
81
+ });
82
+ onCleanup(unsubscribe);
83
+ }
84
+ );
85
+
86
+ const sorted = computed(() =>
87
+ [...records.value].sort((a, b) => b.updatedAt - a.updatedAt)
88
+ );
89
+
90
+ function notifyUploadStart(info: ApiCollectionUploadStartInfo) {
91
+ props.onUploadStart?.(info);
92
+ emit("uploadStart", info);
93
+ }
94
+
95
+ function notifyUploadSuccess(info: ApiCollectionUploadSuccessInfo) {
96
+ props.onUploadSuccess?.(info);
97
+ emit("uploadSuccess", info);
98
+ }
99
+
100
+ function openCreateEditor() {
101
+ editingId.value = null;
102
+ editorJson.value = getApiCollectionTemplateJson();
103
+ editorPersistLocal.value = persistLocalDefault.value;
104
+ editorError.value = null;
105
+ editorOpen.value = true;
106
+ }
107
+
108
+ function openEditEditor(record: ApiCollectionRecord) {
109
+ editingId.value = record.id;
110
+ editorJson.value = record.rawJson;
111
+ editorPersistLocal.value = record.persistLocal;
112
+ editorError.value = null;
113
+ editorOpen.value = true;
114
+ }
115
+
116
+ async function handleSaveEditor() {
117
+ busy.value = true;
118
+ editorError.value = null;
119
+ try {
120
+ const result = await upsertApiCollectionFromJson({
121
+ id: editingId.value ?? undefined,
122
+ rawJson: editorJson.value,
123
+ persistLocal: editorPersistLocal.value,
124
+ });
125
+ if (!result.ok) {
126
+ editorError.value = result.error;
127
+ return;
128
+ }
129
+ editorOpen.value = false;
130
+ } finally {
131
+ busy.value = false;
132
+ }
133
+ }
134
+
135
+ async function handleDelete(id: string) {
136
+ const ok = window.confirm(t("blueprint.apiCollection.confirmDelete"));
137
+ if (!ok) return;
138
+ busy.value = true;
139
+ error.value = null;
140
+ try {
141
+ await removeApiCollection(id);
142
+ } catch (err) {
143
+ error.value = err instanceof Error ? err.message : String(err);
144
+ } finally {
145
+ busy.value = false;
146
+ }
147
+ }
148
+
149
+ async function handleSync(record: ApiCollectionRecord) {
150
+ if (!props.onSyncCollection) {
151
+ error.value = t("blueprint.apiCollection.syncUnavailable");
152
+ return;
153
+ }
154
+ busy.value = true;
155
+ error.value = null;
156
+ try {
157
+ const result = await props.onSyncCollection(record);
158
+ window.alert(formatSyncResultMessage(result));
159
+ } catch (err) {
160
+ error.value = err instanceof Error ? err.message : String(err);
161
+ } finally {
162
+ busy.value = false;
163
+ }
164
+ }
165
+
166
+ async function handleSyncFromEditor() {
167
+ busy.value = true;
168
+ editorError.value = null;
169
+ try {
170
+ const result = await upsertApiCollectionFromJson({
171
+ id: editingId.value ?? undefined,
172
+ rawJson: editorJson.value,
173
+ persistLocal: editorPersistLocal.value,
174
+ });
175
+ if (!result.ok) {
176
+ editorError.value = result.error;
177
+ return;
178
+ }
179
+ editingId.value = result.record.id;
180
+ if (!props.onSyncCollection) {
181
+ editorError.value = t("blueprint.apiCollection.syncUnavailable");
182
+ return;
183
+ }
184
+ const syncResult = await props.onSyncCollection(result.record);
185
+ window.alert(formatSyncResultMessage(syncResult));
186
+ } catch (err) {
187
+ editorError.value = err instanceof Error ? err.message : String(err);
188
+ } finally {
189
+ busy.value = false;
190
+ }
191
+ }
192
+
193
+ async function handleFiles(files: FileList | null) {
194
+ if (!files || files.length === 0) return;
195
+ const list = Array.from(files);
196
+ busy.value = true;
197
+ error.value = null;
198
+ notifyUploadStart({
199
+ fileNames: list.map((file) => file.name),
200
+ count: list.length,
201
+ });
202
+
203
+ const uploaded: ApiCollectionUploadSuccessInfo["collections"] = [];
204
+ const errors: string[] = [];
205
+
206
+ try {
207
+ for (const file of list) {
208
+ const text = await file.text();
209
+ const result = await upsertApiCollectionFromJson({
210
+ rawJson: text,
211
+ persistLocal: persistLocalDefault.value,
212
+ fallbackName: file.name,
213
+ });
214
+ if (!result.ok) {
215
+ errors.push(`${file.name}: ${result.error}`);
216
+ continue;
217
+ }
218
+ uploaded.push({
219
+ id: result.record.id,
220
+ name: result.record.name,
221
+ apiCount: result.record.document.apis.length,
222
+ persistLocal: result.record.persistLocal,
223
+ });
224
+ }
225
+ if (uploaded.length > 0) {
226
+ notifyUploadSuccess({ collections: uploaded });
227
+ }
228
+ if (errors.length > 0) {
229
+ error.value = errors.join("\n");
230
+ }
231
+ } catch (err) {
232
+ error.value = err instanceof Error ? err.message : String(err);
233
+ } finally {
234
+ busy.value = false;
235
+ if (fileInputRef.value) fileInputRef.value.value = "";
236
+ }
237
+ }
238
+ </script>
239
+
240
+ <template>
241
+ <Modal
242
+ :open="open"
243
+ :title="t('blueprint.apiCollection.managerTitle')"
244
+ :footer="null"
245
+ width="960px"
246
+ :body-style="{ maxHeight: '90vh', display: 'flex', flexDirection: 'column' }"
247
+ @update:open="onOpenChange"
248
+ >
249
+ <p class="mb-3 text-xs text-muted-foreground">
250
+ {{ t("blueprint.apiCollection.managerDescription") }}
251
+ </p>
252
+
253
+ <div class="mb-2 flex flex-wrap items-center gap-2 border-b border-border/60 pb-3">
254
+ <Button size="small" type="primary" :disabled="busy" @click="fileInputRef?.click()">
255
+ {{ t("blueprint.apiCollection.upload") }}
256
+ </Button>
257
+ <Button size="small" :disabled="busy" @click="openCreateEditor">
258
+ {{ t("blueprint.apiCollection.create") }}
259
+ </Button>
260
+ <Button size="small" @click="downloadApiCollectionTemplate()">
261
+ {{ t("blueprint.apiCollection.downloadTemplate") }}
262
+ </Button>
263
+ <label class="ml-auto flex cursor-pointer items-center gap-2 text-[11px] text-muted-foreground">
264
+ <Checkbox v-model:checked="persistLocalDefault" />
265
+ {{ t("blueprint.apiCollection.persistLocal") }}
266
+ </label>
267
+ <input
268
+ ref="fileInputRef"
269
+ type="file"
270
+ accept="application/json,.json"
271
+ multiple
272
+ class="hidden"
273
+ @change="(e) => void handleFiles((e.target as HTMLInputElement).files)"
274
+ />
275
+ </div>
276
+
277
+ <p class="mb-2 text-[11px] text-muted-foreground">
278
+ {{ t("blueprint.apiCollection.persistLocalHint") }}
279
+ </p>
280
+
281
+ <pre
282
+ v-if="error"
283
+ class="mb-2 max-h-24 overflow-auto whitespace-pre-wrap rounded border border-destructive/40 bg-destructive/5 p-2 text-[11px] text-destructive"
284
+ >{{ error }}</pre>
285
+
286
+ <div class="min-h-0 flex-1 space-y-2 overflow-auto py-1">
287
+ <p
288
+ v-if="sorted.length === 0"
289
+ class="py-8 text-center text-xs text-muted-foreground"
290
+ >
291
+ {{ t("blueprint.apiCollection.empty") }}
292
+ </p>
293
+ <div
294
+ v-for="record in sorted"
295
+ :key="record.id"
296
+ class="flex items-start gap-2 rounded-md border border-border/70 bg-muted/20 px-2.5 py-2"
297
+ >
298
+ <div class="min-w-0 flex-1">
299
+ <div class="truncate text-xs font-medium text-foreground">
300
+ {{ record.name }}
301
+ </div>
302
+ <div class="mt-0.5 flex flex-wrap gap-x-2 text-[10px] text-muted-foreground">
303
+ <span>
304
+ {{
305
+ t("blueprint.apiCollection.apiCount", {
306
+ count: record.document.apis.length,
307
+ })
308
+ }}
309
+ </span>
310
+ <span>
311
+ {{
312
+ record.persistLocal
313
+ ? t("blueprint.apiCollection.persisted")
314
+ : t("blueprint.apiCollection.memoryOnly")
315
+ }}
316
+ </span>
317
+ </div>
318
+ </div>
319
+ <Button
320
+ size="small"
321
+ :disabled="busy || !onSyncCollection"
322
+ :title="t('blueprint.apiCollection.syncHint')"
323
+ @click="void handleSync(record)"
324
+ >
325
+ {{ t("blueprint.apiCollection.sync") }}
326
+ </Button>
327
+ <Button size="small" :disabled="busy" @click="openEditEditor(record)">
328
+ {{ t("common.edit") }}
329
+ </Button>
330
+ <Button
331
+ size="small"
332
+ danger
333
+ :disabled="busy"
334
+ @click="void handleDelete(record.id)"
335
+ >
336
+ {{ t("common.delete") }}
337
+ </Button>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="mt-4 flex justify-end">
342
+ <Button @click="onOpenChange(false)">{{ t("common.close") }}</Button>
343
+ </div>
344
+ </Modal>
345
+
346
+ <Modal
347
+ :open="editorOpen"
348
+ :title="
349
+ editingId
350
+ ? t('blueprint.apiCollection.editTitle')
351
+ : t('blueprint.apiCollection.createTitle')
352
+ "
353
+ :footer="null"
354
+ width="1100px"
355
+ :style="{ top: '4vh' }"
356
+ @update:open="(v) => (editorOpen = v)"
357
+ >
358
+ <p class="mb-3 text-xs text-muted-foreground">
359
+ {{ t("blueprint.apiCollection.editorDescription") }}
360
+ </p>
361
+
362
+ <div class="space-y-2">
363
+ <div class="flex items-center justify-between gap-2">
364
+ <span class="text-xs">{{ t("blueprint.apiCollection.jsonEditor") }}</span>
365
+ <label class="flex cursor-pointer items-center gap-2 text-[11px] text-muted-foreground">
366
+ <Checkbox v-model:checked="editorPersistLocal" />
367
+ {{ t("blueprint.apiCollection.persistLocal") }}
368
+ </label>
369
+ </div>
370
+ <Input.TextArea
371
+ v-model:value="editorJson"
372
+ :rows="28"
373
+ :spellcheck="false"
374
+ class="!min-h-[62vh] font-mono text-[12px] leading-relaxed"
375
+ :status="editorError ? 'error' : undefined"
376
+ />
377
+ <p v-if="editorError" class="text-[11px] text-destructive">{{ editorError }}</p>
378
+ </div>
379
+
380
+ <div class="mt-4 flex flex-wrap justify-end gap-2">
381
+ <Button @click="downloadApiCollectionTemplate()">
382
+ {{ t("blueprint.apiCollection.downloadTemplate") }}
383
+ </Button>
384
+ <Button
385
+ :disabled="busy || !onSyncCollection"
386
+ :title="t('blueprint.apiCollection.syncHint')"
387
+ @click="void handleSyncFromEditor()"
388
+ >
389
+ {{ t("blueprint.apiCollection.sync") }}
390
+ </Button>
391
+ <Button @click="editorOpen = false">{{ t("common.cancel") }}</Button>
392
+ <Button type="primary" :disabled="busy" @click="void handleSaveEditor()">
393
+ {{ t("common.save") }}
394
+ </Button>
395
+ </div>
396
+ </Modal>
397
+ </template>
@@ -8,6 +8,7 @@ import {
8
8
  type NodeMouseEvent,
9
9
  type EdgeMouseEvent,
10
10
  } from "@vue-flow/core";
11
+ import { Background } from "@vue-flow/background";
11
12
  import { computed, ref, toRef, watch } from "vue";
12
13
  import "@vue-flow/core/dist/style.css";
13
14
  import "@vue-flow/core/dist/theme-default.css";
@@ -210,7 +211,15 @@ function handleDeleteEdge(edgeId: string) {
210
211
  @edge-context-menu="onEdgeContextMenu"
211
212
  @edge-click="onEdgeClick"
212
213
  @pane-click="onPaneClick"
213
- />
214
+ >
215
+ <Background
216
+ id="bp-dots"
217
+ variant="dots"
218
+ :gap="18"
219
+ :size="1.25"
220
+ color="rgba(148, 163, 184, 0.45)"
221
+ />
222
+ </VueFlow>
214
223
  <BlueprintContextMenu
215
224
  :menu="menu"
216
225
  @close="menu = null"