@arronqzy/react-blueprint 1.0.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 (124) hide show
  1. package/.eslintrc.cjs +5 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/assets/index-CneNgzWb.css +1 -0
  58. package/dist/assets/index-DsVhHV0-.js +226 -0
  59. package/dist/index.html +14 -0
  60. package/index.html +13 -0
  61. package/index.ts +70 -0
  62. package/package.json +43 -0
  63. package/postcss.config.cjs +6 -0
  64. package/src/BlueprintCanvasContext.tsx +61 -0
  65. package/src/BlueprintContextMenu.tsx +108 -0
  66. package/src/BlueprintNodeConfigSidebar.tsx +358 -0
  67. package/src/blueprint.css +321 -0
  68. package/src/blueprintNodeTypes.ts +20 -0
  69. package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
  70. package/src/components/BlueprintLibrarySelect.tsx +68 -0
  71. package/src/components/BlueprintMetaDialog.tsx +96 -0
  72. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
  73. package/src/components/BlueprintPanelToolbar.tsx +664 -0
  74. package/src/components/ClockNodeConfigPanel.tsx +139 -0
  75. package/src/components/FetchNodeConfigPanel.tsx +716 -0
  76. package/src/components/FetchUrlAutocomplete.tsx +190 -0
  77. package/src/components/JsonNodeConfigPanel.tsx +79 -0
  78. package/src/components/LogicNodeConfigPanel.tsx +80 -0
  79. package/src/components/ViewElementMultiSelect.tsx +140 -0
  80. package/src/createBlueprintEdgeTypes.tsx +10 -0
  81. package/src/dev-seed.ts +34 -0
  82. package/src/dev.css +38 -0
  83. package/src/edges/BlueprintSmoothEdge.tsx +35 -0
  84. package/src/fetch-config-task-store.ts +197 -0
  85. package/src/flowCoordinates.ts +19 -0
  86. package/src/flowDefaults.ts +9 -0
  87. package/src/graph/blueprint-graph.ts +261 -0
  88. package/src/graph/document.ts +384 -0
  89. package/src/graph/index.ts +7 -0
  90. package/src/graph/node-summary.ts +88 -0
  91. package/src/graph/node-types.ts +9 -0
  92. package/src/graph/react-flow-adapter.ts +127 -0
  93. package/src/graph/sync-edges.ts +69 -0
  94. package/src/graph/sync-nodes.ts +104 -0
  95. package/src/hooks/useBlueprintDebugSession.ts +515 -0
  96. package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
  97. package/src/hooks/useBlueprintFlowState.ts +486 -0
  98. package/src/hooks/useBlueprintFlowViewport.ts +54 -0
  99. package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
  100. package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
  101. package/src/index.tsx +271 -0
  102. package/src/library/blueprint-io.ts +108 -0
  103. package/src/library/blueprint-library-db.ts +112 -0
  104. package/src/library/execution-log-db.ts +171 -0
  105. package/src/library/execution-log-settings.ts +50 -0
  106. package/src/library/swagger-docs.ts +56 -0
  107. package/src/library/types.ts +35 -0
  108. package/src/main.tsx +43 -0
  109. package/src/nodes/AndFlowNode.tsx +59 -0
  110. package/src/nodes/BlueprintFlowNode.tsx +26 -0
  111. package/src/nodes/BlueprintNodeCard.tsx +169 -0
  112. package/src/nodes/BlueprintNodeShell.tsx +45 -0
  113. package/src/nodes/ClockFlowNode.tsx +67 -0
  114. package/src/nodes/FetchFlowNode.tsx +26 -0
  115. package/src/nodes/JsonFlowNode.tsx +26 -0
  116. package/src/nodes/LifecycleFlowNode.tsx +44 -0
  117. package/src/nodes/LogicFlowNode.tsx +26 -0
  118. package/src/runtime/document-to-runnable-graph.ts +48 -0
  119. package/src/runtime/execution-overlay.ts +169 -0
  120. package/src/types.ts +1 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/tailwind.config.ts +6 -0
  123. package/tsconfig.json +12 -0
  124. package/vite.config.ts +16 -0
