whistle.mock-plugins 1.0.6 → 1.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/app/cgi-bin/interfaces.js +35 -0
- package/app/cgi-bin/test-interface.js +260 -262
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +8 -2
- package/lib/models.js +12 -1
- package/lib/routes/interfaces.js +274 -56
- package/lib/ruleManager.js +41 -2
- package/lib/rules-server.js +246 -139
- package/package.json +1 -1
- package/ui/src/components/interface-management/InterfaceTestModal.js +313 -0
- package/ui/src/components/interface-management/PreviewModal.js +50 -0
- package/ui/src/components/interface-management/README.md +184 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +427 -0
- package/ui/src/components/interface-management/constants.js +36 -0
- package/ui/src/components/interface-management/index.js +6 -0
- package/ui/src/components/interface-management/utils.js +103 -0
- package/ui/src/pages/InterfaceManagement.js +360 -653
- package/ui/src/pages/MockData.js +32 -0
|
@@ -1,70 +1,57 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { useParams, useHistory } from 'react-router-dom';
|
|
3
3
|
import AppLayout from '../components/AppLayout';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Table, Button, Modal, Form, Input, Select, message, Switch,
|
|
6
|
+
Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col
|
|
7
|
+
} from 'antd';
|
|
8
|
+
import {
|
|
9
|
+
PlusOutlined, EditOutlined, DeleteOutlined,
|
|
10
|
+
PlayCircleOutlined, FileTextOutlined, PlusCircleOutlined
|
|
11
|
+
} from '@ant-design/icons';
|
|
6
12
|
import '../styles/interface-management.css';
|
|
7
13
|
import axios from 'axios';
|
|
8
14
|
|
|
15
|
+
// 导入拆分后的组件
|
|
16
|
+
import {
|
|
17
|
+
ResponseContentEditor,
|
|
18
|
+
InterfaceTestModal,
|
|
19
|
+
PreviewModal,
|
|
20
|
+
contentTypes,
|
|
21
|
+
proxyTypes,
|
|
22
|
+
statusCodes,
|
|
23
|
+
httpMethods,
|
|
24
|
+
refreshCacheAfterUpdate,
|
|
25
|
+
formatResponseContent,
|
|
26
|
+
generateResponseId
|
|
27
|
+
} from '../components/interface-management';
|
|
28
|
+
|
|
9
29
|
const { Option } = Select;
|
|
10
|
-
const { TextArea } = Input;
|
|
11
|
-
const { TabPane } = Tabs;
|
|
12
30
|
|
|
13
31
|
const InterfaceManagement = () => {
|
|
14
32
|
const { featureId } = useParams();
|
|
15
33
|
const history = useHistory();
|
|
34
|
+
|
|
35
|
+
// 基础状态
|
|
16
36
|
const [features, setFeatures] = useState([]);
|
|
17
37
|
const [interfaces, setInterfaces] = useState([]);
|
|
18
38
|
const [loading, setLoading] = useState(false);
|
|
19
39
|
const [featuresLoading, setFeaturesLoading] = useState(false);
|
|
20
40
|
const [interfacesLoading, setInterfacesLoading] = useState(false);
|
|
41
|
+
const [selectedFeatureId, setSelectedFeatureId] = useState(null);
|
|
42
|
+
|
|
43
|
+
// 模态框状态
|
|
21
44
|
const [modalVisible, setModalVisible] = useState(false);
|
|
22
|
-
const [editingInterface, setEditingInterface] = useState(null);
|
|
23
45
|
const [testModalVisible, setTestModalVisible] = useState(false);
|
|
24
|
-
const [testUrl, setTestUrl] = useState('');
|
|
25
|
-
const [testResult, setTestResult] = useState(null);
|
|
26
|
-
const [testLoading, setTestLoading] = useState(false);
|
|
27
|
-
const [selectedFeatureId, setSelectedFeatureId] = useState(null);
|
|
28
|
-
const [form] = Form.useForm();
|
|
29
|
-
const [testForm] = Form.useForm();
|
|
30
|
-
const [previewContent, setPreviewContent] = useState(null);
|
|
31
46
|
const [previewVisible, setPreviewVisible] = useState(false);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const proxyTypes = [
|
|
42
|
-
{ value: 'response', label: '模拟响应' },
|
|
43
|
-
{ value: 'redirect', label: '重定向' },
|
|
44
|
-
{ value: 'url_redirect', label: 'URL重定向' },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const statusCodes = [
|
|
48
|
-
{ value: '200', label: '200 OK' },
|
|
49
|
-
{ value: '201', label: '201 Created' },
|
|
50
|
-
{ value: '204', label: '204 No Content' },
|
|
51
|
-
{ value: '400', label: '400 Bad Request' },
|
|
52
|
-
{ value: '401', label: '401 Unauthorized' },
|
|
53
|
-
{ value: '403', label: '403 Forbidden' },
|
|
54
|
-
{ value: '404', label: '404 Not Found' },
|
|
55
|
-
{ value: '500', label: '500 Internal Server Error' },
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
const httpMethods = [
|
|
59
|
-
{ value: 'ALL', label: '所有方法' },
|
|
60
|
-
{ value: 'GET', label: 'GET' },
|
|
61
|
-
{ value: 'POST', label: 'POST' },
|
|
62
|
-
{ value: 'PUT', label: 'PUT' },
|
|
63
|
-
{ value: 'DELETE', label: 'DELETE' },
|
|
64
|
-
{ value: 'PATCH', label: 'PATCH' },
|
|
65
|
-
{ value: 'HEAD', label: 'HEAD' },
|
|
66
|
-
{ value: 'OPTIONS', label: 'OPTIONS' },
|
|
67
|
-
];
|
|
47
|
+
|
|
48
|
+
// 编辑状态
|
|
49
|
+
const [editingInterface, setEditingInterface] = useState(null);
|
|
50
|
+
const [previewContent, setPreviewContent] = useState(null);
|
|
51
|
+
const [currentResponseId, setCurrentResponseId] = useState(null);
|
|
52
|
+
|
|
53
|
+
// 表单实例
|
|
54
|
+
const [form] = Form.useForm();
|
|
68
55
|
|
|
69
56
|
useEffect(() => {
|
|
70
57
|
fetchFeatures();
|
|
@@ -110,9 +97,38 @@ const InterfaceManagement = () => {
|
|
|
110
97
|
try {
|
|
111
98
|
setInterfacesLoading(true);
|
|
112
99
|
const response = await axios.get(`/cgi-bin/interfaces?featureId=${selectedFeatureId}`);
|
|
100
|
+
|
|
101
|
+
console.log('获取接口列表响应:', response.data);
|
|
102
|
+
|
|
113
103
|
if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
|
|
114
|
-
|
|
104
|
+
// 确保每个接口的响应数据格式正确
|
|
105
|
+
const processedInterfaces = response.data.data.map(item => {
|
|
106
|
+
console.log(`处理接口 ${item.name}:`, {
|
|
107
|
+
hasResponses: !!item.responses,
|
|
108
|
+
responsesLength: item.responses ? item.responses.length : 0,
|
|
109
|
+
activeResponseId: item.activeResponseId
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 确保 responses 是数组
|
|
113
|
+
if (!Array.isArray(item.responses)) {
|
|
114
|
+
item.responses = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 确保每个响应都有必要的字段
|
|
118
|
+
item.responses = item.responses.map((resp, index) => ({
|
|
119
|
+
id: resp.id || `resp-${index}-${Date.now()}`,
|
|
120
|
+
name: resp.name || `响应 ${index + 1}`,
|
|
121
|
+
description: resp.description || '',
|
|
122
|
+
content: resp.content || '{}'
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
return item;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
setInterfaces(processedInterfaces);
|
|
129
|
+
console.log('处理后的接口列表:', processedInterfaces);
|
|
115
130
|
} else {
|
|
131
|
+
console.warn('接口数据格式不正确:', response.data);
|
|
116
132
|
setInterfaces([]);
|
|
117
133
|
message.warning('获取接口配置数据格式不正确');
|
|
118
134
|
}
|
|
@@ -131,11 +147,35 @@ const InterfaceManagement = () => {
|
|
|
131
147
|
return;
|
|
132
148
|
}
|
|
133
149
|
form.resetFields();
|
|
150
|
+
|
|
151
|
+
// 创建默认响应
|
|
152
|
+
const defaultResponseId = generateResponseId();
|
|
153
|
+
const defaultResponses = [{
|
|
154
|
+
id: defaultResponseId,
|
|
155
|
+
name: '默认响应',
|
|
156
|
+
description: '',
|
|
157
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
158
|
+
}];
|
|
159
|
+
|
|
160
|
+
console.log('创建新接口,初始化响应数据:', defaultResponses);
|
|
161
|
+
|
|
162
|
+
form.setFieldsValue({
|
|
163
|
+
proxyType: 'response',
|
|
164
|
+
statusCode: '200',
|
|
165
|
+
contentType: 'application/json; charset=utf-8',
|
|
166
|
+
responses: defaultResponses,
|
|
167
|
+
activeResponseId: defaultResponseId,
|
|
168
|
+
httpMethod: 'ALL',
|
|
169
|
+
headerItems: [] // 初始化为空数组
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
setCurrentResponseId(defaultResponseId);
|
|
134
173
|
setEditingInterface(null);
|
|
135
174
|
setModalVisible(true);
|
|
136
175
|
};
|
|
137
176
|
|
|
138
177
|
const handleEditInterface = (record) => {
|
|
178
|
+
console.log('开始编辑接口:', record.name, '原始数据:', record);
|
|
139
179
|
setEditingInterface(record);
|
|
140
180
|
|
|
141
181
|
// 将自定义请求头转换为数组格式,用于动态表单项
|
|
@@ -146,6 +186,42 @@ const InterfaceManagement = () => {
|
|
|
146
186
|
headerValue: value
|
|
147
187
|
}));
|
|
148
188
|
}
|
|
189
|
+
|
|
190
|
+
// 处理多响应数据
|
|
191
|
+
let initialResponses = [];
|
|
192
|
+
let initialResponseId = null;
|
|
193
|
+
|
|
194
|
+
// 深拷贝记录中的响应数据,避免引用问题
|
|
195
|
+
if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
|
|
196
|
+
console.log('使用现有响应数据, 数量:', record.responses.length);
|
|
197
|
+
|
|
198
|
+
initialResponses = JSON.parse(JSON.stringify(record.responses));
|
|
199
|
+
initialResponseId = record.activeResponseId || record.responses[0].id;
|
|
200
|
+
|
|
201
|
+
// 确保每个响应都有名称
|
|
202
|
+
initialResponses = initialResponses.map((resp, index) => ({
|
|
203
|
+
id: resp.id || generateResponseId(),
|
|
204
|
+
name: resp.name || `响应 ${index + 1}`,
|
|
205
|
+
description: resp.description || '',
|
|
206
|
+
content: resp.content || '{}'
|
|
207
|
+
}));
|
|
208
|
+
} else if (record.responseContent) {
|
|
209
|
+
// 向后兼容:如果只有传统的 responseContent 字段,创建一个默认响应
|
|
210
|
+
console.log('创建默认响应,使用responseContent字段');
|
|
211
|
+
const defaultResponseId = generateResponseId();
|
|
212
|
+
initialResponses = [
|
|
213
|
+
{
|
|
214
|
+
id: defaultResponseId,
|
|
215
|
+
name: '默认响应',
|
|
216
|
+
description: '',
|
|
217
|
+
content: record.responseContent
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
initialResponseId = defaultResponseId;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log('编辑接口,设置响应数据:', initialResponses);
|
|
224
|
+
setCurrentResponseId(initialResponseId);
|
|
149
225
|
|
|
150
226
|
form.setFieldsValue({
|
|
151
227
|
name: record.name,
|
|
@@ -153,7 +229,8 @@ const InterfaceManagement = () => {
|
|
|
153
229
|
proxyType: record.proxyType || 'response',
|
|
154
230
|
statusCode: record.httpStatus?.toString() || '200',
|
|
155
231
|
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
156
|
-
|
|
232
|
+
responses: initialResponses,
|
|
233
|
+
activeResponseId: initialResponseId,
|
|
157
234
|
httpMethod: record.httpMethod || 'ALL',
|
|
158
235
|
targetUrl: record.targetUrl || '',
|
|
159
236
|
headerItems: headersArray // 使用数组存储表单项
|
|
@@ -167,6 +244,9 @@ const InterfaceManagement = () => {
|
|
|
167
244
|
if (response.data && response.data.code === 0) {
|
|
168
245
|
message.success('接口删除成功');
|
|
169
246
|
fetchInterfaces();
|
|
247
|
+
|
|
248
|
+
// 刷新规则缓存
|
|
249
|
+
refreshCacheAfterUpdate();
|
|
170
250
|
} else {
|
|
171
251
|
throw new Error(response.data?.message || '接口删除失败');
|
|
172
252
|
}
|
|
@@ -193,41 +273,93 @@ const InterfaceManagement = () => {
|
|
|
193
273
|
}
|
|
194
274
|
};
|
|
195
275
|
|
|
196
|
-
const cleanJsonResponse = (jsonStr) => {
|
|
197
|
-
try {
|
|
198
|
-
const parsed = JSON.parse(jsonStr);
|
|
199
|
-
return JSON.stringify(parsed);
|
|
200
|
-
} catch (e) {
|
|
201
|
-
return jsonStr;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// 验证URL是否合法
|
|
206
|
-
const isValidUrl = (url) => {
|
|
207
|
-
if (!url) return false;
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
new URL(url);
|
|
211
|
-
return true;
|
|
212
|
-
} catch (e) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
|
|
217
276
|
const handleSubmit = async () => {
|
|
218
277
|
try {
|
|
219
278
|
const values = await form.validateFields();
|
|
279
|
+
console.log('=== 开始表单提交 ===');
|
|
280
|
+
console.log('表单提交,原始值:', JSON.stringify(values, null, 2));
|
|
220
281
|
|
|
221
|
-
//
|
|
222
|
-
|
|
282
|
+
// 获取当前激活的响应ID和所有响应
|
|
283
|
+
const activeResponseId = values.activeResponseId;
|
|
284
|
+
let responses = values.responses || [];
|
|
285
|
+
|
|
286
|
+
console.log('提取的原始响应数据:', responses);
|
|
287
|
+
console.log('响应数据类型:', typeof responses);
|
|
288
|
+
console.log('是否为数组:', Array.isArray(responses));
|
|
289
|
+
|
|
290
|
+
// 确保responses是数组格式
|
|
291
|
+
if (!Array.isArray(responses)) {
|
|
292
|
+
console.log('响应数据不是数组,尝试解析...');
|
|
223
293
|
try {
|
|
224
|
-
|
|
294
|
+
if (typeof responses === 'string') {
|
|
295
|
+
responses = JSON.parse(responses);
|
|
296
|
+
if (!Array.isArray(responses)) {
|
|
297
|
+
responses = [];
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
responses = [];
|
|
301
|
+
}
|
|
225
302
|
} catch (e) {
|
|
226
|
-
|
|
227
|
-
|
|
303
|
+
console.error('解析响应数据失败:', e);
|
|
304
|
+
responses = [];
|
|
228
305
|
}
|
|
229
306
|
}
|
|
230
307
|
|
|
308
|
+
console.log('处理前的响应数据:', JSON.stringify(responses, null, 2));
|
|
309
|
+
|
|
310
|
+
// 确保每个响应都有名称、描述和内容
|
|
311
|
+
const cleanedResponses = responses.map((resp, index) => {
|
|
312
|
+
console.log(`处理响应 ${index}:`, resp);
|
|
313
|
+
const cleaned = {
|
|
314
|
+
id: resp.id || generateResponseId(),
|
|
315
|
+
name: resp.name || `响应 ${index + 1}`,
|
|
316
|
+
description: resp.description || '',
|
|
317
|
+
content: resp.content || '{}'
|
|
318
|
+
};
|
|
319
|
+
console.log(`处理后的响应 ${index}:`, cleaned);
|
|
320
|
+
return cleaned;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// 如果没有响应,创建一个默认响应
|
|
324
|
+
if (cleanedResponses.length === 0) {
|
|
325
|
+
console.log('没有响应数据,创建默认响应');
|
|
326
|
+
const defaultId = generateResponseId();
|
|
327
|
+
cleanedResponses.push({
|
|
328
|
+
id: defaultId,
|
|
329
|
+
name: '默认响应',
|
|
330
|
+
description: '',
|
|
331
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
332
|
+
});
|
|
333
|
+
setCurrentResponseId(defaultId);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 确保有一个激活的响应ID
|
|
337
|
+
const validActiveResponseId = activeResponseId && cleanedResponses.some(r => r.id === activeResponseId)
|
|
338
|
+
? activeResponseId
|
|
339
|
+
: cleanedResponses[0].id;
|
|
340
|
+
|
|
341
|
+
// 获取激活的响应
|
|
342
|
+
const activeResponse = cleanedResponses.find(r => r.id === validActiveResponseId) || cleanedResponses[0];
|
|
343
|
+
|
|
344
|
+
console.log('=== 清理后的响应数据 ===');
|
|
345
|
+
console.log('清理后的响应数据:', JSON.stringify(cleanedResponses, null, 2));
|
|
346
|
+
console.log('激活的响应ID:', validActiveResponseId);
|
|
347
|
+
console.log('激活的响应:', activeResponse);
|
|
348
|
+
|
|
349
|
+
// 检查responseBody是否是有效的JSON
|
|
350
|
+
if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
|
|
351
|
+
cleanedResponses.forEach(response => {
|
|
352
|
+
try {
|
|
353
|
+
if (response.content) {
|
|
354
|
+
JSON.parse(response.content);
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
message.error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
358
|
+
throw new Error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
231
363
|
// 处理自定义请求头,将表单项数组转换为对象格式
|
|
232
364
|
let customHeaders = {};
|
|
233
365
|
|
|
@@ -246,7 +378,11 @@ const InterfaceManagement = () => {
|
|
|
246
378
|
featureId: selectedFeatureId,
|
|
247
379
|
urlPattern: values.pattern,
|
|
248
380
|
proxyType: values.proxyType,
|
|
249
|
-
|
|
381
|
+
// 同时保存所有响应和当前活跃的响应
|
|
382
|
+
responses: cleanedResponses,
|
|
383
|
+
activeResponseId: validActiveResponseId,
|
|
384
|
+
// 兼容性保留:将当前活跃响应的内容保存到 responseContent
|
|
385
|
+
responseContent: activeResponse ? activeResponse.content : '',
|
|
250
386
|
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
251
387
|
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
252
388
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
@@ -256,30 +392,41 @@ const InterfaceManagement = () => {
|
|
|
256
392
|
active: true
|
|
257
393
|
};
|
|
258
394
|
|
|
395
|
+
console.log('=== 最终提交的接口数据 ===');
|
|
396
|
+
console.log('提交的接口数据:', JSON.stringify(interfaceData, null, 2));
|
|
397
|
+
|
|
398
|
+
let response;
|
|
259
399
|
if (editingInterface) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
} else {
|
|
264
|
-
throw new Error(response.data?.message || '接口更新失败');
|
|
265
|
-
}
|
|
400
|
+
// 更新现有接口
|
|
401
|
+
console.log('执行接口更新,ID:', editingInterface.id);
|
|
402
|
+
response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
|
|
266
403
|
} else {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
404
|
+
// 创建新接口
|
|
405
|
+
console.log('执行接口创建');
|
|
406
|
+
response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log('服务器响应:', response.data);
|
|
410
|
+
|
|
411
|
+
if (response.data && response.data.code === 0) {
|
|
412
|
+
console.log('=== 接口操作成功 ===');
|
|
413
|
+
message.success(editingInterface ? '接口更新成功' : '接口创建成功');
|
|
414
|
+
setModalVisible(false);
|
|
415
|
+
fetchInterfaces();
|
|
416
|
+
|
|
417
|
+
// 刷新规则缓存
|
|
418
|
+
refreshCacheAfterUpdate();
|
|
419
|
+
} else {
|
|
420
|
+
throw new Error(response.data?.message || '操作失败');
|
|
273
421
|
}
|
|
274
|
-
|
|
275
|
-
setModalVisible(false);
|
|
276
|
-
fetchInterfaces();
|
|
277
422
|
} catch (error) {
|
|
278
|
-
if (error.
|
|
279
|
-
|
|
423
|
+
if (error.message && error.message.includes('JSON格式')) {
|
|
424
|
+
// 已经显示了错误信息,不需要再显示
|
|
425
|
+
return;
|
|
280
426
|
}
|
|
427
|
+
console.error('=== 接口操作失败 ===');
|
|
281
428
|
console.error('操作失败:', error);
|
|
282
|
-
message.error(error.response?.data?.message || error.message || '
|
|
429
|
+
message.error(error.response?.data?.message || error.message || '操作失败');
|
|
283
430
|
}
|
|
284
431
|
};
|
|
285
432
|
|
|
@@ -287,173 +434,43 @@ const InterfaceManagement = () => {
|
|
|
287
434
|
setModalVisible(false);
|
|
288
435
|
};
|
|
289
436
|
|
|
290
|
-
const handleTestInterface = (record) => {
|
|
437
|
+
const handleTestInterface = async (record) => {
|
|
291
438
|
setEditingInterface(record);
|
|
292
|
-
setTestUrl('');
|
|
293
|
-
setTestResult(null);
|
|
294
439
|
setTestModalVisible(true);
|
|
295
|
-
testForm.resetFields();
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
// 检查 URL 是否匹配模式
|
|
299
|
-
const isUrlMatchPattern = (url, pattern, proxyType) => {
|
|
300
|
-
if (!url || !pattern) return false;
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
// 对于url_redirect类型,需要完全匹配
|
|
304
|
-
if (proxyType === 'url_redirect') {
|
|
305
|
-
return url === pattern;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
309
|
-
if (proxyType === 'redirect') {
|
|
310
|
-
return url.indexOf(pattern) === 0;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// 以下是默认的匹配逻辑(用于response类型等)
|
|
314
|
-
// 如果是正则表达式
|
|
315
|
-
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
316
|
-
const regex = new RegExp(pattern.slice(1, -1));
|
|
317
|
-
return regex.test(url);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 如果是通配符模式
|
|
321
|
-
if (pattern.includes('*')) {
|
|
322
|
-
const regexPattern = pattern
|
|
323
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
324
|
-
.replace(/\*/g, '.*');
|
|
325
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
326
|
-
return regex.test(url);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 精确匹配
|
|
330
|
-
return url === pattern;
|
|
331
|
-
} catch (e) {
|
|
332
|
-
console.error('URL匹配检查失败:', e);
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const handleTestSubmit = async () => {
|
|
338
|
-
try {
|
|
339
|
-
const values = await testForm.validateFields();
|
|
340
|
-
setTestLoading(true);
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
// 验证测试 URL 是否匹配当前接口的匹配规则
|
|
344
|
-
if (!isUrlMatchPattern(values.testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
|
|
345
|
-
let errorMessage = '测试URL与接口匹配规则不匹配';
|
|
346
|
-
|
|
347
|
-
// 根据不同的代理类型提供更具体的错误信息
|
|
348
|
-
if (editingInterface.proxyType === 'url_redirect') {
|
|
349
|
-
errorMessage = `测试URL必须完全匹配 ${editingInterface.urlPattern}`;
|
|
350
|
-
} else if (editingInterface.proxyType === 'redirect') {
|
|
351
|
-
errorMessage = `测试URL必须以 ${editingInterface.urlPattern} 开头`;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
setTestResult({
|
|
355
|
-
success: false,
|
|
356
|
-
error: errorMessage
|
|
357
|
-
});
|
|
358
|
-
message.error(errorMessage);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 调用实际测试接口
|
|
363
|
-
const response = await axios.post(`/cgi-bin/test-interface`, {
|
|
364
|
-
url: values.testUrl,
|
|
365
|
-
interfaceId: editingInterface.id
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
if (response.data && response.data.code === 0 && response.data.data) {
|
|
369
|
-
setTestResult({
|
|
370
|
-
success: true,
|
|
371
|
-
statusCode: response.data.data.statusCode,
|
|
372
|
-
contentType: response.data.data.contentType,
|
|
373
|
-
responseBody: response.data.data.responseBody,
|
|
374
|
-
matchedRule: editingInterface.urlPattern,
|
|
375
|
-
httpMethod: editingInterface.httpMethod,
|
|
376
|
-
requestUrl: values.testUrl,
|
|
377
|
-
mockInfo: response.data.data.mockInfo
|
|
378
|
-
});
|
|
379
|
-
message.success('测试成功');
|
|
380
|
-
} else {
|
|
381
|
-
throw new Error(response.data?.message || '测试响应格式不正确');
|
|
382
|
-
}
|
|
383
|
-
} catch (error) {
|
|
384
|
-
setTestResult({
|
|
385
|
-
success: false,
|
|
386
|
-
error: error.response?.data?.message || error.message || '测试失败'
|
|
387
|
-
});
|
|
388
|
-
message.error(error.response?.data?.message || error.message || '测试失败');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
setTestLoading(false);
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (error.errorFields) {
|
|
394
|
-
return; // 表单验证错误
|
|
395
|
-
}
|
|
396
|
-
setTestLoading(false);
|
|
397
|
-
}
|
|
398
440
|
};
|
|
399
441
|
|
|
400
442
|
const handleSelectFeature = (featureId) => {
|
|
401
443
|
setSelectedFeatureId(featureId);
|
|
402
444
|
};
|
|
403
445
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return content;
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const formatJsonContent = () => {
|
|
417
|
-
const proxyType = form.getFieldValue('proxyType');
|
|
418
|
-
if (proxyType !== 'response') {
|
|
446
|
+
// 预览响应内容
|
|
447
|
+
const handlePreview = () => {
|
|
448
|
+
// 获取当前选中的响应内容
|
|
449
|
+
const responses = form.getFieldValue('responses') || [];
|
|
450
|
+
const activeResponseId = form.getFieldValue('activeResponseId');
|
|
451
|
+
const activeResponse = responses.find(r => r.id === activeResponseId);
|
|
452
|
+
|
|
453
|
+
if (!activeResponse) {
|
|
454
|
+
message.error('未找到有效的响应内容');
|
|
419
455
|
return;
|
|
420
456
|
}
|
|
457
|
+
|
|
458
|
+
// 根据内容类型格式化响应内容
|
|
459
|
+
const contentType = form.getFieldValue('contentType') || '';
|
|
460
|
+
const formattedContent = formatResponseContent(activeResponse.content, contentType);
|
|
421
461
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
}
|
|
462
|
+
// 设置预览内容,包含响应名称
|
|
463
|
+
setPreviewContent({
|
|
464
|
+
title: `预览: ${activeResponse.name || '未命名响应'}`,
|
|
465
|
+
content: formattedContent,
|
|
466
|
+
description: '',
|
|
467
|
+
contentType
|
|
468
|
+
});
|
|
469
|
+
setPreviewVisible(true);
|
|
432
470
|
};
|
|
433
471
|
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
if (proxyType !== 'response') {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const responseBody = form.getFieldValue('responseBody');
|
|
441
|
-
if (responseBody) {
|
|
442
|
-
try {
|
|
443
|
-
// 如果是JSON,格式化显示
|
|
444
|
-
const contentType = form.getFieldValue('contentType');
|
|
445
|
-
if (contentType && contentType.includes('json')) {
|
|
446
|
-
setPreviewContent(JSON.stringify(JSON.parse(responseBody), null, 2));
|
|
447
|
-
} else {
|
|
448
|
-
setPreviewContent(responseBody);
|
|
449
|
-
}
|
|
450
|
-
setPreviewVisible(true);
|
|
451
|
-
} catch (error) {
|
|
452
|
-
message.error('内容格式不正确,无法预览');
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
message.warning('响应内容为空,无法预览');
|
|
456
|
-
}
|
|
472
|
+
const handleResponseSelect = (responseId) => {
|
|
473
|
+
setCurrentResponseId(responseId);
|
|
457
474
|
};
|
|
458
475
|
|
|
459
476
|
const filteredInterfaces = interfaces.filter(item =>
|
|
@@ -638,6 +655,7 @@ const InterfaceManagement = () => {
|
|
|
638
655
|
添加接口
|
|
639
656
|
</Button>
|
|
640
657
|
</div>
|
|
658
|
+
|
|
641
659
|
{!features.length && (
|
|
642
660
|
<Alert
|
|
643
661
|
message="未找到功能模块"
|
|
@@ -669,6 +687,7 @@ const InterfaceManagement = () => {
|
|
|
669
687
|
/>
|
|
670
688
|
</div>
|
|
671
689
|
|
|
690
|
+
{/* 接口编辑/创建模态框 */}
|
|
672
691
|
<Modal
|
|
673
692
|
title={editingInterface ? '编辑接口' : '添加接口'}
|
|
674
693
|
open={modalVisible}
|
|
@@ -689,12 +708,13 @@ const InterfaceManagement = () => {
|
|
|
689
708
|
proxyType: 'response',
|
|
690
709
|
statusCode: '200',
|
|
691
710
|
contentType: 'application/json; charset=utf-8',
|
|
692
|
-
|
|
711
|
+
responses: [],
|
|
693
712
|
httpMethod: 'ALL',
|
|
694
713
|
targetUrl: '',
|
|
695
714
|
headerItems: []
|
|
696
715
|
}}
|
|
697
716
|
>
|
|
717
|
+
{/* 基础信息表单项保持原样 */}
|
|
698
718
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
699
719
|
<div style={{ flex: 1 }}>
|
|
700
720
|
<Form.Item
|
|
@@ -845,7 +865,7 @@ const InterfaceManagement = () => {
|
|
|
845
865
|
|
|
846
866
|
{/* 根据proxyType显示不同的表单项 */}
|
|
847
867
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
848
|
-
{({ getFieldValue }) => {
|
|
868
|
+
{({ getFieldValue, setFieldsValue }) => {
|
|
849
869
|
const proxyType = getFieldValue('proxyType');
|
|
850
870
|
|
|
851
871
|
if (proxyType === 'response') {
|
|
@@ -880,43 +900,48 @@ const InterfaceManagement = () => {
|
|
|
880
900
|
</div>
|
|
881
901
|
</div>
|
|
882
902
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
label={
|
|
889
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
890
|
-
<span>响应内容</span>
|
|
891
|
-
<Space>
|
|
892
|
-
<Button
|
|
893
|
-
type="link"
|
|
894
|
-
icon={<EyeOutlined />}
|
|
895
|
-
onClick={handlePreview}
|
|
896
|
-
style={{ padding: 0 }}
|
|
897
|
-
>
|
|
898
|
-
预览
|
|
899
|
-
</Button>
|
|
900
|
-
<Button
|
|
901
|
-
type="link"
|
|
902
|
-
icon={<FormatPainterOutlined />}
|
|
903
|
-
onClick={formatJsonContent}
|
|
904
|
-
style={{ padding: 0 }}
|
|
905
|
-
>
|
|
906
|
-
格式化JSON
|
|
907
|
-
</Button>
|
|
908
|
-
</Space>
|
|
909
|
-
</div>
|
|
910
|
-
}
|
|
911
|
-
rules={[{ required: true, message: '请输入响应内容' }]}
|
|
903
|
+
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
904
|
+
<Form.Item
|
|
905
|
+
name="responses"
|
|
906
|
+
initialValue={[]}
|
|
907
|
+
hidden
|
|
912
908
|
>
|
|
913
|
-
<
|
|
909
|
+
<Input />
|
|
910
|
+
</Form.Item>
|
|
911
|
+
<Form.Item name="activeResponseId" hidden>
|
|
912
|
+
<Input />
|
|
913
|
+
</Form.Item>
|
|
914
|
+
|
|
915
|
+
{/* 使用新的合并组件 */}
|
|
916
|
+
<Form.Item noStyle shouldUpdate>
|
|
917
|
+
{({ getFieldValue }) => {
|
|
918
|
+
let responses = getFieldValue('responses') || [];
|
|
919
|
+
const activeResponseId = getFieldValue('activeResponseId');
|
|
920
|
+
|
|
921
|
+
// 确保是数组
|
|
922
|
+
if (!Array.isArray(responses)) {
|
|
923
|
+
try {
|
|
924
|
+
if (typeof responses === 'string') {
|
|
925
|
+
responses = JSON.parse(responses);
|
|
926
|
+
}
|
|
927
|
+
} catch (e) {
|
|
928
|
+
responses = [];
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return (
|
|
933
|
+
<ResponseContentEditor
|
|
934
|
+
form={form}
|
|
935
|
+
responses={responses}
|
|
936
|
+
activeResponseId={activeResponseId}
|
|
937
|
+
onPreview={handlePreview}
|
|
938
|
+
/>
|
|
939
|
+
);
|
|
940
|
+
}}
|
|
914
941
|
</Form.Item>
|
|
915
942
|
</>
|
|
916
943
|
);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
944
|
+
} else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
920
945
|
return (
|
|
921
946
|
<>
|
|
922
947
|
<Form.Item
|
|
@@ -924,172 +949,64 @@ const InterfaceManagement = () => {
|
|
|
924
949
|
label="重定向目标URL"
|
|
925
950
|
rules={[
|
|
926
951
|
{ required: true, message: '请输入重定向目标URL' },
|
|
927
|
-
{
|
|
928
|
-
validator
|
|
952
|
+
{
|
|
953
|
+
validator(_, value) {
|
|
929
954
|
if (!value) return Promise.resolve();
|
|
930
|
-
|
|
931
955
|
try {
|
|
932
956
|
new URL(value);
|
|
933
957
|
return Promise.resolve();
|
|
934
958
|
} catch (e) {
|
|
935
|
-
return Promise.reject(new Error('请输入有效的URL
|
|
959
|
+
return Promise.reject(new Error('请输入有效的URL格式(包括http://或https://)'));
|
|
936
960
|
}
|
|
937
961
|
}
|
|
938
962
|
}
|
|
939
963
|
]}
|
|
940
|
-
tooltip=
|
|
941
|
-
"重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
|
|
942
|
-
"URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
|
|
943
|
-
}
|
|
964
|
+
tooltip="重定向的目标URL,必须是完整的URL格式"
|
|
944
965
|
>
|
|
945
966
|
<Input
|
|
946
|
-
placeholder=
|
|
947
|
-
"例如:https://example.com/api" :
|
|
948
|
-
"例如:https://example.com/api/users"
|
|
949
|
-
}
|
|
950
|
-
addonBefore={
|
|
951
|
-
<Select
|
|
952
|
-
defaultValue="https://"
|
|
953
|
-
className="select-before"
|
|
954
|
-
style={{ width: 100 }}
|
|
955
|
-
onChange={(value) => {
|
|
956
|
-
const currentUrl = form.getFieldValue('targetUrl') || '';
|
|
957
|
-
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
|
|
958
|
-
form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
|
|
959
|
-
}}
|
|
960
|
-
>
|
|
961
|
-
<Option value="http://">http://</Option>
|
|
962
|
-
<Option value="https://">https://</Option>
|
|
963
|
-
</Select>
|
|
964
|
-
}
|
|
967
|
+
placeholder="例如:https://api.example.com/users"
|
|
965
968
|
/>
|
|
966
969
|
</Form.Item>
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
tooltip="为请求添加或替换HTTP请求头"
|
|
971
|
-
className="custom-headers-container"
|
|
972
|
-
>
|
|
970
|
+
|
|
971
|
+
{/* 自定义请求头设置 */}
|
|
972
|
+
<Form.Item label="自定义请求头">
|
|
973
973
|
<Form.List name="headerItems">
|
|
974
974
|
{(fields, { add, remove }) => (
|
|
975
975
|
<>
|
|
976
976
|
{fields.map(({ key, name, ...restField }) => (
|
|
977
|
-
<
|
|
978
|
-
<
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
noStyle
|
|
1002
|
-
rules={[
|
|
1003
|
-
{
|
|
1004
|
-
validator: (_, value) => {
|
|
1005
|
-
if (!value) return Promise.resolve();
|
|
1006
|
-
|
|
1007
|
-
// 检查是否是随机数格式 @xxxx-xxx
|
|
1008
|
-
if (value.startsWith('@')) {
|
|
1009
|
-
const randomPattern = value.substring(1);
|
|
1010
|
-
// 验证格式: 只能包含x和-,且首尾不能是-
|
|
1011
|
-
if (!/^[x][-x]*[x]$/.test(randomPattern) ||
|
|
1012
|
-
randomPattern.startsWith('-') ||
|
|
1013
|
-
randomPattern.endsWith('-')) {
|
|
1014
|
-
return Promise.reject(
|
|
1015
|
-
new Error('随机数格式错误,应为@xxxx-xxx格式,只能包含x和-,且首尾不能是-')
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return Promise.resolve();
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
]}
|
|
1023
|
-
>
|
|
1024
|
-
<Input placeholder="Header-Value" />
|
|
1025
|
-
</Form.Item>
|
|
1026
|
-
</Col>
|
|
1027
|
-
<Col span={2}>
|
|
1028
|
-
<Button
|
|
1029
|
-
type="text"
|
|
1030
|
-
icon={<MinusCircleOutlined />}
|
|
1031
|
-
onClick={() => remove(name)}
|
|
1032
|
-
style={{ color: '#ff4d4f' }}
|
|
1033
|
-
/>
|
|
1034
|
-
</Col>
|
|
1035
|
-
</Row>
|
|
977
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
978
|
+
<Form.Item
|
|
979
|
+
{...restField}
|
|
980
|
+
name={[name, 'headerName']}
|
|
981
|
+
rules={[{ required: true, message: '请输入请求头名称' }]}
|
|
982
|
+
style={{ flex: 1, margin: 0 }}
|
|
983
|
+
>
|
|
984
|
+
<Input placeholder="请求头名称(如:Authorization)" />
|
|
985
|
+
</Form.Item>
|
|
986
|
+
<Form.Item
|
|
987
|
+
{...restField}
|
|
988
|
+
name={[name, 'headerValue']}
|
|
989
|
+
rules={[{ required: true, message: '请输入请求头值' }]}
|
|
990
|
+
style={{ flex: 1, margin: 0 }}
|
|
991
|
+
>
|
|
992
|
+
<Input placeholder="请求头值(如:Bearer token123)" />
|
|
993
|
+
</Form.Item>
|
|
994
|
+
<Button
|
|
995
|
+
type="text"
|
|
996
|
+
danger
|
|
997
|
+
icon={<DeleteOutlined />}
|
|
998
|
+
onClick={() => remove(name)}
|
|
999
|
+
/>
|
|
1000
|
+
</div>
|
|
1036
1001
|
))}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
添加请求头
|
|
1046
|
-
</Button>
|
|
1047
|
-
</Form.Item>
|
|
1048
|
-
|
|
1049
|
-
<div style={{ marginTop: 8 }}>
|
|
1050
|
-
<Button
|
|
1051
|
-
type="link"
|
|
1052
|
-
onClick={() => {
|
|
1053
|
-
add({ headerName: 'Content-Type', headerValue: 'application/json' });
|
|
1054
|
-
}}
|
|
1055
|
-
>
|
|
1056
|
-
添加 Content-Type
|
|
1057
|
-
</Button>
|
|
1058
|
-
<Button
|
|
1059
|
-
type="link"
|
|
1060
|
-
onClick={() => {
|
|
1061
|
-
add({ headerName: 'Authorization', headerValue: 'Bearer ' });
|
|
1062
|
-
}}
|
|
1063
|
-
>
|
|
1064
|
-
添加 Authorization
|
|
1065
|
-
</Button>
|
|
1066
|
-
<Button
|
|
1067
|
-
type="link"
|
|
1068
|
-
onClick={() => {
|
|
1069
|
-
add({ headerName: 'User-Agent', headerValue: 'Mozilla/5.0' });
|
|
1070
|
-
}}
|
|
1071
|
-
>
|
|
1072
|
-
添加 User-Agent
|
|
1073
|
-
</Button>
|
|
1074
|
-
<Button
|
|
1075
|
-
type="link"
|
|
1076
|
-
onClick={() => {
|
|
1077
|
-
add({ headerName: 'X-Random-ID', headerValue: '@xxxx-xxxx' });
|
|
1078
|
-
}}
|
|
1079
|
-
>
|
|
1080
|
-
添加随机ID
|
|
1081
|
-
</Button>
|
|
1082
|
-
</div>
|
|
1083
|
-
|
|
1084
|
-
<div style={{ marginTop: 8, background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
|
|
1085
|
-
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>随机数格式说明:</div>
|
|
1086
|
-
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
|
1087
|
-
<li>使用 <code>@</code> 开头表示这是一个随机数值</li>
|
|
1088
|
-
<li>格式示例:<code>@xxxx-xxxx</code> 将生成如 <code>a1b2-c3d4</code> 的随机值</li>
|
|
1089
|
-
<li>每个 <code>x</code> 将替换为随机字母或数字</li>
|
|
1090
|
-
<li><code>-</code> 将保留在输出中作为分隔符</li>
|
|
1091
|
-
</ul>
|
|
1092
|
-
</div>
|
|
1002
|
+
<Button
|
|
1003
|
+
type="dashed"
|
|
1004
|
+
onClick={() => add()}
|
|
1005
|
+
icon={<PlusOutlined />}
|
|
1006
|
+
style={{ width: '100%', marginTop: '8px' }}
|
|
1007
|
+
>
|
|
1008
|
+
添加请求头
|
|
1009
|
+
</Button>
|
|
1093
1010
|
</>
|
|
1094
1011
|
)}
|
|
1095
1012
|
</Form.List>
|
|
@@ -1104,229 +1021,19 @@ const InterfaceManagement = () => {
|
|
|
1104
1021
|
</Form>
|
|
1105
1022
|
</Modal>
|
|
1106
1023
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1024
|
+
{/* 使用拆分后的测试模态框组件 */}
|
|
1025
|
+
<InterfaceTestModal
|
|
1026
|
+
visible={testModalVisible}
|
|
1110
1027
|
onCancel={() => setTestModalVisible(false)}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
关闭
|
|
1114
|
-
</Button>,
|
|
1115
|
-
<Button key="submit" type="primary" loading={testLoading} onClick={handleTestSubmit}>
|
|
1116
|
-
测试
|
|
1117
|
-
</Button>
|
|
1118
|
-
]}
|
|
1119
|
-
width={800}
|
|
1120
|
-
>
|
|
1121
|
-
{editingInterface && (
|
|
1122
|
-
<div className="test-interface-container">
|
|
1123
|
-
<Alert
|
|
1124
|
-
message="URL匹配规则说明"
|
|
1125
|
-
description={
|
|
1126
|
-
<div>
|
|
1127
|
-
<p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
|
|
1128
|
-
<p>代理类型:{
|
|
1129
|
-
proxyTypes.find(item => item.value === editingInterface.proxyType)?.label ||
|
|
1130
|
-
editingInterface.proxyType || '模拟响应'
|
|
1131
|
-
}</p>
|
|
1132
|
-
|
|
1133
|
-
{editingInterface.proxyType === 'url_redirect' && (
|
|
1134
|
-
<div>
|
|
1135
|
-
<p><b>URL重定向模式规则:</b></p>
|
|
1136
|
-
<ul>
|
|
1137
|
-
<li>URL匹配规则必须是完整URL(包括http://或https://)</li>
|
|
1138
|
-
<li>需要完全匹配URL路径,测试URL必须与匹配规则<b>完全一致</b></li>
|
|
1139
|
-
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
1140
|
-
</ul>
|
|
1141
|
-
</div>
|
|
1142
|
-
)}
|
|
1143
|
-
|
|
1144
|
-
{editingInterface.proxyType === 'redirect' && (
|
|
1145
|
-
<div>
|
|
1146
|
-
<p><b>重定向模式规则:</b></p>
|
|
1147
|
-
<ul>
|
|
1148
|
-
<li>URL匹配规则必须以http://或https://开头</li>
|
|
1149
|
-
<li>测试URL必须以匹配规则开头(<b>前缀匹配</b>)</li>
|
|
1150
|
-
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
1151
|
-
</ul>
|
|
1152
|
-
</div>
|
|
1153
|
-
)}
|
|
1154
|
-
|
|
1155
|
-
{editingInterface.proxyType === 'response' && (
|
|
1156
|
-
<div>
|
|
1157
|
-
<p>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugins://,然后插件会根据接口的匹配规则处理请求。</p>
|
|
1158
|
-
<p>插件支持以下匹配方式:</p>
|
|
1159
|
-
<ul>
|
|
1160
|
-
<li>精确匹配:完全匹配URL路径部分</li>
|
|
1161
|
-
<li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
|
|
1162
|
-
<li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
|
|
1163
|
-
</ul>
|
|
1164
|
-
</div>
|
|
1165
|
-
)}
|
|
1166
|
-
|
|
1167
|
-
<p>请在下方输入完整测试URL进行测试</p>
|
|
1168
|
-
</div>
|
|
1169
|
-
}
|
|
1170
|
-
type="info"
|
|
1171
|
-
showIcon
|
|
1172
|
-
style={{ marginBottom: 16 }}
|
|
1173
|
-
/>
|
|
1174
|
-
|
|
1175
|
-
<Form
|
|
1176
|
-
form={testForm}
|
|
1177
|
-
layout="vertical"
|
|
1178
|
-
>
|
|
1179
|
-
<Form.Item
|
|
1180
|
-
name="testUrl"
|
|
1181
|
-
label="测试URL"
|
|
1182
|
-
rules={[{ required: true, message: '请输入测试URL' }]}
|
|
1183
|
-
>
|
|
1184
|
-
<Input
|
|
1185
|
-
placeholder="输入完整URL,例如:https://example.com/api/users"
|
|
1186
|
-
onChange={(e) => setTestUrl(e.target.value)}
|
|
1187
|
-
/>
|
|
1188
|
-
</Form.Item>
|
|
1189
|
-
</Form>
|
|
1190
|
-
|
|
1191
|
-
{testResult && (
|
|
1192
|
-
<div className="test-result">
|
|
1193
|
-
<div className="result-header">
|
|
1194
|
-
<h3>测试结果</h3>
|
|
1195
|
-
{testResult.success ? (
|
|
1196
|
-
<span className="status success">成功</span>
|
|
1197
|
-
) : (
|
|
1198
|
-
<span className="status error">失败</span>
|
|
1199
|
-
)}
|
|
1200
|
-
</div>
|
|
1201
|
-
|
|
1202
|
-
{testResult.success ? (
|
|
1203
|
-
<div className="result-content">
|
|
1204
|
-
<div className="result-item">
|
|
1205
|
-
<span className="label">请求URL:</span>
|
|
1206
|
-
<span className="value">{testResult.requestUrl}</span>
|
|
1207
|
-
</div>
|
|
1208
|
-
<div className="result-item">
|
|
1209
|
-
<span className="label">匹配规则:</span>
|
|
1210
|
-
<span className="value">{testResult.matchedRule}</span>
|
|
1211
|
-
</div>
|
|
1212
|
-
<div className="result-item">
|
|
1213
|
-
<span className="label">请求方法:</span>
|
|
1214
|
-
<span className="value">{testResult.httpMethod || 'ALL'}</span>
|
|
1215
|
-
</div>
|
|
1216
|
-
<div className="result-item">
|
|
1217
|
-
<span className="label">处理方式:</span>
|
|
1218
|
-
<span className="value">
|
|
1219
|
-
{(() => {
|
|
1220
|
-
const found = proxyTypes.find(item => item.value === editingInterface.proxyType);
|
|
1221
|
-
return found ? found.label : editingInterface.proxyType || '模拟响应';
|
|
1222
|
-
})()}
|
|
1223
|
-
</span>
|
|
1224
|
-
</div>
|
|
1225
|
-
|
|
1226
|
-
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') && (
|
|
1227
|
-
<div className="result-item">
|
|
1228
|
-
<span className="label">重定向目标:</span>
|
|
1229
|
-
<span className="value">{testResult.targetUrl || editingInterface.targetUrl}</span>
|
|
1230
|
-
</div>
|
|
1231
|
-
)}
|
|
1232
|
-
|
|
1233
|
-
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') &&
|
|
1234
|
-
testResult.formattedHeaders && (
|
|
1235
|
-
<div className="result-item">
|
|
1236
|
-
<span className="label">自定义请求头:</span>
|
|
1237
|
-
<div className="value" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
1238
|
-
{
|
|
1239
|
-
editingInterface.customHeaders &&
|
|
1240
|
-
Object.entries(editingInterface.customHeaders)
|
|
1241
|
-
.map(([key, value]) => {
|
|
1242
|
-
const generatedValue = testResult.customHeaders[key];
|
|
1243
|
-
return (
|
|
1244
|
-
<div key={key}>
|
|
1245
|
-
{key}: {value.startsWith('@') ? (
|
|
1246
|
-
<span>
|
|
1247
|
-
<span style={{ color: '#d9d9d9' }}>{value}</span>
|
|
1248
|
-
{' '} → {' '}
|
|
1249
|
-
<span style={{ color: '#52c41a', fontWeight: 'bold' }}>{generatedValue}</span>
|
|
1250
|
-
<span style={{ color: '#8c8c8c', fontSize: '12px' }}> (随机生成)</span>
|
|
1251
|
-
</span>
|
|
1252
|
-
) : generatedValue}
|
|
1253
|
-
</div>
|
|
1254
|
-
);
|
|
1255
|
-
})
|
|
1256
|
-
}
|
|
1257
|
-
</div>
|
|
1258
|
-
</div>
|
|
1259
|
-
)}
|
|
1260
|
-
|
|
1261
|
-
{editingInterface.proxyType === 'response' && (
|
|
1262
|
-
<>
|
|
1263
|
-
<div className="result-item">
|
|
1264
|
-
<span className="label">状态码:</span>
|
|
1265
|
-
<span className="value">{testResult.statusCode}</span>
|
|
1266
|
-
</div>
|
|
1267
|
-
<div className="result-item">
|
|
1268
|
-
<span className="label">内容类型:</span>
|
|
1269
|
-
<span className="value">{testResult.contentType}</span>
|
|
1270
|
-
</div>
|
|
1271
|
-
</>
|
|
1272
|
-
)}
|
|
1273
|
-
|
|
1274
|
-
{testResult.mockInfo && (
|
|
1275
|
-
<>
|
|
1276
|
-
<div className="result-item">
|
|
1277
|
-
<span className="label">模拟延迟:</span>
|
|
1278
|
-
<span className="value">{testResult.mockInfo.delay}ms</span>
|
|
1279
|
-
</div>
|
|
1280
|
-
<div className="result-item">
|
|
1281
|
-
<span className="label">响应时间:</span>
|
|
1282
|
-
<span className="value">{new Date(testResult.mockInfo.timestamp).toLocaleString()}</span>
|
|
1283
|
-
</div>
|
|
1284
|
-
</>
|
|
1285
|
-
)}
|
|
1286
|
-
|
|
1287
|
-
{editingInterface.proxyType === 'response' && (
|
|
1288
|
-
<div className="result-body">
|
|
1289
|
-
<div className="label">响应内容:</div>
|
|
1290
|
-
<pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
|
|
1291
|
-
</div>
|
|
1292
|
-
)}
|
|
1293
|
-
</div>
|
|
1294
|
-
) : (
|
|
1295
|
-
<div className="result-error">
|
|
1296
|
-
<ExclamationCircleOutlined /> {testResult.error}
|
|
1297
|
-
</div>
|
|
1298
|
-
)}
|
|
1299
|
-
</div>
|
|
1300
|
-
)}
|
|
1301
|
-
</div>
|
|
1302
|
-
)}
|
|
1303
|
-
</Modal>
|
|
1028
|
+
editingInterface={editingInterface}
|
|
1029
|
+
/>
|
|
1304
1030
|
|
|
1305
|
-
{/*
|
|
1306
|
-
<
|
|
1307
|
-
|
|
1308
|
-
open={previewVisible}
|
|
1031
|
+
{/* 使用拆分后的预览模态框组件 */}
|
|
1032
|
+
<PreviewModal
|
|
1033
|
+
visible={previewVisible}
|
|
1309
1034
|
onCancel={() => setPreviewVisible(false)}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
关闭
|
|
1313
|
-
</Button>
|
|
1314
|
-
]}
|
|
1315
|
-
width={600}
|
|
1316
|
-
>
|
|
1317
|
-
<div style={{
|
|
1318
|
-
maxHeight: '60vh',
|
|
1319
|
-
overflow: 'auto',
|
|
1320
|
-
backgroundColor: '#f5f5f5',
|
|
1321
|
-
padding: '16px',
|
|
1322
|
-
borderRadius: '4px',
|
|
1323
|
-
fontFamily: 'monospace'
|
|
1324
|
-
}}>
|
|
1325
|
-
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
|
1326
|
-
{previewContent}
|
|
1327
|
-
</pre>
|
|
1328
|
-
</div>
|
|
1329
|
-
</Modal>
|
|
1035
|
+
previewContent={previewContent}
|
|
1036
|
+
/>
|
|
1330
1037
|
</div>
|
|
1331
1038
|
</AppLayout>
|
|
1332
1039
|
);
|