@arronqzy/vue-blueprint 0.1.0

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.
Files changed (57) hide show
  1. package/README.md +50 -0
  2. package/package.json +44 -0
  3. package/src/BlueprintCanvasContext.ts +71 -0
  4. package/src/BlueprintNodeConfigSidebar.vue +338 -0
  5. package/src/blueprint.css +327 -0
  6. package/src/blueprintNodeTypes.ts +20 -0
  7. package/src/components/BluePrintVueRoot.vue +73 -0
  8. package/src/components/BlueprintCanvas.vue +220 -0
  9. package/src/components/BlueprintContextMenu.vue +114 -0
  10. package/src/components/BlueprintExecutionLogPanel.vue +294 -0
  11. package/src/components/BlueprintMetaDialog.vue +80 -0
  12. package/src/components/BlueprintNodeSwitchTaskDialog.vue +41 -0
  13. package/src/components/ClockNodeConfigPanel.vue +124 -0
  14. package/src/components/FetchNodeConfigPanel.vue +559 -0
  15. package/src/components/FetchUrlAutocomplete.vue +174 -0
  16. package/src/components/JsonNodeConfigPanel.vue +73 -0
  17. package/src/components/LogicNodeConfigPanel.vue +73 -0
  18. package/src/components/ViewElementMultiSelect.vue +50 -0
  19. package/src/composables/useBlueprintDebugSession.ts +441 -0
  20. package/src/composables/useBlueprintFlowState.ts +486 -0
  21. package/src/composables/useBlueprintFlowViewport.ts +65 -0
  22. package/src/composables/useBlueprintNodeSelectionGuard.ts +41 -0
  23. package/src/composables/useBlueprintPageLifecycle.ts +244 -0
  24. package/src/createBlueprintEdgeTypes.ts +10 -0
  25. package/src/edges/BlueprintSmoothEdge.vue +31 -0
  26. package/src/env.d.ts +7 -0
  27. package/src/fetch-config-task-store.ts +206 -0
  28. package/src/flowCoordinates.ts +19 -0
  29. package/src/flowDefaults.ts +9 -0
  30. package/src/graph/blueprint-graph.ts +265 -0
  31. package/src/graph/document.ts +422 -0
  32. package/src/graph/index.ts +7 -0
  33. package/src/graph/node-summary.ts +88 -0
  34. package/src/graph/node-types.ts +9 -0
  35. package/src/graph/sync-edges.ts +69 -0
  36. package/src/graph/sync-nodes.ts +110 -0
  37. package/src/graph/vue-flow-adapter.ts +127 -0
  38. package/src/index.ts +37 -0
  39. package/src/library/blueprint-io.ts +108 -0
  40. package/src/library/blueprint-library-db.ts +112 -0
  41. package/src/library/execution-log-db.ts +171 -0
  42. package/src/library/execution-log-settings.ts +50 -0
  43. package/src/library/swagger-docs.ts +56 -0
  44. package/src/library/types.ts +35 -0
  45. package/src/nodes/AndFlowNode.vue +60 -0
  46. package/src/nodes/BlueprintFlowNode.vue +26 -0
  47. package/src/nodes/BlueprintNodeCard.vue +155 -0
  48. package/src/nodes/BlueprintNodeShell.vue +70 -0
  49. package/src/nodes/ClockFlowNode.vue +60 -0
  50. package/src/nodes/FetchFlowNode.vue +26 -0
  51. package/src/nodes/JsonFlowNode.vue +26 -0
  52. package/src/nodes/LifecycleFlowNode.vue +45 -0
  53. package/src/nodes/LogicFlowNode.vue +26 -0
  54. package/src/runtime/document-to-runnable-graph.ts +51 -0
  55. package/src/runtime/execution-overlay.ts +169 -0
  56. package/src/types.ts +1 -0
  57. package/src/utils/cn.ts +3 -0
