vue-api-request-builder 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 +188 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.es.js +633 -0
- package/dist/index.umd.js +6 -0
- package/dist/vite.svg +1 -0
- package/dist/vue-api-request-builder.css +1 -0
- package/lib/components/KeyValueInput.vue +144 -0
- package/lib/components/RequestForm.vue +388 -0
- package/lib/components/ResponseSection.vue +144 -0
- package/lib/index.ts +17 -0
- package/lib/types/request.ts +55 -0
- package/lib/utils/request.ts +194 -0
- package/package.json +49 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="key-value-input flex flex-col gap-1">
|
|
3
|
+
<div class="flex gap-2">
|
|
4
|
+
<a-button type="primary" @click="addItem" class="w-40 max-w-full" size="small">
|
|
5
|
+
{{ addButtonText }}
|
|
6
|
+
</a-button>
|
|
7
|
+
<a-popconfirm
|
|
8
|
+
title="确认清空"
|
|
9
|
+
description="确定要清空所有参数吗?"
|
|
10
|
+
ok-text="确认"
|
|
11
|
+
cancel-text="取消"
|
|
12
|
+
@confirm="clearItems"
|
|
13
|
+
>
|
|
14
|
+
<a-button type="primary" danger class="w-40 max-w-full" size="small" :disabled="items.length === 0">
|
|
15
|
+
清空
|
|
16
|
+
</a-button>
|
|
17
|
+
</a-popconfirm>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<template v-if="items.length > 0">
|
|
21
|
+
<div class="flex flex-col gap-1">
|
|
22
|
+
<div v-for="(item, index) in items" :key="index" class="flex">
|
|
23
|
+
<div class="flex w-full gap-1 items-center">
|
|
24
|
+
<a-button type="primary" danger @click="removeItem(index)" class="w-16" size="small">
|
|
25
|
+
删除
|
|
26
|
+
</a-button>
|
|
27
|
+
<a-input v-model:value="item.key" placeholder="键" style="width: 60%" size="small" />
|
|
28
|
+
<a-input v-model:value="item.value" placeholder="值" style="width: 100%" size="small" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<a-typography v-if="showPreview" class="m-0">
|
|
35
|
+
<a-typography-paragraph class="!m-0">
|
|
36
|
+
<pre>{{ previewText }}</pre>
|
|
37
|
+
</a-typography-paragraph>
|
|
38
|
+
</a-typography>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { ref, watch } from "vue";
|
|
44
|
+
|
|
45
|
+
interface KeyValueItem {
|
|
46
|
+
key: string;
|
|
47
|
+
value: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
modelValue: KeyValueItem[];
|
|
52
|
+
addButtonText?: string;
|
|
53
|
+
showPreview?: boolean;
|
|
54
|
+
previewText?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
58
|
+
addButtonText: "添加参数",
|
|
59
|
+
showPreview: false,
|
|
60
|
+
previewText: "",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const emit = defineEmits<{
|
|
64
|
+
(e: "update:modelValue", value: KeyValueItem[]): void;
|
|
65
|
+
}>();
|
|
66
|
+
|
|
67
|
+
const items = ref<KeyValueItem[]>(props.modelValue);
|
|
68
|
+
|
|
69
|
+
watch(
|
|
70
|
+
() => props.modelValue,
|
|
71
|
+
(newValue) => {
|
|
72
|
+
items.value = newValue;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
watch(
|
|
77
|
+
items,
|
|
78
|
+
(newValue) => {
|
|
79
|
+
emit("update:modelValue", newValue);
|
|
80
|
+
},
|
|
81
|
+
{ deep: true }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const addItem = () => {
|
|
85
|
+
items.value.push({ key: "", value: "" });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const removeItem = (index: number) => {
|
|
89
|
+
items.value.splice(index, 1);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const clearItems = () => {
|
|
93
|
+
items.value = [];
|
|
94
|
+
};
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
.key-value-input {
|
|
99
|
+
width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.empty-state {
|
|
103
|
+
color: #999;
|
|
104
|
+
text-align: center;
|
|
105
|
+
padding: 8px 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.key-value-input :deep(.ant-typography pre) {
|
|
109
|
+
background-color: #f5f5f5;
|
|
110
|
+
padding: 4px 8px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
margin: 0;
|
|
113
|
+
width: 100%;
|
|
114
|
+
overflow-x: auto;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.key-value-input :deep(.ant-space-item) {
|
|
118
|
+
flex: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.key-value-input :deep(.ant-space-item:first-child) {
|
|
122
|
+
flex: 0 0 100px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.key-value-input :deep(.ant-space-item:nth-child(2)) {
|
|
126
|
+
flex: 0 0 200px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.key-value-input :deep(.ant-list-item) {
|
|
130
|
+
padding: 4px 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
:deep(.ant-typography pre) {
|
|
134
|
+
background-color: #f5f5f5;
|
|
135
|
+
padding: 8px;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
margin: 0;
|
|
138
|
+
width: 100%;
|
|
139
|
+
overflow-x: auto;
|
|
140
|
+
max-height: 60px;
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
line-height: 1.5;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="request-form">
|
|
3
|
+
<a-form layout="vertical">
|
|
4
|
+
<!-- 请求部分 -->
|
|
5
|
+
<a-card title="URL配置" class="form-section" size="small">
|
|
6
|
+
<div class="flex flex-col gap-1">
|
|
7
|
+
<div class="flex flex-row gap-1">
|
|
8
|
+
<a-input v-model:value="url" placeholder="基础URL" size="small" />
|
|
9
|
+
<a-button v-if="canParseUrl" type="primary" size="small" @click="parseUrl"
|
|
10
|
+
>拆解</a-button
|
|
11
|
+
>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="flex flex-row gap-1">
|
|
14
|
+
<a-select v-model:value="method" class="w-40" size="small">
|
|
15
|
+
<a-select-option value="GET">GET</a-select-option>
|
|
16
|
+
<a-select-option value="POST">POST</a-select-option>
|
|
17
|
+
<a-select-option value="PUT">PUT</a-select-option>
|
|
18
|
+
<a-select-option value="DELETE">DELETE</a-select-option>
|
|
19
|
+
<a-select-option value="OPTIONS">OPTIONS</a-select-option>
|
|
20
|
+
</a-select>
|
|
21
|
+
<a-input v-model:value="path" placeholder="路径" size="small" />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- URL参数拼接部分 -->
|
|
26
|
+
<div class="mt-1">
|
|
27
|
+
<KeyValueInput
|
|
28
|
+
v-model="params"
|
|
29
|
+
add-button-text="添加参数"
|
|
30
|
+
:show-preview="true"
|
|
31
|
+
:preview-text="url + path + queryString"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</a-card>
|
|
35
|
+
|
|
36
|
+
<!-- 认证部分 -->
|
|
37
|
+
<a-card title="认证方案" class="form-section" size="small">
|
|
38
|
+
<a-radio-group v-model:value="auth" button-style="solid" size="small">
|
|
39
|
+
<a-radio-button value="none">无认证</a-radio-button>
|
|
40
|
+
<a-radio-button value="Basic">Basic认证</a-radio-button>
|
|
41
|
+
<a-radio-button value="Bearer">Bearer认证</a-radio-button>
|
|
42
|
+
</a-radio-group>
|
|
43
|
+
|
|
44
|
+
<template v-if="auth === 'Basic'">
|
|
45
|
+
<div class="flex flex-col gap-1 mt-2">
|
|
46
|
+
<div class="flex flex-row gap-1 h-6">
|
|
47
|
+
<p class="m-0 w-16">用户名</p>
|
|
48
|
+
<a-input v-model:value="httpUser" size="small" />
|
|
49
|
+
</div>
|
|
50
|
+
<div class="flex flex-row gap-1">
|
|
51
|
+
<p class="m-0 w-16">密码</p>
|
|
52
|
+
<a-input-password v-model:value="httpPassword" size="small" />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
<template v-if="auth === 'Bearer'">
|
|
57
|
+
<div class="flex flex-col gap-1 mt-1">
|
|
58
|
+
<div class="flex flex-row gap-1 items-center">
|
|
59
|
+
<p class="m-0 w-16 h-full">Token</p>
|
|
60
|
+
<a-textarea
|
|
61
|
+
v-model:value="httpToken"
|
|
62
|
+
size="small"
|
|
63
|
+
class="mt-1"
|
|
64
|
+
rows="4"
|
|
65
|
+
placeholder="请输入Token"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
</a-card>
|
|
71
|
+
|
|
72
|
+
<!-- 请求头部分 -->
|
|
73
|
+
<a-card title="请求头" class="form-section" size="small">
|
|
74
|
+
<KeyValueInput v-model="headers" add-button-text="添加请求头" :show-preview="false" />
|
|
75
|
+
</a-card>
|
|
76
|
+
|
|
77
|
+
<!-- 请求体部分 -->
|
|
78
|
+
<a-card
|
|
79
|
+
v-if="method === 'POST' || method === 'PUT'"
|
|
80
|
+
title="请求体"
|
|
81
|
+
class="form-section"
|
|
82
|
+
size="small"
|
|
83
|
+
>
|
|
84
|
+
<a-radio-group v-model:value="contentType" button-style="solid" size="small" class="mb-1">
|
|
85
|
+
<a-radio-button value="application/json">JSON</a-radio-button>
|
|
86
|
+
<a-radio-button value="multipart/form-data">Form Data</a-radio-button>
|
|
87
|
+
<a-radio-button value="text/plain">Raw</a-radio-button>
|
|
88
|
+
</a-radio-group>
|
|
89
|
+
|
|
90
|
+
<!-- JSON 输入 -->
|
|
91
|
+
<template v-if="contentType === 'application/json'">
|
|
92
|
+
<a-textarea
|
|
93
|
+
v-model:value="jsonBody"
|
|
94
|
+
:rows="6"
|
|
95
|
+
placeholder="请输入 JSON 数据"
|
|
96
|
+
@input="handleJsonInput"
|
|
97
|
+
/>
|
|
98
|
+
<a-alert
|
|
99
|
+
v-if="jsonError"
|
|
100
|
+
type="error"
|
|
101
|
+
:message="jsonError"
|
|
102
|
+
banner
|
|
103
|
+
style="margin-bottom: 8px"
|
|
104
|
+
/>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<!-- Form Data 输入 -->
|
|
108
|
+
<template v-if="contentType === 'multipart/form-data'">
|
|
109
|
+
<KeyValueInput
|
|
110
|
+
v-model="bodyParams"
|
|
111
|
+
add-button-text="添加表单字段"
|
|
112
|
+
:show-preview="false"
|
|
113
|
+
/>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<!-- Raw 输入 -->
|
|
117
|
+
<template v-if="contentType === 'text/plain'">
|
|
118
|
+
<a-textarea v-model:value="rawBody" :rows="6" placeholder="请输入原始数据" />
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<!-- 预览部分 -->
|
|
122
|
+
<pre>{{ requestBodyPreview }}</pre>
|
|
123
|
+
</a-card>
|
|
124
|
+
</a-form>
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<script setup lang="ts">
|
|
129
|
+
import { ref, computed, watch } from "vue";
|
|
130
|
+
import KeyValueInput from "./KeyValueInput.vue";
|
|
131
|
+
import type { RequestSchema, KeyValuePair } from "../types/request";
|
|
132
|
+
import { defaultRequestSchema } from "../types/request";
|
|
133
|
+
|
|
134
|
+
interface Props {
|
|
135
|
+
modelValue?: RequestSchema;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
139
|
+
modelValue: () => defaultRequestSchema,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const emit = defineEmits<{
|
|
143
|
+
(e: "update:modelValue", value: RequestSchema): void;
|
|
144
|
+
}>();
|
|
145
|
+
|
|
146
|
+
// 响应式状态
|
|
147
|
+
const method = ref(props.modelValue.method);
|
|
148
|
+
const url = ref(props.modelValue.url);
|
|
149
|
+
const auth = ref(props.modelValue.auth.type);
|
|
150
|
+
const path = ref(props.modelValue.path);
|
|
151
|
+
const httpUser = ref(props.modelValue.auth.username || "");
|
|
152
|
+
const httpPassword = ref(props.modelValue.auth.password || "");
|
|
153
|
+
const httpToken = ref(props.modelValue.auth.token || "");
|
|
154
|
+
const params = ref<KeyValuePair[]>(props.modelValue.params);
|
|
155
|
+
const headers = ref<KeyValuePair[]>(props.modelValue.headers);
|
|
156
|
+
const bodyParams = ref<KeyValuePair[]>(props.modelValue.body.formData || []);
|
|
157
|
+
const contentType = ref(props.modelValue.body.type);
|
|
158
|
+
const jsonBody = ref(props.modelValue.body.json || "");
|
|
159
|
+
const rawBody = ref(props.modelValue.body.raw || "");
|
|
160
|
+
const jsonError = ref("");
|
|
161
|
+
|
|
162
|
+
// 创建一个防抖函数
|
|
163
|
+
function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
|
|
164
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
165
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
166
|
+
clearTimeout(timeoutId);
|
|
167
|
+
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 创建一个生成schema的函数
|
|
172
|
+
const generateSchema = () => {
|
|
173
|
+
return {
|
|
174
|
+
method: method.value,
|
|
175
|
+
url: url.value,
|
|
176
|
+
path: path.value,
|
|
177
|
+
auth: {
|
|
178
|
+
type: auth.value,
|
|
179
|
+
...(auth.value === "Basic"
|
|
180
|
+
? {
|
|
181
|
+
username: httpUser.value,
|
|
182
|
+
password: httpPassword.value,
|
|
183
|
+
}
|
|
184
|
+
: auth.value === "Bearer"
|
|
185
|
+
? {
|
|
186
|
+
token: httpToken.value,
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
189
|
+
},
|
|
190
|
+
params: params.value,
|
|
191
|
+
headers: headers.value,
|
|
192
|
+
body: {
|
|
193
|
+
type: contentType.value,
|
|
194
|
+
...(contentType.value === "application/json"
|
|
195
|
+
? { json: jsonBody.value }
|
|
196
|
+
: contentType.value === "multipart/form-data"
|
|
197
|
+
? { formData: bodyParams.value }
|
|
198
|
+
: { raw: rawBody.value }),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 创建一个更新schema的函数
|
|
204
|
+
const updateSchema = debounce(() => {
|
|
205
|
+
emit("update:modelValue", generateSchema());
|
|
206
|
+
}, 100);
|
|
207
|
+
|
|
208
|
+
// 监听内部状态变化
|
|
209
|
+
watch(
|
|
210
|
+
[
|
|
211
|
+
method,
|
|
212
|
+
url,
|
|
213
|
+
path,
|
|
214
|
+
auth,
|
|
215
|
+
httpUser,
|
|
216
|
+
httpPassword,
|
|
217
|
+
httpToken,
|
|
218
|
+
params,
|
|
219
|
+
headers,
|
|
220
|
+
contentType,
|
|
221
|
+
bodyParams,
|
|
222
|
+
jsonBody,
|
|
223
|
+
rawBody,
|
|
224
|
+
],
|
|
225
|
+
() => {
|
|
226
|
+
updateSchema();
|
|
227
|
+
},
|
|
228
|
+
{ deep: true }
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// 监听外部变化
|
|
232
|
+
watch(
|
|
233
|
+
() => props.modelValue,
|
|
234
|
+
(newValue) => {
|
|
235
|
+
const currentSchema = generateSchema();
|
|
236
|
+
// 防止循环更新
|
|
237
|
+
if (JSON.stringify(newValue) === JSON.stringify(currentSchema)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
method.value = newValue.method;
|
|
242
|
+
url.value = newValue.url;
|
|
243
|
+
path.value = newValue.path;
|
|
244
|
+
auth.value = newValue.auth.type;
|
|
245
|
+
httpUser.value = newValue.auth.username || "";
|
|
246
|
+
httpPassword.value = newValue.auth.password || "";
|
|
247
|
+
httpToken.value = newValue.auth.token || "";
|
|
248
|
+
params.value = newValue.params;
|
|
249
|
+
headers.value = newValue.headers;
|
|
250
|
+
contentType.value = newValue.body.type;
|
|
251
|
+
bodyParams.value = newValue.body.formData || [];
|
|
252
|
+
jsonBody.value = newValue.body.json || "";
|
|
253
|
+
rawBody.value = newValue.body.raw || "";
|
|
254
|
+
},
|
|
255
|
+
{ deep: true }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// 计算属性
|
|
259
|
+
const queryString = computed(() => {
|
|
260
|
+
const result = params.value
|
|
261
|
+
.filter((p) => !!p.key)
|
|
262
|
+
.map((p) => p.key + "=" + encodeURIComponent(p.value))
|
|
263
|
+
.join("&");
|
|
264
|
+
return result === "" ? "" : "?" + result;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const requestBodyPreview = computed(() => {
|
|
268
|
+
switch (contentType.value) {
|
|
269
|
+
case "application/json":
|
|
270
|
+
try {
|
|
271
|
+
if (!jsonBody.value) {
|
|
272
|
+
return "-空-";
|
|
273
|
+
}
|
|
274
|
+
return JSON.stringify(JSON.parse(jsonBody.value), null, 2);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return "Invalid JSON";
|
|
277
|
+
}
|
|
278
|
+
case "multipart/form-data":
|
|
279
|
+
const boundary = "----WebKitFormBoundaryPreview";
|
|
280
|
+
const formData = bodyParams.value
|
|
281
|
+
.filter((p) => !!p.key)
|
|
282
|
+
.map(
|
|
283
|
+
(p) =>
|
|
284
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${p.key}"\r\n\r\n${p.value}\r\n`
|
|
285
|
+
)
|
|
286
|
+
.join("");
|
|
287
|
+
return formData + `--${boundary}--\r\n`;
|
|
288
|
+
case "text/plain":
|
|
289
|
+
return rawBody.value || "-空-";
|
|
290
|
+
default:
|
|
291
|
+
return "-空-";
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const handleJsonInput = () => {
|
|
296
|
+
try {
|
|
297
|
+
if (jsonBody.value) {
|
|
298
|
+
JSON.parse(jsonBody.value);
|
|
299
|
+
jsonError.value = "";
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
jsonError.value = "Invalid JSON format";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const parseUrl = () => {
|
|
307
|
+
try {
|
|
308
|
+
const fullUrl = url.value;
|
|
309
|
+
const urlObj = new URL(fullUrl);
|
|
310
|
+
|
|
311
|
+
// 设置基础URL
|
|
312
|
+
url.value = `${urlObj.protocol}//${urlObj.host}`;
|
|
313
|
+
|
|
314
|
+
// 设置路径
|
|
315
|
+
path.value = urlObj.pathname;
|
|
316
|
+
|
|
317
|
+
// 解析查询参数
|
|
318
|
+
const searchParams = new URLSearchParams(urlObj.search);
|
|
319
|
+
const newParams: KeyValuePair[] = [];
|
|
320
|
+
searchParams.forEach((value, key) => {
|
|
321
|
+
newParams.push({ key, value });
|
|
322
|
+
});
|
|
323
|
+
params.value = newParams;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
// 如果URL解析失败,显示错误提示
|
|
326
|
+
console.error("URL解析失败:", error);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const canParseUrl = computed(() => {
|
|
331
|
+
if (!url.value) return false;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const fullUrl = url.value;
|
|
335
|
+
const urlObj = new URL(fullUrl);
|
|
336
|
+
|
|
337
|
+
// 检查是否有查询参数或路径
|
|
338
|
+
return urlObj.search !== "" || urlObj.pathname !== "/";
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
</script>
|
|
344
|
+
|
|
345
|
+
<style scoped>
|
|
346
|
+
.request-form {
|
|
347
|
+
margin: 0;
|
|
348
|
+
padding: 0px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.request-form * {
|
|
352
|
+
font-size: 0.9rem;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.form-section {
|
|
356
|
+
margin-bottom: 8px;
|
|
357
|
+
width: 100%;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
:deep(.ant-card-head) {
|
|
361
|
+
background-color: #fafafa;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:deep(.ant-card-head-title) {
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
padding: 4px 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:deep(.ant-card-body) {
|
|
370
|
+
padding: 8px 12px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
:deep(.ant-space) {
|
|
374
|
+
width: 100%;
|
|
375
|
+
display: flex;
|
|
376
|
+
gap: 8px;
|
|
377
|
+
align-items: center;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
pre {
|
|
381
|
+
background-color: #f5f5f5;
|
|
382
|
+
padding: 8px;
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
margin: 0;
|
|
385
|
+
width: 100%;
|
|
386
|
+
overflow-x: auto;
|
|
387
|
+
}
|
|
388
|
+
</style>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a-card title="响应" class="form-section" size="small">
|
|
3
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
|
4
|
+
<a-radio-group v-model:value="requestMethod" button-style="solid" size="small">
|
|
5
|
+
<a-radio-button value="xhr">XMLHttpRequest</a-radio-button>
|
|
6
|
+
<a-radio-button value="fetch">Fetch</a-radio-button>
|
|
7
|
+
</a-radio-group>
|
|
8
|
+
<a-button type="primary" @click="sendRequest" size="small">发送</a-button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<a-alert
|
|
12
|
+
v-if="errorMessage"
|
|
13
|
+
:message="errorMessage"
|
|
14
|
+
type="error"
|
|
15
|
+
show-icon
|
|
16
|
+
style="margin-bottom: 8px"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-col gap-2">
|
|
20
|
+
<div class="text-sm font-bold">基本信息</div>
|
|
21
|
+
<div class="flex flex-col gap-1">
|
|
22
|
+
<div>
|
|
23
|
+
<span>状态码:</span>
|
|
24
|
+
<a-tag :color="getStatusColor(response.status)">{{ response.status }}</a-tag>
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<span>耗时:</span>
|
|
28
|
+
<span>{{ response.timing ? `${response.timing}ms` : "-" }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="text-sm font-bold">响应头</div>
|
|
32
|
+
<template v-if="Object.keys(response.headers).length > 0">
|
|
33
|
+
<table class="border border-solid border-gray-300 w-full">
|
|
34
|
+
<tbody>
|
|
35
|
+
<tr v-for="[key, value] in Object.entries(response.headers)" :key="key">
|
|
36
|
+
<td class="border border-gray-300">{{ key }}</td>
|
|
37
|
+
<td class="border border-gray-300">{{ value }}</td>
|
|
38
|
+
</tr>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</template>
|
|
42
|
+
<p v-else>无响应头</p>
|
|
43
|
+
<div class="text-sm font-bold">响应体</div>
|
|
44
|
+
<a-textarea
|
|
45
|
+
v-model:value="response.body"
|
|
46
|
+
:rows="5"
|
|
47
|
+
readonly
|
|
48
|
+
style="width: 100%"
|
|
49
|
+
size="small"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</a-card>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { ref } from "vue";
|
|
57
|
+
import type { RequestSchema, ResponseData } from "../types/request";
|
|
58
|
+
import { executeRequest, type RequestMethod } from "../utils/request";
|
|
59
|
+
|
|
60
|
+
interface Props {
|
|
61
|
+
modelValue: RequestSchema;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const props = defineProps<Props>();
|
|
65
|
+
|
|
66
|
+
const requestMethod = ref<RequestMethod>("xhr");
|
|
67
|
+
const response = ref<ResponseData>({
|
|
68
|
+
status: "",
|
|
69
|
+
headers: {},
|
|
70
|
+
body: "",
|
|
71
|
+
timing: 0,
|
|
72
|
+
});
|
|
73
|
+
const errorMessage = ref<string>("");
|
|
74
|
+
|
|
75
|
+
const getStatusColor = (status: string | number): string => {
|
|
76
|
+
const statusNum = Number(status);
|
|
77
|
+
if (statusNum >= 200 && statusNum < 300) return "success";
|
|
78
|
+
if (statusNum >= 300 && statusNum < 400) return "warning";
|
|
79
|
+
if (statusNum >= 400 && statusNum < 500) return "error";
|
|
80
|
+
if (statusNum >= 500) return "error";
|
|
81
|
+
return "default";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getErrorMessage = (error: unknown): string => {
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return error.message;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return "请求失败";
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sendRequest = async () => {
|
|
93
|
+
errorMessage.value = "";
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
try {
|
|
96
|
+
response.value = await executeRequest(props.modelValue, requestMethod.value);
|
|
97
|
+
response.value.timing = Date.now() - startTime;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
errorMessage.value = getErrorMessage(error);
|
|
100
|
+
response.value = {
|
|
101
|
+
status: "Error",
|
|
102
|
+
headers: {},
|
|
103
|
+
body: error instanceof Error ? error.message : "Request failed",
|
|
104
|
+
timing: Date.now() - startTime,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<style scoped>
|
|
111
|
+
.form-section {
|
|
112
|
+
margin-bottom: 8px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.response-section {
|
|
116
|
+
margin-bottom: 16px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.section-title {
|
|
120
|
+
font-size: 14px;
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
color: #1f1f1f;
|
|
123
|
+
margin-bottom: 8px;
|
|
124
|
+
padding-left: 4px;
|
|
125
|
+
border-left: 3px solid #1890ff;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
:deep(.ant-card-head) {
|
|
129
|
+
background-color: #fafafa;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:deep(.ant-card-head-title) {
|
|
133
|
+
font-weight: 500;
|
|
134
|
+
padding: 4px 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
:deep(.ant-card-body) {
|
|
138
|
+
padding: 8px 12px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
:deep(.ant-descriptions-item-label) {
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import RequestForm from './components/RequestForm.vue';
|
|
2
|
+
import KeyValueInput from './components/KeyValueInput.vue';
|
|
3
|
+
import type { RequestSchema, KeyValuePair, ResponseData } from './types/request';
|
|
4
|
+
import { defaultRequestSchema } from './types/request';
|
|
5
|
+
import { executeRequest } from './utils/request';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
RequestForm,
|
|
9
|
+
KeyValueInput,
|
|
10
|
+
type RequestSchema,
|
|
11
|
+
type KeyValuePair,
|
|
12
|
+
type ResponseData,
|
|
13
|
+
defaultRequestSchema,
|
|
14
|
+
executeRequest
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default RequestForm;
|