@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.
- package/README.md +50 -0
- package/package.json +44 -0
- package/src/BlueprintCanvasContext.ts +71 -0
- package/src/BlueprintNodeConfigSidebar.vue +338 -0
- package/src/blueprint.css +327 -0
- package/src/blueprintNodeTypes.ts +20 -0
- package/src/components/BluePrintVueRoot.vue +73 -0
- package/src/components/BlueprintCanvas.vue +220 -0
- package/src/components/BlueprintContextMenu.vue +114 -0
- package/src/components/BlueprintExecutionLogPanel.vue +294 -0
- package/src/components/BlueprintMetaDialog.vue +80 -0
- package/src/components/BlueprintNodeSwitchTaskDialog.vue +41 -0
- package/src/components/ClockNodeConfigPanel.vue +124 -0
- package/src/components/FetchNodeConfigPanel.vue +559 -0
- package/src/components/FetchUrlAutocomplete.vue +174 -0
- package/src/components/JsonNodeConfigPanel.vue +73 -0
- package/src/components/LogicNodeConfigPanel.vue +73 -0
- package/src/components/ViewElementMultiSelect.vue +50 -0
- package/src/composables/useBlueprintDebugSession.ts +441 -0
- package/src/composables/useBlueprintFlowState.ts +486 -0
- package/src/composables/useBlueprintFlowViewport.ts +65 -0
- package/src/composables/useBlueprintNodeSelectionGuard.ts +41 -0
- package/src/composables/useBlueprintPageLifecycle.ts +244 -0
- package/src/createBlueprintEdgeTypes.ts +10 -0
- package/src/edges/BlueprintSmoothEdge.vue +31 -0
- package/src/env.d.ts +7 -0
- package/src/fetch-config-task-store.ts +206 -0
- package/src/flowCoordinates.ts +19 -0
- package/src/flowDefaults.ts +9 -0
- package/src/graph/blueprint-graph.ts +265 -0
- package/src/graph/document.ts +422 -0
- package/src/graph/index.ts +7 -0
- package/src/graph/node-summary.ts +88 -0
- package/src/graph/node-types.ts +9 -0
- package/src/graph/sync-edges.ts +69 -0
- package/src/graph/sync-nodes.ts +110 -0
- package/src/graph/vue-flow-adapter.ts +127 -0
- package/src/index.ts +37 -0
- package/src/library/blueprint-io.ts +108 -0
- package/src/library/blueprint-library-db.ts +112 -0
- package/src/library/execution-log-db.ts +171 -0
- package/src/library/execution-log-settings.ts +50 -0
- package/src/library/swagger-docs.ts +56 -0
- package/src/library/types.ts +35 -0
- package/src/nodes/AndFlowNode.vue +60 -0
- package/src/nodes/BlueprintFlowNode.vue +26 -0
- package/src/nodes/BlueprintNodeCard.vue +155 -0
- package/src/nodes/BlueprintNodeShell.vue +70 -0
- package/src/nodes/ClockFlowNode.vue +60 -0
- package/src/nodes/FetchFlowNode.vue +26 -0
- package/src/nodes/JsonFlowNode.vue +26 -0
- package/src/nodes/LifecycleFlowNode.vue +45 -0
- package/src/nodes/LogicFlowNode.vue +26 -0
- package/src/runtime/document-to-runnable-graph.ts +51 -0
- package/src/runtime/execution-overlay.ts +169 -0
- package/src/types.ts +1 -0
- 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>
|