whistle.mock-plugins 1.0.3 → 1.0.5
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 +3 -0
- package/app/cgi-bin/logs.js +38 -29
- package/app/cgi-bin/test-interface.js +228 -35
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/dataManager.js +6 -0
- package/lib/ruleManager.js +62 -30
- package/lib/rules-server.js +179 -5
- package/lib/server.js +74 -3
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +651 -145
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { useParams, useHistory } from 'react-router-dom';
|
|
3
3
|
import AppLayout from '../components/AppLayout';
|
|
4
|
-
import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs } from 'antd';
|
|
5
|
-
import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs, Card, Divider, Badge, Tooltip, Row, Col } from 'antd';
|
|
5
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined, CopyOutlined, CodeOutlined, CheckCircleOutlined, CloseCircleOutlined, MenuOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
|
6
6
|
import '../styles/interface-management.css';
|
|
7
7
|
import axios from 'axios';
|
|
8
8
|
|
|
@@ -38,6 +38,12 @@ const InterfaceManagement = () => {
|
|
|
38
38
|
{ value: 'application/javascript; charset=utf-8', label: 'JavaScript' },
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
const proxyTypes = [
|
|
42
|
+
{ value: 'response', label: '模拟响应' },
|
|
43
|
+
{ value: 'redirect', label: '重定向' },
|
|
44
|
+
{ value: 'url_redirect', label: 'URL重定向' },
|
|
45
|
+
];
|
|
46
|
+
|
|
41
47
|
const statusCodes = [
|
|
42
48
|
{ value: '200', label: '200 OK' },
|
|
43
49
|
{ value: '201', label: '201 Created' },
|
|
@@ -131,13 +137,26 @@ const InterfaceManagement = () => {
|
|
|
131
137
|
|
|
132
138
|
const handleEditInterface = (record) => {
|
|
133
139
|
setEditingInterface(record);
|
|
140
|
+
|
|
141
|
+
// 将自定义请求头转换为数组格式,用于动态表单项
|
|
142
|
+
let headersArray = [];
|
|
143
|
+
if (record.customHeaders && typeof record.customHeaders === 'object') {
|
|
144
|
+
headersArray = Object.entries(record.customHeaders).map(([key, value]) => ({
|
|
145
|
+
headerName: key,
|
|
146
|
+
headerValue: value
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
form.setFieldsValue({
|
|
135
151
|
name: record.name,
|
|
136
152
|
pattern: record.urlPattern,
|
|
153
|
+
proxyType: record.proxyType || 'response',
|
|
137
154
|
statusCode: record.httpStatus?.toString() || '200',
|
|
138
155
|
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
139
156
|
responseBody: record.responseContent || '',
|
|
140
157
|
httpMethod: record.httpMethod || 'ALL',
|
|
158
|
+
targetUrl: record.targetUrl || '',
|
|
159
|
+
headerItems: headersArray // 使用数组存储表单项
|
|
141
160
|
});
|
|
142
161
|
setModalVisible(true);
|
|
143
162
|
};
|
|
@@ -174,7 +193,6 @@ const InterfaceManagement = () => {
|
|
|
174
193
|
}
|
|
175
194
|
};
|
|
176
195
|
|
|
177
|
-
// 清理JSON响应内容中不需要的空白和格式化
|
|
178
196
|
const cleanJsonResponse = (jsonStr) => {
|
|
179
197
|
try {
|
|
180
198
|
const parsed = JSON.parse(jsonStr);
|
|
@@ -184,35 +202,53 @@ const InterfaceManagement = () => {
|
|
|
184
202
|
}
|
|
185
203
|
};
|
|
186
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
|
+
|
|
187
217
|
const handleSubmit = async () => {
|
|
188
218
|
try {
|
|
189
219
|
const values = await form.validateFields();
|
|
190
220
|
|
|
191
|
-
//
|
|
192
|
-
if (
|
|
193
|
-
message.error('URL匹配规则格式不正确');
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 验证响应内容格式
|
|
198
|
-
if (values.contentType.includes('json')) {
|
|
221
|
+
// 检查responseBody是否是有效的JSON
|
|
222
|
+
if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
|
|
199
223
|
try {
|
|
200
|
-
|
|
201
|
-
const cleanedJson = cleanJsonResponse(values.responseBody);
|
|
202
|
-
JSON.parse(cleanedJson);
|
|
203
|
-
values.responseBody = cleanedJson;
|
|
224
|
+
JSON.parse(values.responseBody);
|
|
204
225
|
} catch (e) {
|
|
205
|
-
message.error('JSON
|
|
226
|
+
message.error('响应体不是有效的JSON格式');
|
|
206
227
|
return;
|
|
207
228
|
}
|
|
208
229
|
}
|
|
230
|
+
|
|
231
|
+
// 处理自定义请求头,将表单项数组转换为对象格式
|
|
232
|
+
let customHeaders = {};
|
|
233
|
+
|
|
234
|
+
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
|
|
235
|
+
if (values.headerItems && values.headerItems.length > 0) {
|
|
236
|
+
values.headerItems.forEach(item => {
|
|
237
|
+
if (item && item.headerName && item.headerName.trim()) {
|
|
238
|
+
customHeaders[item.headerName.trim()] = item.headerValue || '';
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
209
243
|
|
|
210
244
|
const interfaceData = {
|
|
211
245
|
name: values.name,
|
|
212
246
|
featureId: selectedFeatureId,
|
|
213
247
|
urlPattern: values.pattern,
|
|
214
|
-
proxyType:
|
|
215
|
-
responseContent: values.responseBody,
|
|
248
|
+
proxyType: values.proxyType,
|
|
249
|
+
responseContent: values.proxyType === 'response' ? values.responseBody : '',
|
|
250
|
+
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
251
|
+
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
216
252
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
217
253
|
contentType: values.contentType,
|
|
218
254
|
responseDelay: 0,
|
|
@@ -247,29 +283,6 @@ const InterfaceManagement = () => {
|
|
|
247
283
|
}
|
|
248
284
|
};
|
|
249
285
|
|
|
250
|
-
// 验证 URL 匹配规则格式
|
|
251
|
-
const isValidPattern = (pattern) => {
|
|
252
|
-
if (!pattern) return false;
|
|
253
|
-
|
|
254
|
-
// 检查是否是有效的正则表达式
|
|
255
|
-
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
256
|
-
try {
|
|
257
|
-
new RegExp(pattern.slice(1, -1));
|
|
258
|
-
return true;
|
|
259
|
-
} catch (e) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// 检查通配符格式
|
|
265
|
-
if (pattern.includes('*')) {
|
|
266
|
-
return /^[a-zA-Z0-9\-_/.*]+$/.test(pattern);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// 检查普通路径格式
|
|
270
|
-
return /^[a-zA-Z0-9\-_/]+$/.test(pattern);
|
|
271
|
-
};
|
|
272
|
-
|
|
273
286
|
const handleCancel = () => {
|
|
274
287
|
setModalVisible(false);
|
|
275
288
|
};
|
|
@@ -282,6 +295,45 @@ const InterfaceManagement = () => {
|
|
|
282
295
|
testForm.resetFields();
|
|
283
296
|
};
|
|
284
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
|
+
|
|
285
337
|
const handleTestSubmit = async () => {
|
|
286
338
|
try {
|
|
287
339
|
const values = await testForm.validateFields();
|
|
@@ -289,12 +341,21 @@ const InterfaceManagement = () => {
|
|
|
289
341
|
|
|
290
342
|
try {
|
|
291
343
|
// 验证测试 URL 是否匹配当前接口的匹配规则
|
|
292
|
-
if (!isUrlMatchPattern(values.testUrl, editingInterface.urlPattern)) {
|
|
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
|
+
|
|
293
354
|
setTestResult({
|
|
294
355
|
success: false,
|
|
295
|
-
error:
|
|
356
|
+
error: errorMessage
|
|
296
357
|
});
|
|
297
|
-
message.error(
|
|
358
|
+
message.error(errorMessage);
|
|
298
359
|
return;
|
|
299
360
|
}
|
|
300
361
|
|
|
@@ -336,34 +397,6 @@ const InterfaceManagement = () => {
|
|
|
336
397
|
}
|
|
337
398
|
};
|
|
338
399
|
|
|
339
|
-
// 检查 URL 是否匹配模式
|
|
340
|
-
const isUrlMatchPattern = (url, pattern) => {
|
|
341
|
-
if (!url || !pattern) return false;
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
// 如果是正则表达式
|
|
345
|
-
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
346
|
-
const regex = new RegExp(pattern.slice(1, -1));
|
|
347
|
-
return regex.test(url);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 如果是通配符模式
|
|
351
|
-
if (pattern.includes('*')) {
|
|
352
|
-
const regexPattern = pattern
|
|
353
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
354
|
-
.replace(/\*/g, '.*');
|
|
355
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
356
|
-
return regex.test(url);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 精确匹配
|
|
360
|
-
return url === pattern;
|
|
361
|
-
} catch (e) {
|
|
362
|
-
console.error('URL匹配检查失败:', e);
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
400
|
const handleSelectFeature = (featureId) => {
|
|
368
401
|
setSelectedFeatureId(featureId);
|
|
369
402
|
};
|
|
@@ -381,6 +414,11 @@ const InterfaceManagement = () => {
|
|
|
381
414
|
};
|
|
382
415
|
|
|
383
416
|
const formatJsonContent = () => {
|
|
417
|
+
const proxyType = form.getFieldValue('proxyType');
|
|
418
|
+
if (proxyType !== 'response') {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
384
422
|
const responseBody = form.getFieldValue('responseBody');
|
|
385
423
|
if (responseBody) {
|
|
386
424
|
try {
|
|
@@ -394,6 +432,11 @@ const InterfaceManagement = () => {
|
|
|
394
432
|
};
|
|
395
433
|
|
|
396
434
|
const handlePreview = () => {
|
|
435
|
+
const proxyType = form.getFieldValue('proxyType');
|
|
436
|
+
if (proxyType !== 'response') {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
397
440
|
const responseBody = form.getFieldValue('responseBody');
|
|
398
441
|
if (responseBody) {
|
|
399
442
|
try {
|
|
@@ -442,22 +485,88 @@ const InterfaceManagement = () => {
|
|
|
442
485
|
key: 'urlPattern',
|
|
443
486
|
ellipsis: true,
|
|
444
487
|
},
|
|
488
|
+
{
|
|
489
|
+
title: '处理方式',
|
|
490
|
+
dataIndex: 'proxyType',
|
|
491
|
+
key: 'proxyType',
|
|
492
|
+
width: 120,
|
|
493
|
+
render: (text) => {
|
|
494
|
+
const found = proxyTypes.find(item => item.value === text);
|
|
495
|
+
return found ? found.label : text || '模拟响应';
|
|
496
|
+
}
|
|
497
|
+
},
|
|
445
498
|
{
|
|
446
499
|
title: '状态码',
|
|
447
500
|
dataIndex: 'httpStatus',
|
|
448
501
|
key: 'httpStatus',
|
|
449
502
|
width: 100,
|
|
503
|
+
render: (text, record) => {
|
|
504
|
+
return record.proxyType === 'response' ? text : '-';
|
|
505
|
+
}
|
|
450
506
|
},
|
|
451
507
|
{
|
|
452
508
|
title: '内容类型',
|
|
453
509
|
dataIndex: 'contentType',
|
|
454
510
|
key: 'contentType',
|
|
455
511
|
width: 120,
|
|
456
|
-
render: (text) => {
|
|
512
|
+
render: (text, record) => {
|
|
513
|
+
if (record.proxyType !== 'response') {
|
|
514
|
+
return '-';
|
|
515
|
+
}
|
|
457
516
|
const found = contentTypes.find(item => item.value === text);
|
|
458
517
|
return found ? found.label : text;
|
|
459
518
|
}
|
|
460
519
|
},
|
|
520
|
+
{
|
|
521
|
+
title: '目标URL',
|
|
522
|
+
dataIndex: 'targetUrl',
|
|
523
|
+
key: 'targetUrl',
|
|
524
|
+
ellipsis: true,
|
|
525
|
+
render: (text, record) => {
|
|
526
|
+
return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
title: '自定义头',
|
|
531
|
+
dataIndex: 'customHeaders',
|
|
532
|
+
key: 'customHeaders',
|
|
533
|
+
width: 100,
|
|
534
|
+
render: (_, record) => {
|
|
535
|
+
if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
|
|
536
|
+
return '-';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const headers = record.customHeaders || {};
|
|
540
|
+
const count = Object.keys(headers).length;
|
|
541
|
+
|
|
542
|
+
if (count === 0) {
|
|
543
|
+
return '-';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 检查是否包含随机值
|
|
547
|
+
const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<Tooltip title={
|
|
551
|
+
<div>
|
|
552
|
+
{Object.entries(headers).map(([key, value]) => (
|
|
553
|
+
<div key={key}>
|
|
554
|
+
{key}: {value}
|
|
555
|
+
{value && value.startsWith('@') && (
|
|
556
|
+
<span style={{ color: '#52c41a' }}> (随机)</span>
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
))}
|
|
560
|
+
</div>
|
|
561
|
+
}>
|
|
562
|
+
<span style={{ color: '#1890ff' }}>
|
|
563
|
+
{count}个
|
|
564
|
+
{hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
|
|
565
|
+
</span>
|
|
566
|
+
</Tooltip>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
461
570
|
{
|
|
462
571
|
title: '请求方法',
|
|
463
572
|
dataIndex: 'httpMethod',
|
|
@@ -577,10 +686,13 @@ const InterfaceManagement = () => {
|
|
|
577
686
|
initialValues={{
|
|
578
687
|
name: '',
|
|
579
688
|
pattern: '',
|
|
689
|
+
proxyType: 'response',
|
|
580
690
|
statusCode: '200',
|
|
581
691
|
contentType: 'application/json; charset=utf-8',
|
|
582
692
|
responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
|
|
583
|
-
httpMethod: 'ALL'
|
|
693
|
+
httpMethod: 'ALL',
|
|
694
|
+
targetUrl: '',
|
|
695
|
+
headerItems: []
|
|
584
696
|
}}
|
|
585
697
|
>
|
|
586
698
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
@@ -597,36 +709,120 @@ const InterfaceManagement = () => {
|
|
|
597
709
|
<Form.Item
|
|
598
710
|
name="pattern"
|
|
599
711
|
label="URL匹配规则"
|
|
600
|
-
rules={[
|
|
601
|
-
|
|
712
|
+
rules={[
|
|
713
|
+
{ required: true, message: '请输入URL匹配规则' },
|
|
714
|
+
({ getFieldValue }) => ({
|
|
715
|
+
validator(_, value) {
|
|
716
|
+
const proxyType = getFieldValue('proxyType');
|
|
717
|
+
if (!value) return Promise.resolve();
|
|
718
|
+
|
|
719
|
+
// URL重定向模式下,必须是完整URL
|
|
720
|
+
if (proxyType === 'url_redirect') {
|
|
721
|
+
try {
|
|
722
|
+
new URL(value);
|
|
723
|
+
return Promise.resolve();
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL(包括http://或https://)'));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 重定向模式下,必须以http://或https://开头
|
|
730
|
+
if (proxyType === 'redirect') {
|
|
731
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
732
|
+
return Promise.resolve();
|
|
733
|
+
}
|
|
734
|
+
return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头'));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 响应模式下的验证
|
|
738
|
+
if (proxyType === 'response') {
|
|
739
|
+
// 正则表达式验证
|
|
740
|
+
if (value.startsWith('/') && value.endsWith('/')) {
|
|
741
|
+
try {
|
|
742
|
+
new RegExp(value.slice(1, -1));
|
|
743
|
+
return Promise.resolve();
|
|
744
|
+
} catch (e) {
|
|
745
|
+
return Promise.reject(new Error('无效的正则表达式格式'));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 通配符验证
|
|
750
|
+
if (value.includes('*')) {
|
|
751
|
+
if (/^[a-zA-Z0-9\-_/.*]+$/.test(value)) {
|
|
752
|
+
return Promise.resolve();
|
|
753
|
+
}
|
|
754
|
+
return Promise.reject(new Error('通配符URL格式不正确'));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 普通路径验证
|
|
758
|
+
if (/^[a-zA-Z0-9\-_/]+$/.test(value)) {
|
|
759
|
+
return Promise.resolve();
|
|
760
|
+
}
|
|
761
|
+
return Promise.reject(new Error('URL路径格式不正确'));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return Promise.resolve();
|
|
765
|
+
},
|
|
766
|
+
}),
|
|
767
|
+
]}
|
|
768
|
+
tooltip={{
|
|
769
|
+
title: (
|
|
770
|
+
<>
|
|
771
|
+
<div>不同处理方式下URL匹配规则要求:</div>
|
|
772
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
773
|
+
<li><b>模拟响应:</b> 支持路径格式如 /api/users,通配符如 /api/*,正则如 /\/api\/\d+/</li>
|
|
774
|
+
<li><b>重定向:</b> 必须以http://或https://开头,例如:https://example.com/api</li>
|
|
775
|
+
<li><b>URL重定向:</b> 必须是完整URL,包括http://或https://,例如:https://example.com/api/users</li>
|
|
776
|
+
</ul>
|
|
777
|
+
</>
|
|
778
|
+
),
|
|
779
|
+
overlayStyle: { maxWidth: '450px' }
|
|
780
|
+
}}
|
|
602
781
|
>
|
|
603
|
-
<Input
|
|
782
|
+
<Input
|
|
783
|
+
placeholder="根据选择的处理方式输入相应格式的URL"
|
|
784
|
+
/>
|
|
604
785
|
</Form.Item>
|
|
605
786
|
</div>
|
|
606
787
|
</div>
|
|
607
|
-
|
|
788
|
+
|
|
608
789
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
609
790
|
<div style={{ flex: 1 }}>
|
|
610
791
|
<Form.Item
|
|
611
|
-
name="
|
|
612
|
-
label="
|
|
613
|
-
rules={[{ required: true, message: '
|
|
792
|
+
name="proxyType"
|
|
793
|
+
label="处理方式"
|
|
794
|
+
rules={[{ required: true, message: '请选择处理方式' }]}
|
|
795
|
+
tooltip={{
|
|
796
|
+
title: (
|
|
797
|
+
<>
|
|
798
|
+
<div>不同处理方式的规则说明:</div>
|
|
799
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
800
|
+
<li><b>模拟响应:</b> 返回您定义的响应内容</li>
|
|
801
|
+
<li><b>重定向:</b> 将请求重定向到其他URL,URL匹配规则必须以http://或https://开头</li>
|
|
802
|
+
<li><b>URL重定向:</b> 完全匹配URL时重定向,URL匹配规则必须是完整URL</li>
|
|
803
|
+
</ul>
|
|
804
|
+
</>
|
|
805
|
+
),
|
|
806
|
+
overlayStyle: { maxWidth: '450px' }
|
|
807
|
+
}}
|
|
614
808
|
>
|
|
615
|
-
<Select
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
809
|
+
<Select onChange={(value) => {
|
|
810
|
+
// 当切换代理类型时,清空pattern字段,并提供不同的placeholder
|
|
811
|
+
form.setFieldsValue({ pattern: '' });
|
|
812
|
+
|
|
813
|
+
// 为不同的代理类型提供不同的pattern占位符
|
|
814
|
+
const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
|
|
815
|
+
if (patternInput) {
|
|
816
|
+
if (value === 'response') {
|
|
817
|
+
patternInput.placeholder = "例如:/api/users,/api/users/*,/api\/users\/\d+/";
|
|
818
|
+
} else if (value === 'redirect') {
|
|
819
|
+
patternInput.placeholder = "例如:https://example.com/api";
|
|
820
|
+
} else if (value === 'url_redirect') {
|
|
821
|
+
patternInput.placeholder = "例如:https://example.com/api/users";
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}}>
|
|
825
|
+
{proxyTypes.map(item => (
|
|
630
826
|
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
631
827
|
))}
|
|
632
828
|
</Select>
|
|
@@ -647,37 +843,263 @@ const InterfaceManagement = () => {
|
|
|
647
843
|
</div>
|
|
648
844
|
</div>
|
|
649
845
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
846
|
+
{/* 根据proxyType显示不同的表单项 */}
|
|
847
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
848
|
+
{({ getFieldValue }) => {
|
|
849
|
+
const proxyType = getFieldValue('proxyType');
|
|
850
|
+
|
|
851
|
+
if (proxyType === 'response') {
|
|
852
|
+
return (
|
|
853
|
+
<>
|
|
854
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
855
|
+
<div style={{ flex: 1 }}>
|
|
856
|
+
<Form.Item
|
|
857
|
+
name="statusCode"
|
|
858
|
+
label="状态码"
|
|
859
|
+
rules={[{ required: true, message: '请选择状态码' }]}
|
|
860
|
+
>
|
|
861
|
+
<Select>
|
|
862
|
+
{statusCodes.map(item => (
|
|
863
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
864
|
+
))}
|
|
865
|
+
</Select>
|
|
866
|
+
</Form.Item>
|
|
867
|
+
</div>
|
|
868
|
+
<div style={{ flex: 1 }}>
|
|
869
|
+
<Form.Item
|
|
870
|
+
name="contentType"
|
|
871
|
+
label="内容类型"
|
|
872
|
+
rules={[{ required: true, message: '请选择内容类型' }]}
|
|
873
|
+
>
|
|
874
|
+
<Select>
|
|
875
|
+
{contentTypes.map(item => (
|
|
876
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
877
|
+
))}
|
|
878
|
+
</Select>
|
|
879
|
+
</Form.Item>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
|
|
883
|
+
<Form.Item
|
|
884
|
+
name="responseBody"
|
|
885
|
+
labelCol={{
|
|
886
|
+
span: 8, /* 宽度比例 */
|
|
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: '请输入响应内容' }]}
|
|
912
|
+
>
|
|
913
|
+
<TextArea rows={12} placeholder="请输入响应内容" style={{ fontFamily: 'monospace' }} />
|
|
914
|
+
</Form.Item>
|
|
915
|
+
</>
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
920
|
+
return (
|
|
921
|
+
<>
|
|
922
|
+
<Form.Item
|
|
923
|
+
name="targetUrl"
|
|
924
|
+
label="重定向目标URL"
|
|
925
|
+
rules={[
|
|
926
|
+
{ required: true, message: '请输入重定向目标URL' },
|
|
927
|
+
{
|
|
928
|
+
validator: (_, value) => {
|
|
929
|
+
if (!value) return Promise.resolve();
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
new URL(value);
|
|
933
|
+
return Promise.resolve();
|
|
934
|
+
} catch (e) {
|
|
935
|
+
return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
]}
|
|
940
|
+
tooltip={proxyType === 'redirect' ?
|
|
941
|
+
"重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
|
|
942
|
+
"URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
|
|
943
|
+
}
|
|
944
|
+
>
|
|
945
|
+
<Input
|
|
946
|
+
placeholder={proxyType === 'redirect' ?
|
|
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
|
+
}
|
|
965
|
+
/>
|
|
966
|
+
</Form.Item>
|
|
967
|
+
|
|
968
|
+
<Form.Item
|
|
969
|
+
label="自定义请求头"
|
|
970
|
+
tooltip="为请求添加或替换HTTP请求头"
|
|
971
|
+
className="custom-headers-container"
|
|
972
|
+
>
|
|
973
|
+
<Form.List name="headerItems">
|
|
974
|
+
{(fields, { add, remove }) => (
|
|
975
|
+
<>
|
|
976
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
977
|
+
<Row key={key} gutter={8} style={{ marginBottom: 8 }}>
|
|
978
|
+
<Col span={10}>
|
|
979
|
+
<Form.Item
|
|
980
|
+
{...restField}
|
|
981
|
+
name={[name, 'headerName']}
|
|
982
|
+
noStyle
|
|
983
|
+
rules={[
|
|
984
|
+
{
|
|
985
|
+
required: true,
|
|
986
|
+
message: '请输入请求头名称'
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
pattern: /^[^:]+$/,
|
|
990
|
+
message: '请求头名称不能包含冒号'
|
|
991
|
+
}
|
|
992
|
+
]}
|
|
993
|
+
>
|
|
994
|
+
<Input placeholder="Header-Name" />
|
|
995
|
+
</Form.Item>
|
|
996
|
+
</Col>
|
|
997
|
+
<Col span={12}>
|
|
998
|
+
<Form.Item
|
|
999
|
+
{...restField}
|
|
1000
|
+
name={[name, 'headerValue']}
|
|
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>
|
|
1036
|
+
))}
|
|
1037
|
+
|
|
1038
|
+
<Form.Item>
|
|
1039
|
+
<Button
|
|
1040
|
+
type="dashed"
|
|
1041
|
+
onClick={() => add()}
|
|
1042
|
+
block
|
|
1043
|
+
icon={<PlusOutlined />}
|
|
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>
|
|
1093
|
+
</>
|
|
1094
|
+
)}
|
|
1095
|
+
</Form.List>
|
|
1096
|
+
</Form.Item>
|
|
1097
|
+
</>
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return null;
|
|
654
1102
|
}}
|
|
655
|
-
label={
|
|
656
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
657
|
-
<span>响应内容</span>
|
|
658
|
-
<Space>
|
|
659
|
-
<Button
|
|
660
|
-
type="link"
|
|
661
|
-
icon={<EyeOutlined />}
|
|
662
|
-
onClick={handlePreview}
|
|
663
|
-
style={{ padding: 0 }}
|
|
664
|
-
>
|
|
665
|
-
预览
|
|
666
|
-
</Button>
|
|
667
|
-
<Button
|
|
668
|
-
type="link"
|
|
669
|
-
icon={<FormatPainterOutlined />}
|
|
670
|
-
onClick={formatJsonContent}
|
|
671
|
-
style={{ padding: 0 }}
|
|
672
|
-
>
|
|
673
|
-
格式化JSON
|
|
674
|
-
</Button>
|
|
675
|
-
</Space>
|
|
676
|
-
</div>
|
|
677
|
-
}
|
|
678
|
-
rules={[{ required: true, message: '请输入响应内容' }]}
|
|
679
|
-
>
|
|
680
|
-
<TextArea rows={12} placeholder="请输入响应内容" style={{ fontFamily: 'monospace' }} />
|
|
681
1103
|
</Form.Item>
|
|
682
1104
|
</Form>
|
|
683
1105
|
</Modal>
|
|
@@ -703,14 +1125,45 @@ const InterfaceManagement = () => {
|
|
|
703
1125
|
description={
|
|
704
1126
|
<div>
|
|
705
1127
|
<p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
|
|
706
|
-
<p
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<
|
|
713
|
-
|
|
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
|
+
|
|
714
1167
|
<p>请在下方输入完整测试URL进行测试</p>
|
|
715
1168
|
</div>
|
|
716
1169
|
}
|
|
@@ -761,13 +1214,63 @@ const InterfaceManagement = () => {
|
|
|
761
1214
|
<span className="value">{testResult.httpMethod || 'ALL'}</span>
|
|
762
1215
|
</div>
|
|
763
1216
|
<div className="result-item">
|
|
764
|
-
<span className="label"
|
|
765
|
-
<span className="value">
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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>
|
|
770
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
|
+
|
|
771
1274
|
{testResult.mockInfo && (
|
|
772
1275
|
<>
|
|
773
1276
|
<div className="result-item">
|
|
@@ -780,10 +1283,13 @@ const InterfaceManagement = () => {
|
|
|
780
1283
|
</div>
|
|
781
1284
|
</>
|
|
782
1285
|
)}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
<
|
|
786
|
-
|
|
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
|
+
)}
|
|
787
1293
|
</div>
|
|
788
1294
|
) : (
|
|
789
1295
|
<div className="result-error">
|