whistle.mock-plugins 1.0.6-beta.2 → 1.0.6-beta.21
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/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 -16
- package/lib/rules-server.js +473 -210
- package/lib/server.js +0 -28
- package/package.json +1 -1
- package/ui/src/components/interface-management/InterfaceTestModal.js +304 -0
- package/ui/src/components/interface-management/PreviewModal.js +50 -0
- package/ui/src/components/interface-management/README.md +143 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +406 -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 +295 -700
|
@@ -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,34 @@ 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
|
+
});
|
|
170
|
+
|
|
171
|
+
setCurrentResponseId(defaultResponseId);
|
|
134
172
|
setEditingInterface(null);
|
|
135
173
|
setModalVisible(true);
|
|
136
174
|
};
|
|
137
175
|
|
|
138
176
|
const handleEditInterface = (record) => {
|
|
177
|
+
console.log('开始编辑接口:', record.name, '原始数据:', record);
|
|
139
178
|
setEditingInterface(record);
|
|
140
179
|
|
|
141
180
|
// 将自定义请求头转换为数组格式,用于动态表单项
|
|
@@ -146,6 +185,42 @@ const InterfaceManagement = () => {
|
|
|
146
185
|
headerValue: value
|
|
147
186
|
}));
|
|
148
187
|
}
|
|
188
|
+
|
|
189
|
+
// 处理多响应数据
|
|
190
|
+
let initialResponses = [];
|
|
191
|
+
let initialResponseId = null;
|
|
192
|
+
|
|
193
|
+
// 深拷贝记录中的响应数据,避免引用问题
|
|
194
|
+
if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
|
|
195
|
+
console.log('使用现有响应数据, 数量:', record.responses.length);
|
|
196
|
+
|
|
197
|
+
initialResponses = JSON.parse(JSON.stringify(record.responses));
|
|
198
|
+
initialResponseId = record.activeResponseId || record.responses[0].id;
|
|
199
|
+
|
|
200
|
+
// 确保每个响应都有名称
|
|
201
|
+
initialResponses = initialResponses.map((resp, index) => ({
|
|
202
|
+
id: resp.id || generateResponseId(),
|
|
203
|
+
name: resp.name || `响应 ${index + 1}`,
|
|
204
|
+
description: resp.description || '',
|
|
205
|
+
content: resp.content || '{}'
|
|
206
|
+
}));
|
|
207
|
+
} else if (record.responseContent) {
|
|
208
|
+
// 向后兼容:如果只有传统的 responseContent 字段,创建一个默认响应
|
|
209
|
+
console.log('创建默认响应,使用responseContent字段');
|
|
210
|
+
const defaultResponseId = generateResponseId();
|
|
211
|
+
initialResponses = [
|
|
212
|
+
{
|
|
213
|
+
id: defaultResponseId,
|
|
214
|
+
name: '默认响应',
|
|
215
|
+
description: '',
|
|
216
|
+
content: record.responseContent
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
initialResponseId = defaultResponseId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('编辑接口,设置响应数据:', initialResponses);
|
|
223
|
+
setCurrentResponseId(initialResponseId);
|
|
149
224
|
|
|
150
225
|
form.setFieldsValue({
|
|
151
226
|
name: record.name,
|
|
@@ -153,7 +228,8 @@ const InterfaceManagement = () => {
|
|
|
153
228
|
proxyType: record.proxyType || 'response',
|
|
154
229
|
statusCode: record.httpStatus?.toString() || '200',
|
|
155
230
|
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
156
|
-
|
|
231
|
+
responses: initialResponses,
|
|
232
|
+
activeResponseId: initialResponseId,
|
|
157
233
|
httpMethod: record.httpMethod || 'ALL',
|
|
158
234
|
targetUrl: record.targetUrl || '',
|
|
159
235
|
headerItems: headersArray // 使用数组存储表单项
|
|
@@ -196,63 +272,93 @@ const InterfaceManagement = () => {
|
|
|
196
272
|
}
|
|
197
273
|
};
|
|
198
274
|
|
|
199
|
-
const cleanJsonResponse = (jsonStr) => {
|
|
200
|
-
try {
|
|
201
|
-
const parsed = JSON.parse(jsonStr);
|
|
202
|
-
return JSON.stringify(parsed);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return jsonStr;
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// 验证URL是否合法
|
|
209
|
-
const isValidUrl = (url) => {
|
|
210
|
-
if (!url) return false;
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
new URL(url);
|
|
214
|
-
return true;
|
|
215
|
-
} catch (e) {
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// 缓存刷新服务
|
|
221
|
-
const flushCache = async () => {
|
|
222
|
-
try {
|
|
223
|
-
const response = await axios.get('/_flush_cache');
|
|
224
|
-
return response.data;
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error('刷新缓存失败:', error);
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// 更新成功后刷新缓存
|
|
232
|
-
const refreshCacheAfterUpdate = async () => {
|
|
233
|
-
try {
|
|
234
|
-
await flushCache();
|
|
235
|
-
// 这里不需要显示提示,因为主要操作会有自己的提示
|
|
236
|
-
} catch (error) {
|
|
237
|
-
// 如果刷新缓存失败,记录错误但不影响用户体验
|
|
238
|
-
console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
275
|
const handleSubmit = async () => {
|
|
243
276
|
try {
|
|
244
277
|
const values = await form.validateFields();
|
|
278
|
+
console.log('=== 开始表单提交 ===');
|
|
279
|
+
console.log('表单提交,原始值:', JSON.stringify(values, null, 2));
|
|
245
280
|
|
|
246
|
-
//
|
|
247
|
-
|
|
281
|
+
// 获取当前激活的响应ID和所有响应
|
|
282
|
+
const activeResponseId = values.activeResponseId;
|
|
283
|
+
let responses = values.responses || [];
|
|
284
|
+
|
|
285
|
+
console.log('提取的原始响应数据:', responses);
|
|
286
|
+
console.log('响应数据类型:', typeof responses);
|
|
287
|
+
console.log('是否为数组:', Array.isArray(responses));
|
|
288
|
+
|
|
289
|
+
// 确保responses是数组格式
|
|
290
|
+
if (!Array.isArray(responses)) {
|
|
291
|
+
console.log('响应数据不是数组,尝试解析...');
|
|
248
292
|
try {
|
|
249
|
-
|
|
293
|
+
if (typeof responses === 'string') {
|
|
294
|
+
responses = JSON.parse(responses);
|
|
295
|
+
if (!Array.isArray(responses)) {
|
|
296
|
+
responses = [];
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
responses = [];
|
|
300
|
+
}
|
|
250
301
|
} catch (e) {
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
console.error('解析响应数据失败:', e);
|
|
303
|
+
responses = [];
|
|
253
304
|
}
|
|
254
305
|
}
|
|
255
306
|
|
|
307
|
+
console.log('处理前的响应数据:', JSON.stringify(responses, null, 2));
|
|
308
|
+
|
|
309
|
+
// 确保每个响应都有名称、描述和内容
|
|
310
|
+
const cleanedResponses = responses.map((resp, index) => {
|
|
311
|
+
console.log(`处理响应 ${index}:`, resp);
|
|
312
|
+
const cleaned = {
|
|
313
|
+
id: resp.id || generateResponseId(),
|
|
314
|
+
name: resp.name || `响应 ${index + 1}`,
|
|
315
|
+
description: resp.description || '',
|
|
316
|
+
content: resp.content || '{}'
|
|
317
|
+
};
|
|
318
|
+
console.log(`处理后的响应 ${index}:`, cleaned);
|
|
319
|
+
return cleaned;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 如果没有响应,创建一个默认响应
|
|
323
|
+
if (cleanedResponses.length === 0) {
|
|
324
|
+
console.log('没有响应数据,创建默认响应');
|
|
325
|
+
const defaultId = generateResponseId();
|
|
326
|
+
cleanedResponses.push({
|
|
327
|
+
id: defaultId,
|
|
328
|
+
name: '默认响应',
|
|
329
|
+
description: '',
|
|
330
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
331
|
+
});
|
|
332
|
+
setCurrentResponseId(defaultId);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 确保有一个激活的响应ID
|
|
336
|
+
const validActiveResponseId = activeResponseId && cleanedResponses.some(r => r.id === activeResponseId)
|
|
337
|
+
? activeResponseId
|
|
338
|
+
: cleanedResponses[0].id;
|
|
339
|
+
|
|
340
|
+
// 获取激活的响应
|
|
341
|
+
const activeResponse = cleanedResponses.find(r => r.id === validActiveResponseId) || cleanedResponses[0];
|
|
342
|
+
|
|
343
|
+
console.log('=== 清理后的响应数据 ===');
|
|
344
|
+
console.log('清理后的响应数据:', JSON.stringify(cleanedResponses, null, 2));
|
|
345
|
+
console.log('激活的响应ID:', validActiveResponseId);
|
|
346
|
+
console.log('激活的响应:', activeResponse);
|
|
347
|
+
|
|
348
|
+
// 检查responseBody是否是有效的JSON
|
|
349
|
+
if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
|
|
350
|
+
cleanedResponses.forEach(response => {
|
|
351
|
+
try {
|
|
352
|
+
if (response.content) {
|
|
353
|
+
JSON.parse(response.content);
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
message.error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
357
|
+
throw new Error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
256
362
|
// 处理自定义请求头,将表单项数组转换为对象格式
|
|
257
363
|
let customHeaders = {};
|
|
258
364
|
|
|
@@ -271,7 +377,11 @@ const InterfaceManagement = () => {
|
|
|
271
377
|
featureId: selectedFeatureId,
|
|
272
378
|
urlPattern: values.pattern,
|
|
273
379
|
proxyType: values.proxyType,
|
|
274
|
-
|
|
380
|
+
// 同时保存所有响应和当前活跃的响应
|
|
381
|
+
responses: cleanedResponses,
|
|
382
|
+
activeResponseId: validActiveResponseId,
|
|
383
|
+
// 兼容性保留:将当前活跃响应的内容保存到 responseContent
|
|
384
|
+
responseContent: activeResponse ? activeResponse.content : '',
|
|
275
385
|
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
276
386
|
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
277
387
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
@@ -281,16 +391,24 @@ const InterfaceManagement = () => {
|
|
|
281
391
|
active: true
|
|
282
392
|
};
|
|
283
393
|
|
|
394
|
+
console.log('=== 最终提交的接口数据 ===');
|
|
395
|
+
console.log('提交的接口数据:', JSON.stringify(interfaceData, null, 2));
|
|
396
|
+
|
|
284
397
|
let response;
|
|
285
398
|
if (editingInterface) {
|
|
286
399
|
// 更新现有接口
|
|
400
|
+
console.log('执行接口更新,ID:', editingInterface.id);
|
|
287
401
|
response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
|
|
288
402
|
} else {
|
|
289
403
|
// 创建新接口
|
|
404
|
+
console.log('执行接口创建');
|
|
290
405
|
response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
291
406
|
}
|
|
292
407
|
|
|
408
|
+
console.log('服务器响应:', response.data);
|
|
409
|
+
|
|
293
410
|
if (response.data && response.data.code === 0) {
|
|
411
|
+
console.log('=== 接口操作成功 ===');
|
|
294
412
|
message.success(editingInterface ? '接口更新成功' : '接口创建成功');
|
|
295
413
|
setModalVisible(false);
|
|
296
414
|
fetchInterfaces();
|
|
@@ -301,6 +419,11 @@ const InterfaceManagement = () => {
|
|
|
301
419
|
throw new Error(response.data?.message || '操作失败');
|
|
302
420
|
}
|
|
303
421
|
} catch (error) {
|
|
422
|
+
if (error.message && error.message.includes('JSON格式')) {
|
|
423
|
+
// 已经显示了错误信息,不需要再显示
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
console.error('=== 接口操作失败 ===');
|
|
304
427
|
console.error('操作失败:', error);
|
|
305
428
|
message.error(error.response?.data?.message || error.message || '操作失败');
|
|
306
429
|
}
|
|
@@ -312,188 +435,41 @@ const InterfaceManagement = () => {
|
|
|
312
435
|
|
|
313
436
|
const handleTestInterface = async (record) => {
|
|
314
437
|
setEditingInterface(record);
|
|
315
|
-
setTestUrl('');
|
|
316
|
-
setTestResult(null);
|
|
317
|
-
setTestLoading(false);
|
|
318
438
|
setTestModalVisible(true);
|
|
319
|
-
testForm.resetFields();
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// 检查 URL 是否匹配模式
|
|
323
|
-
const isUrlMatchPattern = (url, pattern, proxyType) => {
|
|
324
|
-
if (!url || !pattern) return false;
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
// 对于url_redirect类型,需要完全匹配
|
|
328
|
-
if (proxyType === 'url_redirect') {
|
|
329
|
-
return url === pattern;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
333
|
-
if (proxyType === 'redirect') {
|
|
334
|
-
return url.indexOf(pattern) === 0;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// 以下是默认的匹配逻辑(用于response类型等)
|
|
338
|
-
// 如果是正则表达式
|
|
339
|
-
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
340
|
-
const regex = new RegExp(pattern.slice(1, -1));
|
|
341
|
-
return regex.test(url);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 如果是通配符模式
|
|
345
|
-
if (pattern.includes('*')) {
|
|
346
|
-
const regexPattern = pattern
|
|
347
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
348
|
-
.replace(/\*/g, '.*');
|
|
349
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
350
|
-
return regex.test(url);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// 精确匹配
|
|
354
|
-
return url === pattern;
|
|
355
|
-
} catch (e) {
|
|
356
|
-
console.error('URL匹配检查失败:', e);
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const handleTestSubmit = async () => {
|
|
362
|
-
try {
|
|
363
|
-
const values = await testForm.validateFields();
|
|
364
|
-
const { testUrl } = values;
|
|
365
|
-
|
|
366
|
-
if (!testUrl) {
|
|
367
|
-
message.error('请输入测试URL');
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
setTestLoading(true);
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
// 测试前刷新缓存,确保使用最新的接口定义
|
|
375
|
-
await refreshCacheAfterUpdate();
|
|
376
|
-
} catch (error) {
|
|
377
|
-
// 即使刷新缓存失败,也继续测试
|
|
378
|
-
console.warn('刷新缓存失败,将使用现有的缓存数据:', error);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
// 验证测试 URL 是否匹配当前接口的匹配规则
|
|
383
|
-
if (!isUrlMatchPattern(testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
|
|
384
|
-
let errorMessage = '测试URL与接口匹配规则不匹配';
|
|
385
|
-
|
|
386
|
-
// 根据不同的代理类型提供更具体的错误信息
|
|
387
|
-
if (editingInterface.proxyType === 'url_redirect') {
|
|
388
|
-
errorMessage = `测试URL必须完全匹配 ${editingInterface.urlPattern}`;
|
|
389
|
-
} else if (editingInterface.proxyType === 'redirect') {
|
|
390
|
-
errorMessage = `测试URL必须以 ${editingInterface.urlPattern} 开头`;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
setTestResult({
|
|
394
|
-
success: false,
|
|
395
|
-
error: errorMessage
|
|
396
|
-
});
|
|
397
|
-
message.error(errorMessage);
|
|
398
|
-
setTestLoading(false);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 调用实际测试接口
|
|
403
|
-
const response = await axios.post(`/cgi-bin/test-interface`, {
|
|
404
|
-
url: testUrl,
|
|
405
|
-
interfaceId: editingInterface.id
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
if (response.data && response.data.code === 0) {
|
|
409
|
-
// 设置测试结果
|
|
410
|
-
setTestResult({
|
|
411
|
-
success: true,
|
|
412
|
-
...response.data.data,
|
|
413
|
-
matchedRule: editingInterface.urlPattern,
|
|
414
|
-
httpMethod: editingInterface.httpMethod,
|
|
415
|
-
requestUrl: testUrl,
|
|
416
|
-
mockInfo: response.data.data.mockInfo
|
|
417
|
-
});
|
|
418
|
-
} else {
|
|
419
|
-
throw new Error(response.data?.message || '测试响应格式不正确');
|
|
420
|
-
}
|
|
421
|
-
} catch (error) {
|
|
422
|
-
setTestResult({
|
|
423
|
-
success: false,
|
|
424
|
-
error: error.response?.data?.message || error.message || '测试失败'
|
|
425
|
-
});
|
|
426
|
-
message.error(error.response?.data?.message || error.message || '测试失败');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
setTestLoading(false);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
if (error.errorFields) {
|
|
432
|
-
return; // 表单验证错误
|
|
433
|
-
}
|
|
434
|
-
console.error('测试失败:', error);
|
|
435
|
-
message.error(error.message || '测试失败');
|
|
436
|
-
setTestLoading(false);
|
|
437
|
-
}
|
|
438
439
|
};
|
|
439
440
|
|
|
440
441
|
const handleSelectFeature = (featureId) => {
|
|
441
442
|
setSelectedFeatureId(featureId);
|
|
442
443
|
};
|
|
443
444
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return content;
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const formatJsonContent = () => {
|
|
457
|
-
const proxyType = form.getFieldValue('proxyType');
|
|
458
|
-
if (proxyType !== 'response') {
|
|
445
|
+
// 预览响应内容
|
|
446
|
+
const handlePreview = () => {
|
|
447
|
+
// 获取当前选中的响应内容
|
|
448
|
+
const responses = form.getFieldValue('responses') || [];
|
|
449
|
+
const activeResponseId = form.getFieldValue('activeResponseId');
|
|
450
|
+
const activeResponse = responses.find(r => r.id === activeResponseId);
|
|
451
|
+
|
|
452
|
+
if (!activeResponse) {
|
|
453
|
+
message.error('未找到有效的响应内容');
|
|
459
454
|
return;
|
|
460
455
|
}
|
|
456
|
+
|
|
457
|
+
// 根据内容类型格式化响应内容
|
|
458
|
+
const contentType = form.getFieldValue('contentType') || '';
|
|
459
|
+
const formattedContent = formatResponseContent(activeResponse.content, contentType);
|
|
461
460
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
}
|
|
461
|
+
// 设置预览内容,包含响应名称
|
|
462
|
+
setPreviewContent({
|
|
463
|
+
title: `预览: ${activeResponse.name || '未命名响应'}`,
|
|
464
|
+
content: formattedContent,
|
|
465
|
+
description: '',
|
|
466
|
+
contentType
|
|
467
|
+
});
|
|
468
|
+
setPreviewVisible(true);
|
|
472
469
|
};
|
|
473
470
|
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
if (proxyType !== 'response') {
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const responseBody = form.getFieldValue('responseBody');
|
|
481
|
-
if (responseBody) {
|
|
482
|
-
try {
|
|
483
|
-
// 如果是JSON,格式化显示
|
|
484
|
-
const contentType = form.getFieldValue('contentType');
|
|
485
|
-
if (contentType && contentType.includes('json')) {
|
|
486
|
-
setPreviewContent(JSON.stringify(JSON.parse(responseBody), null, 2));
|
|
487
|
-
} else {
|
|
488
|
-
setPreviewContent(responseBody);
|
|
489
|
-
}
|
|
490
|
-
setPreviewVisible(true);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
message.error('内容格式不正确,无法预览');
|
|
493
|
-
}
|
|
494
|
-
} else {
|
|
495
|
-
message.warning('响应内容为空,无法预览');
|
|
496
|
-
}
|
|
471
|
+
const handleResponseSelect = (responseId) => {
|
|
472
|
+
setCurrentResponseId(responseId);
|
|
497
473
|
};
|
|
498
474
|
|
|
499
475
|
const filteredInterfaces = interfaces.filter(item =>
|
|
@@ -678,6 +654,7 @@ const InterfaceManagement = () => {
|
|
|
678
654
|
添加接口
|
|
679
655
|
</Button>
|
|
680
656
|
</div>
|
|
657
|
+
|
|
681
658
|
{!features.length && (
|
|
682
659
|
<Alert
|
|
683
660
|
message="未找到功能模块"
|
|
@@ -709,6 +686,7 @@ const InterfaceManagement = () => {
|
|
|
709
686
|
/>
|
|
710
687
|
</div>
|
|
711
688
|
|
|
689
|
+
{/* 接口编辑/创建模态框 */}
|
|
712
690
|
<Modal
|
|
713
691
|
title={editingInterface ? '编辑接口' : '添加接口'}
|
|
714
692
|
open={modalVisible}
|
|
@@ -729,12 +707,13 @@ const InterfaceManagement = () => {
|
|
|
729
707
|
proxyType: 'response',
|
|
730
708
|
statusCode: '200',
|
|
731
709
|
contentType: 'application/json; charset=utf-8',
|
|
732
|
-
|
|
710
|
+
responses: [],
|
|
733
711
|
httpMethod: 'ALL',
|
|
734
712
|
targetUrl: '',
|
|
735
713
|
headerItems: []
|
|
736
714
|
}}
|
|
737
715
|
>
|
|
716
|
+
{/* 基础信息表单项保持原样 */}
|
|
738
717
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
739
718
|
<div style={{ flex: 1 }}>
|
|
740
719
|
<Form.Item
|
|
@@ -885,7 +864,7 @@ const InterfaceManagement = () => {
|
|
|
885
864
|
|
|
886
865
|
{/* 根据proxyType显示不同的表单项 */}
|
|
887
866
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
888
|
-
{({ getFieldValue }) => {
|
|
867
|
+
{({ getFieldValue, setFieldsValue }) => {
|
|
889
868
|
const proxyType = getFieldValue('proxyType');
|
|
890
869
|
|
|
891
870
|
if (proxyType === 'response') {
|
|
@@ -920,453 +899,69 @@ const InterfaceManagement = () => {
|
|
|
920
899
|
</div>
|
|
921
900
|
</div>
|
|
922
901
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
label={
|
|
929
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
930
|
-
<span>响应内容</span>
|
|
931
|
-
<Space>
|
|
932
|
-
<Button
|
|
933
|
-
type="link"
|
|
934
|
-
icon={<EyeOutlined />}
|
|
935
|
-
onClick={handlePreview}
|
|
936
|
-
style={{ padding: 0 }}
|
|
937
|
-
>
|
|
938
|
-
预览
|
|
939
|
-
</Button>
|
|
940
|
-
<Button
|
|
941
|
-
type="link"
|
|
942
|
-
icon={<FormatPainterOutlined />}
|
|
943
|
-
onClick={formatJsonContent}
|
|
944
|
-
style={{ padding: 0 }}
|
|
945
|
-
>
|
|
946
|
-
格式化JSON
|
|
947
|
-
</Button>
|
|
948
|
-
</Space>
|
|
949
|
-
</div>
|
|
950
|
-
}
|
|
951
|
-
rules={[{ required: true, message: '请输入响应内容' }]}
|
|
902
|
+
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
903
|
+
<Form.Item
|
|
904
|
+
name="responses"
|
|
905
|
+
initialValue={[]}
|
|
906
|
+
hidden
|
|
952
907
|
>
|
|
953
|
-
<
|
|
908
|
+
<Input />
|
|
954
909
|
</Form.Item>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
try {
|
|
972
|
-
new URL(value);
|
|
973
|
-
return Promise.resolve();
|
|
974
|
-
} catch (e) {
|
|
975
|
-
return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
|
|
910
|
+
<Form.Item name="activeResponseId" hidden>
|
|
911
|
+
<Input />
|
|
912
|
+
</Form.Item>
|
|
913
|
+
|
|
914
|
+
{/* 使用新的合并组件 */}
|
|
915
|
+
<Form.Item noStyle shouldUpdate>
|
|
916
|
+
{({ getFieldValue }) => {
|
|
917
|
+
let responses = getFieldValue('responses') || [];
|
|
918
|
+
const activeResponseId = getFieldValue('activeResponseId');
|
|
919
|
+
|
|
920
|
+
// 确保是数组
|
|
921
|
+
if (!Array.isArray(responses)) {
|
|
922
|
+
try {
|
|
923
|
+
if (typeof responses === 'string') {
|
|
924
|
+
responses = JSON.parse(responses);
|
|
976
925
|
}
|
|
926
|
+
} catch (e) {
|
|
927
|
+
responses = [];
|
|
977
928
|
}
|
|
978
929
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
addonBefore={
|
|
991
|
-
<Select
|
|
992
|
-
defaultValue="https://"
|
|
993
|
-
className="select-before"
|
|
994
|
-
style={{ width: 100 }}
|
|
995
|
-
onChange={(value) => {
|
|
996
|
-
const currentUrl = form.getFieldValue('targetUrl') || '';
|
|
997
|
-
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
|
|
998
|
-
form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
|
|
999
|
-
}}
|
|
1000
|
-
>
|
|
1001
|
-
<Option value="http://">http://</Option>
|
|
1002
|
-
<Option value="https://">https://</Option>
|
|
1003
|
-
</Select>
|
|
1004
|
-
}
|
|
1005
|
-
/>
|
|
1006
|
-
</Form.Item>
|
|
1007
|
-
|
|
1008
|
-
<Form.Item
|
|
1009
|
-
label="自定义请求头"
|
|
1010
|
-
tooltip="为请求添加或替换HTTP请求头"
|
|
1011
|
-
className="custom-headers-container"
|
|
1012
|
-
>
|
|
1013
|
-
<Form.List name="headerItems">
|
|
1014
|
-
{(fields, { add, remove }) => (
|
|
1015
|
-
<>
|
|
1016
|
-
{fields.map(({ key, name, ...restField }) => (
|
|
1017
|
-
<Row key={key} gutter={8} style={{ marginBottom: 8 }}>
|
|
1018
|
-
<Col span={10}>
|
|
1019
|
-
<Form.Item
|
|
1020
|
-
{...restField}
|
|
1021
|
-
name={[name, 'headerName']}
|
|
1022
|
-
noStyle
|
|
1023
|
-
rules={[
|
|
1024
|
-
{
|
|
1025
|
-
required: true,
|
|
1026
|
-
message: '请输入请求头名称'
|
|
1027
|
-
},
|
|
1028
|
-
{
|
|
1029
|
-
pattern: /^[^:]+$/,
|
|
1030
|
-
message: '请求头名称不能包含冒号'
|
|
1031
|
-
}
|
|
1032
|
-
]}
|
|
1033
|
-
>
|
|
1034
|
-
<Input placeholder="Header-Name" />
|
|
1035
|
-
</Form.Item>
|
|
1036
|
-
</Col>
|
|
1037
|
-
<Col span={12}>
|
|
1038
|
-
<Form.Item
|
|
1039
|
-
{...restField}
|
|
1040
|
-
name={[name, 'headerValue']}
|
|
1041
|
-
noStyle
|
|
1042
|
-
rules={[
|
|
1043
|
-
{
|
|
1044
|
-
validator: (_, value) => {
|
|
1045
|
-
if (!value) return Promise.resolve();
|
|
1046
|
-
|
|
1047
|
-
// 检查是否是随机数格式 @xxxx-xxx
|
|
1048
|
-
if (value.startsWith('@')) {
|
|
1049
|
-
const randomPattern = value.substring(1);
|
|
1050
|
-
// 验证格式: 只能包含x和-,且首尾不能是-
|
|
1051
|
-
if (!/^[x][-x]*[x]$/.test(randomPattern) ||
|
|
1052
|
-
randomPattern.startsWith('-') ||
|
|
1053
|
-
randomPattern.endsWith('-')) {
|
|
1054
|
-
return Promise.reject(
|
|
1055
|
-
new Error('随机数格式错误,应为@xxxx-xxx格式,只能包含x和-,且首尾不能是-')
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
return Promise.resolve();
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
]}
|
|
1063
|
-
>
|
|
1064
|
-
<Input placeholder="Header-Value" />
|
|
1065
|
-
</Form.Item>
|
|
1066
|
-
</Col>
|
|
1067
|
-
<Col span={2}>
|
|
1068
|
-
<Button
|
|
1069
|
-
type="text"
|
|
1070
|
-
icon={<MinusCircleOutlined />}
|
|
1071
|
-
onClick={() => remove(name)}
|
|
1072
|
-
style={{ color: '#ff4d4f' }}
|
|
1073
|
-
/>
|
|
1074
|
-
</Col>
|
|
1075
|
-
</Row>
|
|
1076
|
-
))}
|
|
1077
|
-
|
|
1078
|
-
<Form.Item>
|
|
1079
|
-
<Button
|
|
1080
|
-
type="dashed"
|
|
1081
|
-
onClick={() => add()}
|
|
1082
|
-
block
|
|
1083
|
-
icon={<PlusOutlined />}
|
|
1084
|
-
>
|
|
1085
|
-
添加请求头
|
|
1086
|
-
</Button>
|
|
1087
|
-
</Form.Item>
|
|
1088
|
-
|
|
1089
|
-
<div style={{ marginTop: 8 }}>
|
|
1090
|
-
<Button
|
|
1091
|
-
type="link"
|
|
1092
|
-
onClick={() => {
|
|
1093
|
-
add({ headerName: 'Content-Type', headerValue: 'application/json' });
|
|
1094
|
-
}}
|
|
1095
|
-
>
|
|
1096
|
-
添加 Content-Type
|
|
1097
|
-
</Button>
|
|
1098
|
-
<Button
|
|
1099
|
-
type="link"
|
|
1100
|
-
onClick={() => {
|
|
1101
|
-
add({ headerName: 'Authorization', headerValue: 'Bearer ' });
|
|
1102
|
-
}}
|
|
1103
|
-
>
|
|
1104
|
-
添加 Authorization
|
|
1105
|
-
</Button>
|
|
1106
|
-
<Button
|
|
1107
|
-
type="link"
|
|
1108
|
-
onClick={() => {
|
|
1109
|
-
add({ headerName: 'User-Agent', headerValue: 'Mozilla/5.0' });
|
|
1110
|
-
}}
|
|
1111
|
-
>
|
|
1112
|
-
添加 User-Agent
|
|
1113
|
-
</Button>
|
|
1114
|
-
<Button
|
|
1115
|
-
type="link"
|
|
1116
|
-
onClick={() => {
|
|
1117
|
-
add({ headerName: 'X-Random-ID', headerValue: '@xxxx-xxxx' });
|
|
1118
|
-
}}
|
|
1119
|
-
>
|
|
1120
|
-
添加随机ID
|
|
1121
|
-
</Button>
|
|
1122
|
-
</div>
|
|
1123
|
-
|
|
1124
|
-
<div style={{ marginTop: 8, background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
|
|
1125
|
-
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>随机数格式说明:</div>
|
|
1126
|
-
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
|
1127
|
-
<li>使用 <code>@</code> 开头表示这是一个随机数值</li>
|
|
1128
|
-
<li>格式示例:<code>@xxxx-xxxx</code> 将生成如 <code>a1b2-c3d4</code> 的随机值</li>
|
|
1129
|
-
<li>每个 <code>x</code> 将替换为随机字母或数字</li>
|
|
1130
|
-
<li><code>-</code> 将保留在输出中作为分隔符</li>
|
|
1131
|
-
</ul>
|
|
1132
|
-
</div>
|
|
1133
|
-
</>
|
|
1134
|
-
)}
|
|
1135
|
-
</Form.List>
|
|
930
|
+
|
|
931
|
+
return (
|
|
932
|
+
<ResponseContentEditor
|
|
933
|
+
form={form}
|
|
934
|
+
responses={responses}
|
|
935
|
+
activeResponseId={activeResponseId}
|
|
936
|
+
onPreview={handlePreview}
|
|
937
|
+
/>
|
|
938
|
+
);
|
|
939
|
+
}}
|
|
1136
940
|
</Form.Item>
|
|
1137
941
|
</>
|
|
1138
942
|
);
|
|
1139
943
|
}
|
|
1140
944
|
|
|
945
|
+
// 这里可以添加其他 proxyType 的处理逻辑
|
|
1141
946
|
return null;
|
|
1142
947
|
}}
|
|
1143
948
|
</Form.Item>
|
|
1144
949
|
</Form>
|
|
1145
950
|
</Modal>
|
|
1146
951
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
952
|
+
{/* 使用拆分后的测试模态框组件 */}
|
|
953
|
+
<InterfaceTestModal
|
|
954
|
+
visible={testModalVisible}
|
|
1150
955
|
onCancel={() => setTestModalVisible(false)}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
关闭
|
|
1154
|
-
</Button>,
|
|
1155
|
-
<Button key="submit" type="primary" loading={testLoading} onClick={handleTestSubmit}>
|
|
1156
|
-
测试
|
|
1157
|
-
</Button>
|
|
1158
|
-
]}
|
|
1159
|
-
width={800}
|
|
1160
|
-
>
|
|
1161
|
-
{editingInterface && (
|
|
1162
|
-
<div className="test-interface-container">
|
|
1163
|
-
<Alert
|
|
1164
|
-
message="URL匹配规则说明"
|
|
1165
|
-
description={
|
|
1166
|
-
<div>
|
|
1167
|
-
<p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
|
|
1168
|
-
<p>代理类型:{
|
|
1169
|
-
proxyTypes.find(item => item.value === editingInterface.proxyType)?.label ||
|
|
1170
|
-
editingInterface.proxyType || '模拟响应'
|
|
1171
|
-
}</p>
|
|
1172
|
-
|
|
1173
|
-
{editingInterface.proxyType === 'url_redirect' && (
|
|
1174
|
-
<div>
|
|
1175
|
-
<p><b>URL重定向模式规则:</b></p>
|
|
1176
|
-
<ul>
|
|
1177
|
-
<li>URL匹配规则必须是完整URL(包括http://或https://)</li>
|
|
1178
|
-
<li>需要完全匹配URL路径,测试URL必须与匹配规则<b>完全一致</b></li>
|
|
1179
|
-
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
1180
|
-
</ul>
|
|
1181
|
-
</div>
|
|
1182
|
-
)}
|
|
1183
|
-
|
|
1184
|
-
{editingInterface.proxyType === 'redirect' && (
|
|
1185
|
-
<div>
|
|
1186
|
-
<p><b>重定向模式规则:</b></p>
|
|
1187
|
-
<ul>
|
|
1188
|
-
<li>URL匹配规则必须以http://或https://开头</li>
|
|
1189
|
-
<li>测试URL必须以匹配规则开头(<b>前缀匹配</b>)</li>
|
|
1190
|
-
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
1191
|
-
</ul>
|
|
1192
|
-
</div>
|
|
1193
|
-
)}
|
|
1194
|
-
|
|
1195
|
-
{editingInterface.proxyType === 'response' && (
|
|
1196
|
-
<div>
|
|
1197
|
-
<p>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugins://,然后插件会根据接口的匹配规则处理请求。</p>
|
|
1198
|
-
<p>插件支持以下匹配方式:</p>
|
|
1199
|
-
<ul>
|
|
1200
|
-
<li>精确匹配:完全匹配URL路径部分</li>
|
|
1201
|
-
<li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
|
|
1202
|
-
<li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
|
|
1203
|
-
</ul>
|
|
1204
|
-
</div>
|
|
1205
|
-
)}
|
|
1206
|
-
|
|
1207
|
-
<p>请在下方输入完整测试URL进行测试</p>
|
|
1208
|
-
</div>
|
|
1209
|
-
}
|
|
1210
|
-
type="info"
|
|
1211
|
-
showIcon
|
|
1212
|
-
style={{ marginBottom: 16 }}
|
|
1213
|
-
/>
|
|
1214
|
-
|
|
1215
|
-
<Form
|
|
1216
|
-
form={testForm}
|
|
1217
|
-
layout="vertical"
|
|
1218
|
-
>
|
|
1219
|
-
<Form.Item
|
|
1220
|
-
name="testUrl"
|
|
1221
|
-
label="测试URL"
|
|
1222
|
-
rules={[{ required: true, message: '请输入测试URL' }]}
|
|
1223
|
-
>
|
|
1224
|
-
<Input
|
|
1225
|
-
placeholder="输入完整URL,例如:https://example.com/api/users"
|
|
1226
|
-
onChange={(e) => setTestUrl(e.target.value)}
|
|
1227
|
-
/>
|
|
1228
|
-
</Form.Item>
|
|
1229
|
-
</Form>
|
|
1230
|
-
|
|
1231
|
-
{testResult && (
|
|
1232
|
-
<div className="test-result">
|
|
1233
|
-
<div className="result-header">
|
|
1234
|
-
<h3>测试结果</h3>
|
|
1235
|
-
{testResult.success ? (
|
|
1236
|
-
<span className="status success">成功</span>
|
|
1237
|
-
) : (
|
|
1238
|
-
<span className="status error">失败</span>
|
|
1239
|
-
)}
|
|
1240
|
-
</div>
|
|
1241
|
-
|
|
1242
|
-
{testResult.success ? (
|
|
1243
|
-
<div className="result-content">
|
|
1244
|
-
<div className="result-item">
|
|
1245
|
-
<span className="label">请求URL:</span>
|
|
1246
|
-
<span className="value">{testResult.requestUrl}</span>
|
|
1247
|
-
</div>
|
|
1248
|
-
<div className="result-item">
|
|
1249
|
-
<span className="label">匹配规则:</span>
|
|
1250
|
-
<span className="value">{testResult.matchedRule}</span>
|
|
1251
|
-
</div>
|
|
1252
|
-
<div className="result-item">
|
|
1253
|
-
<span className="label">请求方法:</span>
|
|
1254
|
-
<span className="value">{testResult.httpMethod || 'ALL'}</span>
|
|
1255
|
-
</div>
|
|
1256
|
-
<div className="result-item">
|
|
1257
|
-
<span className="label">处理方式:</span>
|
|
1258
|
-
<span className="value">
|
|
1259
|
-
{(() => {
|
|
1260
|
-
const found = proxyTypes.find(item => item.value === editingInterface.proxyType);
|
|
1261
|
-
return found ? found.label : editingInterface.proxyType || '模拟响应';
|
|
1262
|
-
})()}
|
|
1263
|
-
</span>
|
|
1264
|
-
</div>
|
|
1265
|
-
|
|
1266
|
-
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') && (
|
|
1267
|
-
<div className="result-item">
|
|
1268
|
-
<span className="label">重定向目标:</span>
|
|
1269
|
-
<span className="value">{testResult.targetUrl || editingInterface.targetUrl}</span>
|
|
1270
|
-
</div>
|
|
1271
|
-
)}
|
|
1272
|
-
|
|
1273
|
-
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') &&
|
|
1274
|
-
testResult.formattedHeaders && (
|
|
1275
|
-
<div className="result-item">
|
|
1276
|
-
<span className="label">自定义请求头:</span>
|
|
1277
|
-
<div className="value" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
1278
|
-
{
|
|
1279
|
-
editingInterface.customHeaders &&
|
|
1280
|
-
Object.entries(editingInterface.customHeaders)
|
|
1281
|
-
.map(([key, value]) => {
|
|
1282
|
-
const generatedValue = testResult.customHeaders[key];
|
|
1283
|
-
return (
|
|
1284
|
-
<div key={key}>
|
|
1285
|
-
{key}: {value.startsWith('@') ? (
|
|
1286
|
-
<span>
|
|
1287
|
-
<span style={{ color: '#d9d9d9' }}>{value}</span>
|
|
1288
|
-
{' '} → {' '}
|
|
1289
|
-
<span style={{ color: '#52c41a', fontWeight: 'bold' }}>{generatedValue}</span>
|
|
1290
|
-
<span style={{ color: '#8c8c8c', fontSize: '12px' }}> (随机生成)</span>
|
|
1291
|
-
</span>
|
|
1292
|
-
) : generatedValue}
|
|
1293
|
-
</div>
|
|
1294
|
-
);
|
|
1295
|
-
})
|
|
1296
|
-
}
|
|
1297
|
-
</div>
|
|
1298
|
-
</div>
|
|
1299
|
-
)}
|
|
1300
|
-
|
|
1301
|
-
{editingInterface.proxyType === 'response' && (
|
|
1302
|
-
<>
|
|
1303
|
-
<div className="result-item">
|
|
1304
|
-
<span className="label">状态码:</span>
|
|
1305
|
-
<span className="value">{testResult.statusCode}</span>
|
|
1306
|
-
</div>
|
|
1307
|
-
<div className="result-item">
|
|
1308
|
-
<span className="label">内容类型:</span>
|
|
1309
|
-
<span className="value">{testResult.contentType}</span>
|
|
1310
|
-
</div>
|
|
1311
|
-
</>
|
|
1312
|
-
)}
|
|
1313
|
-
|
|
1314
|
-
{testResult.mockInfo && (
|
|
1315
|
-
<>
|
|
1316
|
-
<div className="result-item">
|
|
1317
|
-
<span className="label">模拟延迟:</span>
|
|
1318
|
-
<span className="value">{testResult.mockInfo.delay}ms</span>
|
|
1319
|
-
</div>
|
|
1320
|
-
<div className="result-item">
|
|
1321
|
-
<span className="label">响应时间:</span>
|
|
1322
|
-
<span className="value">{new Date(testResult.mockInfo.timestamp).toLocaleString()}</span>
|
|
1323
|
-
</div>
|
|
1324
|
-
</>
|
|
1325
|
-
)}
|
|
1326
|
-
|
|
1327
|
-
{editingInterface.proxyType === 'response' && (
|
|
1328
|
-
<div className="result-body">
|
|
1329
|
-
<div className="label">响应内容:</div>
|
|
1330
|
-
<pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
|
|
1331
|
-
</div>
|
|
1332
|
-
)}
|
|
1333
|
-
</div>
|
|
1334
|
-
) : (
|
|
1335
|
-
<div className="result-error">
|
|
1336
|
-
<ExclamationCircleOutlined /> {testResult.error}
|
|
1337
|
-
</div>
|
|
1338
|
-
)}
|
|
1339
|
-
</div>
|
|
1340
|
-
)}
|
|
1341
|
-
</div>
|
|
1342
|
-
)}
|
|
1343
|
-
</Modal>
|
|
956
|
+
editingInterface={editingInterface}
|
|
957
|
+
/>
|
|
1344
958
|
|
|
1345
|
-
{/*
|
|
1346
|
-
<
|
|
1347
|
-
|
|
1348
|
-
open={previewVisible}
|
|
959
|
+
{/* 使用拆分后的预览模态框组件 */}
|
|
960
|
+
<PreviewModal
|
|
961
|
+
visible={previewVisible}
|
|
1349
962
|
onCancel={() => setPreviewVisible(false)}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
关闭
|
|
1353
|
-
</Button>
|
|
1354
|
-
]}
|
|
1355
|
-
width={600}
|
|
1356
|
-
>
|
|
1357
|
-
<div style={{
|
|
1358
|
-
maxHeight: '60vh',
|
|
1359
|
-
overflow: 'auto',
|
|
1360
|
-
backgroundColor: '#f5f5f5',
|
|
1361
|
-
padding: '16px',
|
|
1362
|
-
borderRadius: '4px',
|
|
1363
|
-
fontFamily: 'monospace'
|
|
1364
|
-
}}>
|
|
1365
|
-
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
|
1366
|
-
{previewContent}
|
|
1367
|
-
</pre>
|
|
1368
|
-
</div>
|
|
1369
|
-
</Modal>
|
|
963
|
+
previewContent={previewContent}
|
|
964
|
+
/>
|
|
1370
965
|
</div>
|
|
1371
966
|
</AppLayout>
|
|
1372
967
|
);
|