@@ -0,0 +1,559 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { Button, Input, Select } from "ant-design-vue";
4
+ import type {
5
+ FetchHttpMethod,
6
+ FetchRequestConfig,
7
+ FetchResponseType,
8
+ } from "@arronqzy/blueprint-dsl";
9
+ import {
10
+ FETCH_CACHES,
11
+ FETCH_CREDENTIALS,
12
+ FETCH_HTTP_METHODS,
13
+ FETCH_MODES,
14
+ FETCH_NODE_TYPE,
15
+ FETCH_REDIRECTS,
16
+ FETCH_RESPONSE_TYPES,
17
+ } from "@arronqzy/blueprint-dsl";
18
+
19
+ import type { BlueprintGraphNode } from "../graph/document";
20
+ import { resolveNodeFetchConfig } from "../graph/document";
21
+ import {
22
+ cancelFetchDebugTask,
23
+ cancelSwaggerLoadTask,
24
+ startFetchDebugTask,
25
+ startSwaggerLoadTask,
26
+ useFetchDebugTask,
27
+ useSwaggerLoadTask,
28
+ } from "../fetch-config-task-store";
29
+ import FetchUrlAutocomplete from "./FetchUrlAutocomplete.vue";
30
+
31
+ export type FetchNodeConfigPanelProps = {
32
+ node: BlueprintGraphNode;
33
+ onUpdateNode: (
34
+ nodeId: string,
35
+ patch: Partial<Pick<BlueprintGraphNode, "fetchConfig" | "configSource">>
36
+ ) => void;
37
+ };
38
+
39
+ const props = defineProps<FetchNodeConfigPanelProps>();
40
+
41
+ const validationError = ref<string | null>(null);
42
+ const fetchValidationError = ref<string | null>(null);
43
+ const debugExpanded = ref(false);
44
+
45
+ const fetchConfig = computed(() => resolveNodeFetchConfig(props.node));
46
+ const endpoints = computed(() => fetchConfig.value.swaggerEndpoints ?? []);
47
+ const hasSwaggerEndpoints = computed(() => endpoints.value.length > 0);
48
+ const urlInputMode = computed(
49
+ () => fetchConfig.value.urlInputMode ?? (hasSwaggerEndpoints.value ? "swagger" : "manual")
50
+ );
51
+
52
+ const swaggerTask = useSwaggerLoadTask(props.node.id);
53
+ const fetchDebugTask = useFetchDebugTask(props.node.id);
54
+ const loadingSwagger = computed(() => swaggerTask.value.status === "loading");
55
+ const loadingFetchDebug = computed(() => fetchDebugTask.value.status === "loading");
56
+ const swaggerTaskError = computed(() =>
57
+ swaggerTask.value.status === "error" ? swaggerTask.value.error : null
58
+ );
59
+ const swaggerError = computed(() => validationError.value ?? swaggerTaskError.value);
60
+
61
+ function patchFetchConfig(node: BlueprintGraphNode, patch: Partial<FetchRequestConfig>) {
62
+ return {
63
+ role: "fetch" as const,
64
+ nodeType: FETCH_NODE_TYPE,
65
+ configSource: "fetch" as const,
66
+ fetchConfig: { ...resolveNodeFetchConfig(node), ...patch },
67
+ };
68
+ }
69
+
70
+ function setUrlInputMode(mode: "swagger" | "manual") {
71
+ props.onUpdateNode(props.node.id, patchFetchConfig(props.node, { urlInputMode: mode }));
72
+ }
73
+
74
+ function handleLoadSwagger() {
75
+ const docsUrl = fetchConfig.value.swaggerDocsUrl?.trim();
76
+ if (!docsUrl) {
77
+ validationError.value = "请先填写 Swagger 文档 URL";
78
+ return;
79
+ }
80
+
81
+ validationError.value = null;
82
+ startSwaggerLoadTask({
83
+ nodeId: props.node.id,
84
+ docsUrl,
85
+ onSuccess: (parsed, url) => {
86
+ props.onUpdateNode(
87
+ props.node.id,
88
+ patchFetchConfig(props.node, {
89
+ swaggerDocsUrl: url,
90
+ apiBaseUrl: parsed.apiBaseUrl,
91
+ swaggerEndpoints: parsed.endpoints,
92
+ urlInputMode: "swagger",
93
+ })
94
+ );
95
+ },
96
+ });
97
+ }
98
+
99
+ function handleAbortSwagger() {
100
+ cancelSwaggerLoadTask(props.node.id);
101
+ validationError.value = null;
102
+ }
103
+
104
+ function handleSendFetchDebug() {
105
+ const url = fetchConfig.value.url?.trim();
106
+ if (!url) {
107
+ fetchValidationError.value = "请先填写请求 URL";
108
+ return;
109
+ }
110
+
111
+ fetchValidationError.value = null;
112
+ startFetchDebugTask({
113
+ nodeId: props.node.id,
114
+ config: fetchConfig.value,
115
+ });
116
+ }
117
+
118
+ function handleAbortFetchDebug() {
119
+ cancelFetchDebugTask(props.node.id);
120
+ fetchValidationError.value = null;
121
+ }
122
+
123
+ function formatFetchDebugData(data: unknown): string {
124
+ if (typeof data === "string") return data;
125
+ try {
126
+ return JSON.stringify(data, null, 2);
127
+ } catch {
128
+ return String(data);
129
+ }
130
+ }
131
+
132
+ const debugBodyText = computed(() => {
133
+ const task = fetchDebugTask.value;
134
+ if (task.status !== "success" || !task.result) return "";
135
+ return formatFetchDebugData(task.result.data);
136
+ });
137
+
138
+ const debugPreview = computed(() => {
139
+ const text = debugBodyText.value;
140
+ return text.length > 120 ? `${text.slice(0, 120).trimEnd()}…` : text;
141
+ });
142
+
143
+ const debugOk = computed(() => {
144
+ const result = fetchDebugTask.value.result;
145
+ return result ? result.status >= 200 && result.status < 300 : false;
146
+ });
147
+
148
+ function handleHeadersBlur(event: Event) {
149
+ try {
150
+ const headers = JSON.parse((event.target as HTMLTextAreaElement).value || "{}") as Record<
151
+ string,
152
+ string
153
+ >;
154
+ props.onUpdateNode(props.node.id, patchFetchConfig(props.node, { headers }));
155
+ } catch {
156
+ /* 保留上次有效值 */
157
+ }
158
+ }
159
+ </script>
160
+
161
+ <template>
162
+ <div class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
163
+ <div class="font-medium text-foreground">数据源获取 (Fetch)</div>
164
+ <p class="text-[11px] text-muted-foreground">
165
+ 收到<strong>真信号</strong>后发起 HTTP 请求;可导入 Swagger 文档后从接口列表联想选择
166
+ URL。
167
+ </p>
168
+
169
+ <label class="block space-y-1">
170
+ <span class="text-muted-foreground">Swagger 文档 URL(可选)</span>
171
+ <div class="flex gap-1.5">
172
+ <Input
173
+ size="small"
174
+ :value="fetchConfig.swaggerDocsUrl ?? ''"
175
+ :disabled="loadingSwagger"
176
+ placeholder="https://example.com/v3/api-docs"
177
+ class="flex-1 font-mono text-[11px]"
178
+ @update:value="
179
+ (v) =>
180
+ onUpdateNode(node.id, patchFetchConfig(node, { swaggerDocsUrl: String(v) }))
181
+ "
182
+ />
183
+ <Button
184
+ v-if="loadingSwagger"
185
+ size="small"
186
+ class="h-8 w-8 shrink-0 text-destructive"
187
+ title="中止解析"
188
+ aria-label="中止 Swagger 解析"
189
+ @click="handleAbortSwagger"
190
+ >
191
+
192
+ </Button>
193
+ <Button
194
+ v-else
195
+ size="small"
196
+ class="h-8 w-8 shrink-0"
197
+ title="解析 Swagger 文档"
198
+ @click="handleLoadSwagger"
199
+ >
200
+
201
+ </Button>
202
+ </div>
203
+ <p v-if="loadingSwagger" class="text-[11px] text-muted-foreground">
204
+ 正在解析 Swagger 文档,点击右侧按钮可中止…
205
+ </p>
206
+ <p v-if="swaggerError" class="text-[11px] text-destructive">{{ swaggerError }}</p>
207
+ <p v-if="endpoints.length > 0" class="text-[11px] text-muted-foreground">
208
+ 已解析 {{ endpoints.length }} 个接口
209
+ </p>
210
+ </label>
211
+
212
+ <label class="block space-y-1">
213
+ <div class="flex items-center justify-between gap-2">
214
+ <span class="text-muted-foreground">请求 URL</span>
215
+ <div v-if="hasSwaggerEndpoints" class="flex rounded-md border border-border p-0.5">
216
+ <button
217
+ type="button"
218
+ :disabled="loadingSwagger"
219
+ class="rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50"
220
+ :class="
221
+ urlInputMode === 'swagger'
222
+ ? 'bg-primary text-primary-foreground'
223
+ : 'text-muted-foreground hover:text-foreground'
224
+ "
225
+ @click="setUrlInputMode('swagger')"
226
+ >
227
+ 接口联想
228
+ </button>
229
+ <button
230
+ type="button"
231
+ :disabled="loadingSwagger"
232
+ class="rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50"
233
+ :class="
234
+ urlInputMode === 'manual'
235
+ ? 'bg-primary text-primary-foreground'
236
+ : 'text-muted-foreground hover:text-foreground'
237
+ "
238
+ @click="setUrlInputMode('manual')"
239
+ >
240
+ 手动输入
241
+ </button>
242
+ </div>
243
+ </div>
244
+
245
+ <div
246
+ v-if="urlInputMode === 'swagger' && hasSwaggerEndpoints"
247
+ class="space-y-2"
248
+ :class="loadingSwagger && 'pointer-events-none opacity-60'"
249
+ >
250
+ <label class="block space-y-1">
251
+ <span class="text-muted-foreground">API 主机 / Base URL</span>
252
+ <Input
253
+ size="small"
254
+ :value="fetchConfig.apiBaseUrl ?? ''"
255
+ placeholder="https://api.example.com/v1"
256
+ class="font-mono text-[11px]"
257
+ @update:value="
258
+ (v) => onUpdateNode(node.id, patchFetchConfig(node, { apiBaseUrl: String(v) }))
259
+ "
260
+ />
261
+ </label>
262
+ <label class="block space-y-1">
263
+ <span class="text-muted-foreground">选择接口</span>
264
+ <div class="flex gap-1.5">
265
+ <div class="min-w-0 flex-1">
266
+ <FetchUrlAutocomplete
267
+ :value="fetchConfig.url"
268
+ :api-base-url="fetchConfig.apiBaseUrl ?? ''"
269
+ :endpoints="endpoints"
270
+ select-only
271
+ placeholder="点击选择或搜索接口"
272
+ @change="
273
+ (url) => onUpdateNode(node.id, patchFetchConfig(node, { url }))
274
+ "
275
+ @select-endpoint="
276
+ (endpoint) =>
277
+ onUpdateNode(
278
+ node.id,
279
+ patchFetchConfig(node, {
280
+ url: endpoint.path,
281
+ method: endpoint.method,
282
+ })
283
+ )
284
+ "
285
+ />
286
+ </div>
287
+ <Button
288
+ v-if="loadingFetchDebug"
289
+ size="small"
290
+ class="h-8 w-8 shrink-0"
291
+ title="中止请求"
292
+ @click="handleAbortFetchDebug"
293
+ >
294
+
295
+ </Button>
296
+ <Button
297
+ v-else
298
+ size="small"
299
+ class="h-8 w-8 shrink-0"
300
+ :disabled="loadingSwagger"
301
+ title="发送调试请求"
302
+ @click="handleSendFetchDebug"
303
+ >
304
+
305
+ </Button>
306
+ </div>
307
+ </label>
308
+ </div>
309
+ <div v-else class="flex gap-1.5">
310
+ <Input
311
+ size="small"
312
+ :value="fetchConfig.url"
313
+ :disabled="loadingFetchDebug"
314
+ placeholder="https://api.example.com/data"
315
+ class="flex-1 font-mono text-[11px]"
316
+ @update:value="(v) => onUpdateNode(node.id, patchFetchConfig(node, { url: String(v) }))"
317
+ />
318
+ <Button
319
+ v-if="loadingFetchDebug"
320
+ size="small"
321
+ class="h-8 w-8 shrink-0"
322
+ title="中止请求"
323
+ @click="handleAbortFetchDebug"
324
+ >
325
+
326
+ </Button>
327
+ <Button
328
+ v-else
329
+ size="small"
330
+ class="h-8 w-8 shrink-0"
331
+ :disabled="loadingSwagger"
332
+ title="发送调试请求"
333
+ @click="handleSendFetchDebug"
334
+ >
335
+
336
+ </Button>
337
+ </div>
338
+
339
+ <p v-if="loadingFetchDebug" class="text-[11px] text-muted-foreground">
340
+ 正在发送调试请求,点击右侧按钮可中止…
341
+ </p>
342
+ <p v-if="fetchValidationError" class="text-[11px] text-destructive">
343
+ {{ fetchValidationError }}
344
+ </p>
345
+
346
+ <div
347
+ v-if="fetchDebugTask.status === 'error'"
348
+ class="rounded-md border border-destructive/40 bg-destructive/5 p-2"
349
+ >
350
+ <p class="text-[11px] text-destructive">
351
+ {{ fetchDebugTask.error ?? "请求失败" }}
352
+ </p>
353
+ </div>
354
+ <div
355
+ v-else-if="fetchDebugTask.status === 'success' && fetchDebugTask.result"
356
+ class="rounded-md border border-border/70 bg-background/80"
357
+ >
358
+ <button
359
+ type="button"
360
+ class="flex w-full items-center gap-2 px-2 py-1.5 text-left"
361
+ @click="debugExpanded = !debugExpanded"
362
+ >
363
+ <span
364
+ class="text-muted-foreground transition-transform"
365
+ :class="debugExpanded && 'rotate-180'"
366
+ >
367
+
368
+ </span>
369
+ <span class="text-[11px] font-medium text-foreground">调试响应</span>
370
+ <span
371
+ class="rounded px-1.5 py-0.5 font-mono text-[10px]"
372
+ :class="
373
+ debugOk
374
+ ? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300'
375
+ : 'bg-destructive/15 text-destructive'
376
+ "
377
+ >
378
+ {{ fetchDebugTask.result.status }} {{ fetchDebugTask.result.statusText }}
379
+ </span>
380
+ <span
381
+ v-if="!debugExpanded"
382
+ class="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground"
383
+ >
384
+ {{ debugPreview }}
385
+ </span>
386
+ </button>
387
+ <div v-if="debugExpanded" class="border-t border-border/60 px-2 py-2">
388
+ <div class="mb-1 truncate font-mono text-[10px] text-muted-foreground">
389
+ {{ fetchDebugTask.result.url }}
390
+ </div>
391
+ <pre
392
+ class="max-h-56 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground"
393
+ >{{ debugBodyText }}</pre>
394
+ </div>
395
+ </div>
396
+ </label>
397
+
398
+ <label class="block space-y-1">
399
+ <span class="text-muted-foreground">请求方法</span>
400
+ <Select
401
+ size="small"
402
+ class="w-full"
403
+ :value="fetchConfig.method ?? 'GET'"
404
+ @change="
405
+ (v) =>
406
+ onUpdateNode(
407
+ node.id,
408
+ patchFetchConfig(node, { method: v as FetchHttpMethod })
409
+ )
410
+ "
411
+ >
412
+ <Select.Option v-for="method in FETCH_HTTP_METHODS" :key="method" :value="method">
413
+ {{ method }}
414
+ </Select.Option>
415
+ </Select>
416
+ </label>
417
+
418
+ <label class="block space-y-1">
419
+ <span class="text-muted-foreground">请求头 (JSON)</span>
420
+ <textarea
421
+ :key="`${node.id}-headers-${JSON.stringify(fetchConfig.headers)}`"
422
+ :value="JSON.stringify(fetchConfig.headers ?? {}, null, 2)"
423
+ rows="4"
424
+ class="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
425
+ placeholder='{"Content-Type":"application/json"}'
426
+ @blur="handleHeadersBlur"
427
+ />
428
+ </label>
429
+
430
+ <label class="block space-y-1">
431
+ <span class="text-muted-foreground">请求体</span>
432
+ <textarea
433
+ :value="fetchConfig.body ?? ''"
434
+ rows="4"
435
+ class="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
436
+ placeholder='{"key":"value"}'
437
+ @input="
438
+ (e) =>
439
+ onUpdateNode(
440
+ node.id,
441
+ patchFetchConfig(node, { body: (e.target as HTMLTextAreaElement).value })
442
+ )
443
+ "
444
+ />
445
+ </label>
446
+
447
+ <div class="grid grid-cols-2 gap-2">
448
+ <label class="block space-y-1">
449
+ <span class="text-muted-foreground">Credentials</span>
450
+ <Select
451
+ size="small"
452
+ class="w-full"
453
+ :value="fetchConfig.credentials ?? 'same-origin'"
454
+ @change="
455
+ (v) =>
456
+ onUpdateNode(
457
+ node.id,
458
+ patchFetchConfig(node, { credentials: v as RequestCredentials })
459
+ )
460
+ "
461
+ >
462
+ <Select.Option v-for="item in FETCH_CREDENTIALS" :key="item" :value="item">
463
+ {{ item }}
464
+ </Select.Option>
465
+ </Select>
466
+ </label>
467
+ <label class="block space-y-1">
468
+ <span class="text-muted-foreground">Mode</span>
469
+ <Select
470
+ size="small"
471
+ class="w-full"
472
+ :value="fetchConfig.mode ?? 'cors'"
473
+ @change="
474
+ (v) =>
475
+ onUpdateNode(node.id, patchFetchConfig(node, { mode: v as RequestMode }))
476
+ "
477
+ >
478
+ <Select.Option v-for="item in FETCH_MODES" :key="item" :value="item">
479
+ {{ item }}
480
+ </Select.Option>
481
+ </Select>
482
+ </label>
483
+ <label class="block space-y-1">
484
+ <span class="text-muted-foreground">Cache</span>
485
+ <Select
486
+ size="small"
487
+ class="w-full"
488
+ :value="fetchConfig.cache ?? 'default'"
489
+ @change="
490
+ (v) =>
491
+ onUpdateNode(node.id, patchFetchConfig(node, { cache: v as RequestCache }))
492
+ "
493
+ >
494
+ <Select.Option v-for="item in FETCH_CACHES" :key="item" :value="item">
495
+ {{ item }}
496
+ </Select.Option>
497
+ </Select>
498
+ </label>
499
+ <label class="block space-y-1">
500
+ <span class="text-muted-foreground">Redirect</span>
501
+ <Select
502
+ size="small"
503
+ class="w-full"
504
+ :value="fetchConfig.redirect ?? 'follow'"
505
+ @change="
506
+ (v) =>
507
+ onUpdateNode(
508
+ node.id,
509
+ patchFetchConfig(node, { redirect: v as RequestRedirect })
510
+ )
511
+ "
512
+ >
513
+ <Select.Option v-for="item in FETCH_REDIRECTS" :key="item" :value="item">
514
+ {{ item }}
515
+ </Select.Option>
516
+ </Select>
517
+ </label>
518
+ </div>
519
+
520
+ <div class="grid grid-cols-2 gap-2">
521
+ <label class="block space-y-1">
522
+ <span class="text-muted-foreground">响应解析</span>
523
+ <Select
524
+ size="small"
525
+ class="w-full"
526
+ :value="fetchConfig.responseType ?? 'json'"
527
+ @change="
528
+ (v) =>
529
+ onUpdateNode(
530
+ node.id,
531
+ patchFetchConfig(node, { responseType: v as FetchResponseType })
532
+ )
533
+ "
534
+ >
535
+ <Select.Option v-for="item in FETCH_RESPONSE_TYPES" :key="item" :value="item">
536
+ {{ item }}
537
+ </Select.Option>
538
+ </Select>
539
+ </label>
540
+ <label class="block space-y-1">
541
+ <span class="text-muted-foreground">超时 (ms)</span>
542
+ <Input
543
+ type="number"
544
+ size="small"
545
+ :min="0"
546
+ :step="1000"
547
+ :value="fetchConfig.timeoutMs ?? 30000"
548
+ @update:value="
549
+ (v) =>
550
+ onUpdateNode(
551
+ node.id,
552
+ patchFetchConfig(node, { timeoutMs: Number(v) || 0 })
553
+ )
554
+ "
555
+ />
556
+ </label>
557
+ </div>
558
+ </div>
559
+ </template>