@@ -0,0 +1,716 @@
1
+ import { useCallback, useState, type ChangeEvent } from "react";
2
+ import {
3
+ Button,
4
+ Input,
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ cn,
11
+ } from "@arronqzy/ui";
12
+ import type {
13
+ FetchHttpMethod,
14
+ FetchRequestConfig,
15
+ FetchResponseType,
16
+ } from "@arronqzy/blueprint-dsl";
17
+ import {
18
+ FETCH_CACHES,
19
+ FETCH_CREDENTIALS,
20
+ FETCH_HTTP_METHODS,
21
+ FETCH_MODES,
22
+ FETCH_REDIRECTS,
23
+ FETCH_RESPONSE_TYPES,
24
+ } from "@arronqzy/blueprint-dsl";
25
+
26
+ import type { BlueprintGraphNode } from "../graph/document";
27
+ import { resolveNodeFetchConfig } from "../graph/document";
28
+ import {
29
+ cancelSwaggerLoadTask,
30
+ cancelFetchDebugTask,
31
+ startFetchDebugTask,
32
+ startSwaggerLoadTask,
33
+ useFetchDebugTask,
34
+ useSwaggerLoadTask,
35
+ } from "../fetch-config-task-store";
36
+ import { FetchUrlAutocomplete } from "./FetchUrlAutocomplete";
37
+
38
+ function SendIcon({ className }: { className?: string }) {
39
+ return (
40
+ <svg
41
+ viewBox="0 0 24 24"
42
+ className={className}
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="2"
46
+ aria-hidden="true"
47
+ >
48
+ <path d="M22 2 11 13" />
49
+ <path d="M22 2 15 22l-4-9-9-4z" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ function StopIcon({ className }: { className?: string }) {
55
+ return (
56
+ <svg
57
+ viewBox="0 0 24 24"
58
+ className={className}
59
+ fill="currentColor"
60
+ aria-hidden="true"
61
+ >
62
+ <rect x="6" y="6" width="12" height="12" rx="1" />
63
+ </svg>
64
+ );
65
+ }
66
+
67
+ function ChevronIcon({ className, expanded }: { className?: string; expanded: boolean }) {
68
+ return (
69
+ <svg
70
+ viewBox="0 0 24 24"
71
+ className={cn("transition-transform", expanded && "rotate-180", className)}
72
+ fill="none"
73
+ stroke="currentColor"
74
+ strokeWidth="2"
75
+ aria-hidden="true"
76
+ >
77
+ <path d="m6 9 6 6 6-6" />
78
+ </svg>
79
+ );
80
+ }
81
+
82
+ function AsyncSendButton({
83
+ loading,
84
+ disabled,
85
+ onSend,
86
+ onAbort,
87
+ sendTitle,
88
+ abortTitle,
89
+ sendAriaLabel,
90
+ abortAriaLabel,
91
+ }: {
92
+ loading: boolean;
93
+ disabled?: boolean;
94
+ onSend: () => void;
95
+ onAbort: () => void;
96
+ sendTitle: string;
97
+ abortTitle: string;
98
+ sendAriaLabel: string;
99
+ abortAriaLabel: string;
100
+ }) {
101
+ if (loading) {
102
+ return (
103
+ <Button
104
+ type="button"
105
+ variant="outline"
106
+ size="icon"
107
+ className="h-8 w-8 shrink-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
108
+ title={abortTitle}
109
+ aria-label={abortAriaLabel}
110
+ onClick={onAbort}
111
+ >
112
+ <StopIcon className="h-3.5 w-3.5" />
113
+ </Button>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <Button
119
+ type="button"
120
+ variant="outline"
121
+ size="icon"
122
+ className="h-8 w-8 shrink-0"
123
+ title={sendTitle}
124
+ aria-label={sendAriaLabel}
125
+ disabled={disabled}
126
+ onClick={onSend}
127
+ >
128
+ <SendIcon className="h-4 w-4" />
129
+ </Button>
130
+ );
131
+ }
132
+
133
+ function formatFetchDebugData(data: unknown): string {
134
+ if (typeof data === "string") return data;
135
+ try {
136
+ return JSON.stringify(data, null, 2);
137
+ } catch {
138
+ return String(data);
139
+ }
140
+ }
141
+
142
+ function FetchDebugResponsePanel({
143
+ task,
144
+ }: {
145
+ task: ReturnType<typeof useFetchDebugTask>;
146
+ }) {
147
+ const [expanded, setExpanded] = useState(false);
148
+
149
+ if (task.status === "idle" || task.status === "loading") return null;
150
+
151
+ if (task.status === "error") {
152
+ return (
153
+ <div className="rounded-md border border-destructive/40 bg-destructive/5 p-2">
154
+ <p className="text-[11px] text-destructive">{task.error ?? "请求失败"}</p>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ const result = task.result;
160
+ if (!result) return null;
161
+
162
+ const bodyText = formatFetchDebugData(result.data);
163
+ const preview =
164
+ bodyText.length > 120 ? `${bodyText.slice(0, 120).trimEnd()}…` : bodyText;
165
+ const ok = result.status >= 200 && result.status < 300;
166
+
167
+ return (
168
+ <div className="rounded-md border border-border/70 bg-background/80">
169
+ <button
170
+ type="button"
171
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left"
172
+ onClick={() => setExpanded((value) => !value)}
173
+ >
174
+ <ChevronIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" expanded={expanded} />
175
+ <span className="text-[11px] font-medium text-foreground">调试响应</span>
176
+ <span
177
+ className={cn(
178
+ "rounded px-1.5 py-0.5 font-mono text-[10px]",
179
+ ok
180
+ ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"
181
+ : "bg-destructive/15 text-destructive"
182
+ )}
183
+ >
184
+ {result.status} {result.statusText}
185
+ </span>
186
+ {!expanded ? (
187
+ <span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
188
+ {preview}
189
+ </span>
190
+ ) : null}
191
+ </button>
192
+ {expanded ? (
193
+ <div className="border-t border-border/60 px-2 py-2">
194
+ <div className="mb-1 truncate font-mono text-[10px] text-muted-foreground">
195
+ {result.url}
196
+ </div>
197
+ <pre className="max-h-56 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground">
198
+ {bodyText}
199
+ </pre>
200
+ </div>
201
+ ) : null}
202
+ </div>
203
+ );
204
+ }
205
+
206
+ function LoaderIcon({ className }: { className?: string }) {
207
+ return (
208
+ <svg
209
+ viewBox="0 0 24 24"
210
+ className={cn("animate-spin", className)}
211
+ fill="none"
212
+ stroke="currentColor"
213
+ strokeWidth="2"
214
+ aria-hidden="true"
215
+ >
216
+ <path d="M12 2v4" />
217
+ <path d="m16.2 7.8 2.9-2.9" />
218
+ <path d="M18 12h4" />
219
+ <path d="m16.2 16.2 2.9 2.9" />
220
+ <path d="M12 18v4" />
221
+ <path d="m4.9 19.1 2.9-2.9" />
222
+ <path d="M2 12h4" />
223
+ <path d="m4.9 4.9 2.9 2.9" />
224
+ </svg>
225
+ );
226
+ }
227
+
228
+ export type FetchNodeConfigPanelProps = {
229
+ node: BlueprintGraphNode;
230
+ onUpdateNode: (
231
+ nodeId: string,
232
+ patch: Partial<Pick<BlueprintGraphNode, "fetchConfig" | "configSource">>
233
+ ) => void;
234
+ };
235
+
236
+ function patchFetchConfig(
237
+ node: BlueprintGraphNode,
238
+ patch: Partial<FetchRequestConfig>
239
+ ) {
240
+ return {
241
+ fetchConfig: { ...resolveNodeFetchConfig(node), ...patch },
242
+ configSource: "fetch" as const,
243
+ };
244
+ }
245
+
246
+ export function FetchNodeConfigPanel({
247
+ node,
248
+ onUpdateNode,
249
+ }: FetchNodeConfigPanelProps) {
250
+ const fetchConfig = resolveNodeFetchConfig(node);
251
+ const endpoints = fetchConfig.swaggerEndpoints ?? [];
252
+ const hasSwaggerEndpoints = endpoints.length > 0;
253
+ const urlInputMode =
254
+ fetchConfig.urlInputMode ?? (hasSwaggerEndpoints ? "swagger" : "manual");
255
+ const swaggerTask = useSwaggerLoadTask(node.id);
256
+ const fetchDebugTask = useFetchDebugTask(node.id);
257
+ const loadingSwagger = swaggerTask.status === "loading";
258
+ const loadingFetchDebug = fetchDebugTask.status === "loading";
259
+ const swaggerTaskError =
260
+ swaggerTask.status === "error" ? swaggerTask.error : null;
261
+ const [validationError, setValidationError] = useState<string | null>(null);
262
+ const [fetchValidationError, setFetchValidationError] = useState<string | null>(null);
263
+ const swaggerError = validationError ?? swaggerTaskError;
264
+
265
+ const setUrlInputMode = useCallback(
266
+ (mode: "swagger" | "manual") => {
267
+ onUpdateNode(node.id, patchFetchConfig(node, { urlInputMode: mode }));
268
+ },
269
+ [node, onUpdateNode]
270
+ );
271
+
272
+ const handleLoadSwagger = useCallback(() => {
273
+ const docsUrl = fetchConfig.swaggerDocsUrl?.trim();
274
+ if (!docsUrl) {
275
+ setValidationError("请先填写 Swagger 文档 URL");
276
+ return;
277
+ }
278
+
279
+ setValidationError(null);
280
+ startSwaggerLoadTask({
281
+ nodeId: node.id,
282
+ docsUrl,
283
+ onSuccess: (parsed, url) => {
284
+ onUpdateNode(
285
+ node.id,
286
+ patchFetchConfig(node, {
287
+ swaggerDocsUrl: url,
288
+ apiBaseUrl: parsed.apiBaseUrl,
289
+ swaggerEndpoints: parsed.endpoints,
290
+ urlInputMode: "swagger",
291
+ })
292
+ );
293
+ },
294
+ });
295
+ }, [fetchConfig.swaggerDocsUrl, node, onUpdateNode]);
296
+
297
+ const handleAbortSwagger = useCallback(() => {
298
+ cancelSwaggerLoadTask(node.id);
299
+ setValidationError(null);
300
+ }, [node.id]);
301
+
302
+ const handleSendFetchDebug = useCallback(() => {
303
+ const url = fetchConfig.url?.trim();
304
+ if (!url) {
305
+ setFetchValidationError("请先填写请求 URL");
306
+ return;
307
+ }
308
+
309
+ setFetchValidationError(null);
310
+ startFetchDebugTask({
311
+ nodeId: node.id,
312
+ config: fetchConfig,
313
+ });
314
+ }, [fetchConfig, node.id]);
315
+
316
+ const handleAbortFetchDebug = useCallback(() => {
317
+ cancelFetchDebugTask(node.id);
318
+ setFetchValidationError(null);
319
+ }, [node.id]);
320
+
321
+ return (
322
+ <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
323
+ <div className="font-medium text-foreground">数据源获取 (Fetch)</div>
324
+ <p className="text-[11px] text-muted-foreground">
325
+ 收到<strong>真信号</strong>后发起 HTTP 请求;可导入 Swagger 文档后从接口列表联想选择
326
+ URL。
327
+ </p>
328
+
329
+ <label className="block space-y-1">
330
+ <span className="text-muted-foreground">Swagger 文档 URL(可选)</span>
331
+ <div className="flex gap-1.5">
332
+ <Input
333
+ value={fetchConfig.swaggerDocsUrl ?? ""}
334
+ disabled={loadingSwagger}
335
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
336
+ onUpdateNode(
337
+ node.id,
338
+ patchFetchConfig(node, { swaggerDocsUrl: e.target.value })
339
+ )
340
+ }
341
+ placeholder="https://example.com/v3/api-docs"
342
+ className="h-8 flex-1 font-mono text-[11px]"
343
+ />
344
+ {loadingSwagger ? (
345
+ <Button
346
+ type="button"
347
+ variant="outline"
348
+ size="icon"
349
+ className="h-8 w-8 shrink-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
350
+ title="中止解析"
351
+ aria-label="中止 Swagger 解析"
352
+ onClick={handleAbortSwagger}
353
+ >
354
+ <StopIcon className="h-3.5 w-3.5" />
355
+ </Button>
356
+ ) : (
357
+ <Button
358
+ type="button"
359
+ variant="outline"
360
+ size="icon"
361
+ className="h-8 w-8 shrink-0"
362
+ title="解析 Swagger 文档"
363
+ onClick={() => handleLoadSwagger()}
364
+ >
365
+ <SendIcon className="h-4 w-4" />
366
+ </Button>
367
+ )}
368
+ </div>
369
+ {loadingSwagger ? (
370
+ <p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
371
+ <LoaderIcon className="h-3.5 w-3.5 shrink-0" />
372
+ 正在解析 Swagger 文档,点击右侧按钮可中止…
373
+ </p>
374
+ ) : null}
375
+ {swaggerError ? (
376
+ <p className="text-[11px] text-destructive">{swaggerError}</p>
377
+ ) : null}
378
+ {endpoints.length > 0 ? (
379
+ <p className="text-[11px] text-muted-foreground">
380
+ 已解析 {endpoints.length} 个接口
381
+ </p>
382
+ ) : null}
383
+ </label>
384
+
385
+ <label className="block space-y-1">
386
+ <div className="flex items-center justify-between gap-2">
387
+ <span className="text-muted-foreground">请求 URL</span>
388
+ {hasSwaggerEndpoints ? (
389
+ <div className="flex rounded-md border border-border p-0.5">
390
+ <button
391
+ type="button"
392
+ disabled={loadingSwagger}
393
+ className={cn(
394
+ "rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50",
395
+ urlInputMode === "swagger"
396
+ ? "bg-primary text-primary-foreground"
397
+ : "text-muted-foreground hover:text-foreground"
398
+ )}
399
+ onClick={() => setUrlInputMode("swagger")}
400
+ >
401
+ 接口联想
402
+ </button>
403
+ <button
404
+ type="button"
405
+ disabled={loadingSwagger}
406
+ className={cn(
407
+ "rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50",
408
+ urlInputMode === "manual"
409
+ ? "bg-primary text-primary-foreground"
410
+ : "text-muted-foreground hover:text-foreground"
411
+ )}
412
+ onClick={() => setUrlInputMode("manual")}
413
+ >
414
+ 手动输入
415
+ </button>
416
+ </div>
417
+ ) : null}
418
+ </div>
419
+
420
+ {urlInputMode === "swagger" && hasSwaggerEndpoints ? (
421
+ <div
422
+ className={cn(
423
+ "space-y-2",
424
+ loadingSwagger && "pointer-events-none opacity-60"
425
+ )}
426
+ >
427
+ <label className="block space-y-1">
428
+ <span className="text-muted-foreground">API 主机 / Base URL</span>
429
+ <Input
430
+ value={fetchConfig.apiBaseUrl ?? ""}
431
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
432
+ onUpdateNode(
433
+ node.id,
434
+ patchFetchConfig(node, { apiBaseUrl: e.target.value })
435
+ )
436
+ }
437
+ placeholder="https://api.example.com/v1"
438
+ className="h-8 font-mono text-[11px]"
439
+ />
440
+ </label>
441
+ <label className="block space-y-1">
442
+ <span className="text-muted-foreground">选择接口</span>
443
+ <div className="flex gap-1.5">
444
+ <div className="min-w-0 flex-1">
445
+ <FetchUrlAutocomplete
446
+ value={fetchConfig.url}
447
+ apiBaseUrl={fetchConfig.apiBaseUrl ?? ""}
448
+ endpoints={endpoints}
449
+ selectOnly
450
+ placeholder="点击选择或搜索接口"
451
+ onChange={(url) =>
452
+ onUpdateNode(node.id, patchFetchConfig(node, { url }))
453
+ }
454
+ onSelectEndpoint={(endpoint) =>
455
+ onUpdateNode(
456
+ node.id,
457
+ patchFetchConfig(node, {
458
+ url: endpoint.path,
459
+ method: endpoint.method,
460
+ })
461
+ )
462
+ }
463
+ />
464
+ </div>
465
+ <AsyncSendButton
466
+ loading={loadingFetchDebug}
467
+ disabled={loadingSwagger}
468
+ onSend={handleSendFetchDebug}
469
+ onAbort={handleAbortFetchDebug}
470
+ sendTitle="发送调试请求"
471
+ abortTitle="中止请求"
472
+ sendAriaLabel="发送调试请求"
473
+ abortAriaLabel="中止调试请求"
474
+ />
475
+ </div>
476
+ </label>
477
+ </div>
478
+ ) : (
479
+ <div className="flex gap-1.5">
480
+ <Input
481
+ value={fetchConfig.url}
482
+ disabled={loadingFetchDebug}
483
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
484
+ onUpdateNode(node.id, patchFetchConfig(node, { url: e.target.value }))
485
+ }
486
+ placeholder="https://api.example.com/data"
487
+ className="h-8 flex-1 font-mono text-[11px]"
488
+ />
489
+ <AsyncSendButton
490
+ loading={loadingFetchDebug}
491
+ disabled={loadingSwagger}
492
+ onSend={handleSendFetchDebug}
493
+ onAbort={handleAbortFetchDebug}
494
+ sendTitle="发送调试请求"
495
+ abortTitle="中止请求"
496
+ sendAriaLabel="发送调试请求"
497
+ abortAriaLabel="中止调试请求"
498
+ />
499
+ </div>
500
+ )}
501
+ {loadingFetchDebug ? (
502
+ <p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
503
+ <LoaderIcon className="h-3.5 w-3.5 shrink-0" />
504
+ 正在发送调试请求,点击右侧按钮可中止…
505
+ </p>
506
+ ) : null}
507
+ {fetchValidationError ? (
508
+ <p className="text-[11px] text-destructive">{fetchValidationError}</p>
509
+ ) : null}
510
+ <FetchDebugResponsePanel task={fetchDebugTask} />
511
+ </label>
512
+
513
+ <label className="block space-y-1">
514
+ <span className="text-muted-foreground">请求方法</span>
515
+ <Select
516
+ value={fetchConfig.method ?? "GET"}
517
+ onValueChange={(value: string) =>
518
+ onUpdateNode(
519
+ node.id,
520
+ patchFetchConfig(node, { method: value as FetchHttpMethod })
521
+ )
522
+ }
523
+ >
524
+ <SelectTrigger className="h-8">
525
+ <SelectValue />
526
+ </SelectTrigger>
527
+ <SelectContent>
528
+ {FETCH_HTTP_METHODS.map((method) => (
529
+ <SelectItem key={method} value={method}>
530
+ {method}
531
+ </SelectItem>
532
+ ))}
533
+ </SelectContent>
534
+ </Select>
535
+ </label>
536
+
537
+ <label className="block space-y-1">
538
+ <span className="text-muted-foreground">请求头 (JSON)</span>
539
+ <textarea
540
+ defaultValue={JSON.stringify(fetchConfig.headers ?? {}, null, 2)}
541
+ key={`${node.id}-headers-${JSON.stringify(fetchConfig.headers)}`}
542
+ onBlur={(e) => {
543
+ try {
544
+ const headers = JSON.parse(e.target.value || "{}") as Record<
545
+ string,
546
+ string
547
+ >;
548
+ onUpdateNode(node.id, patchFetchConfig(node, { headers }));
549
+ } catch {
550
+ /* 保留上次有效值 */
551
+ }
552
+ }}
553
+ rows={4}
554
+ className="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"
555
+ placeholder='{"Content-Type":"application/json"}'
556
+ />
557
+ </label>
558
+
559
+ <label className="block space-y-1">
560
+ <span className="text-muted-foreground">请求体</span>
561
+ <textarea
562
+ value={fetchConfig.body ?? ""}
563
+ onChange={(e) =>
564
+ onUpdateNode(node.id, patchFetchConfig(node, { body: e.target.value }))
565
+ }
566
+ rows={4}
567
+ className="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"
568
+ placeholder='{"key":"value"}'
569
+ />
570
+ </label>
571
+
572
+ <div className="grid grid-cols-2 gap-2">
573
+ <label className="block space-y-1">
574
+ <span className="text-muted-foreground">Credentials</span>
575
+ <Select
576
+ value={fetchConfig.credentials ?? "same-origin"}
577
+ onValueChange={(value: string) =>
578
+ onUpdateNode(
579
+ node.id,
580
+ patchFetchConfig(node, {
581
+ credentials: value as RequestCredentials,
582
+ })
583
+ )
584
+ }
585
+ >
586
+ <SelectTrigger className="h-8">
587
+ <SelectValue />
588
+ </SelectTrigger>
589
+ <SelectContent>
590
+ {FETCH_CREDENTIALS.map((item) => (
591
+ <SelectItem key={item} value={item}>
592
+ {item}
593
+ </SelectItem>
594
+ ))}
595
+ </SelectContent>
596
+ </Select>
597
+ </label>
598
+ <label className="block space-y-1">
599
+ <span className="text-muted-foreground">Mode</span>
600
+ <Select
601
+ value={fetchConfig.mode ?? "cors"}
602
+ onValueChange={(value: string) =>
603
+ onUpdateNode(
604
+ node.id,
605
+ patchFetchConfig(node, { mode: value as RequestMode })
606
+ )
607
+ }
608
+ >
609
+ <SelectTrigger className="h-8">
610
+ <SelectValue />
611
+ </SelectTrigger>
612
+ <SelectContent>
613
+ {FETCH_MODES.map((item) => (
614
+ <SelectItem key={item} value={item}>
615
+ {item}
616
+ </SelectItem>
617
+ ))}
618
+ </SelectContent>
619
+ </Select>
620
+ </label>
621
+ <label className="block space-y-1">
622
+ <span className="text-muted-foreground">Cache</span>
623
+ <Select
624
+ value={fetchConfig.cache ?? "default"}
625
+ onValueChange={(value: string) =>
626
+ onUpdateNode(
627
+ node.id,
628
+ patchFetchConfig(node, { cache: value as RequestCache })
629
+ )
630
+ }
631
+ >
632
+ <SelectTrigger className="h-8">
633
+ <SelectValue />
634
+ </SelectTrigger>
635
+ <SelectContent>
636
+ {FETCH_CACHES.map((item) => (
637
+ <SelectItem key={item} value={item}>
638
+ {item}
639
+ </SelectItem>
640
+ ))}
641
+ </SelectContent>
642
+ </Select>
643
+ </label>
644
+ <label className="block space-y-1">
645
+ <span className="text-muted-foreground">Redirect</span>
646
+ <Select
647
+ value={fetchConfig.redirect ?? "follow"}
648
+ onValueChange={(value: string) =>
649
+ onUpdateNode(
650
+ node.id,
651
+ patchFetchConfig(node, { redirect: value as RequestRedirect })
652
+ )
653
+ }
654
+ >
655
+ <SelectTrigger className="h-8">
656
+ <SelectValue />
657
+ </SelectTrigger>
658
+ <SelectContent>
659
+ {FETCH_REDIRECTS.map((item) => (
660
+ <SelectItem key={item} value={item}>
661
+ {item}
662
+ </SelectItem>
663
+ ))}
664
+ </SelectContent>
665
+ </Select>
666
+ </label>
667
+ </div>
668
+
669
+ <div className="grid grid-cols-2 gap-2">
670
+ <label className="block space-y-1">
671
+ <span className="text-muted-foreground">响应解析</span>
672
+ <Select
673
+ value={fetchConfig.responseType ?? "json"}
674
+ onValueChange={(value: string) =>
675
+ onUpdateNode(
676
+ node.id,
677
+ patchFetchConfig(node, {
678
+ responseType: value as FetchResponseType,
679
+ })
680
+ )
681
+ }
682
+ >
683
+ <SelectTrigger className="h-8">
684
+ <SelectValue />
685
+ </SelectTrigger>
686
+ <SelectContent>
687
+ {FETCH_RESPONSE_TYPES.map((item) => (
688
+ <SelectItem key={item} value={item}>
689
+ {item}
690
+ </SelectItem>
691
+ ))}
692
+ </SelectContent>
693
+ </Select>
694
+ </label>
695
+ <label className="block space-y-1">
696
+ <span className="text-muted-foreground">超时 (ms)</span>
697
+ <Input
698
+ type="number"
699
+ min={0}
700
+ step={1000}
701
+ value={fetchConfig.timeoutMs ?? 30000}
702
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
703
+ onUpdateNode(
704
+ node.id,
705
+ patchFetchConfig(node, {
706
+ timeoutMs: Number(e.target.value) || 0,
707
+ })
708
+ )
709
+ }
710
+ className="h-8"
711
+ />
712
+ </label>
713
+ </div>
714
+ </div>
715
+ );
716
+ }