whistle.mock-plugins 1.0.2 → 1.0.4
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/logs.js +38 -29
- package/app/cgi-bin/test-interface.js +182 -35
- package/app/public/js/index.js +1 -1
- package/lib/dataManager.js +6 -0
- package/lib/ruleManager.js +62 -30
- package/lib/rules-server.js +126 -5
- package/lib/server.js +74 -3
- package/package.json +1 -1
- package/rules.txt +1 -1
- package/ui/src/pages/InterfaceManagement.js +430 -136
|
@@ -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' },
|
|
@@ -134,10 +140,12 @@ const InterfaceManagement = () => {
|
|
|
134
140
|
form.setFieldsValue({
|
|
135
141
|
name: record.name,
|
|
136
142
|
pattern: record.urlPattern,
|
|
143
|
+
proxyType: record.proxyType || 'response',
|
|
137
144
|
statusCode: record.httpStatus?.toString() || '200',
|
|
138
145
|
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
139
146
|
responseBody: record.responseContent || '',
|
|
140
147
|
httpMethod: record.httpMethod || 'ALL',
|
|
148
|
+
targetUrl: record.targetUrl || '',
|
|
141
149
|
});
|
|
142
150
|
setModalVisible(true);
|
|
143
151
|
};
|
|
@@ -174,7 +182,6 @@ const InterfaceManagement = () => {
|
|
|
174
182
|
}
|
|
175
183
|
};
|
|
176
184
|
|
|
177
|
-
// 清理JSON响应内容中不需要的空白和格式化
|
|
178
185
|
const cleanJsonResponse = (jsonStr) => {
|
|
179
186
|
try {
|
|
180
187
|
const parsed = JSON.parse(jsonStr);
|
|
@@ -184,18 +191,24 @@ const InterfaceManagement = () => {
|
|
|
184
191
|
}
|
|
185
192
|
};
|
|
186
193
|
|
|
194
|
+
// 验证URL是否合法
|
|
195
|
+
const isValidUrl = (url) => {
|
|
196
|
+
if (!url) return false;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
new URL(url);
|
|
200
|
+
return true;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
187
206
|
const handleSubmit = async () => {
|
|
188
207
|
try {
|
|
189
208
|
const values = await form.validateFields();
|
|
190
209
|
|
|
191
|
-
// 验证 URL 匹配规则格式
|
|
192
|
-
if (!isValidPattern(values.pattern)) {
|
|
193
|
-
message.error('URL匹配规则格式不正确');
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
210
|
// 验证响应内容格式
|
|
198
|
-
if (values.contentType.includes('json')) {
|
|
211
|
+
if (values.proxyType === 'response' && values.contentType.includes('json')) {
|
|
199
212
|
try {
|
|
200
213
|
// 清理并验证JSON
|
|
201
214
|
const cleanedJson = cleanJsonResponse(values.responseBody);
|
|
@@ -206,13 +219,26 @@ const InterfaceManagement = () => {
|
|
|
206
219
|
return;
|
|
207
220
|
}
|
|
208
221
|
}
|
|
222
|
+
|
|
223
|
+
// 验证重定向URL
|
|
224
|
+
if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !values.targetUrl) {
|
|
225
|
+
message.error('重定向目标URL不能为空');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 验证重定向URL的格式
|
|
230
|
+
if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !isValidUrl(values.targetUrl)) {
|
|
231
|
+
message.error('重定向目标URL格式不正确,请输入完整的URL,包括http://或https://');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
209
234
|
|
|
210
235
|
const interfaceData = {
|
|
211
236
|
name: values.name,
|
|
212
237
|
featureId: selectedFeatureId,
|
|
213
238
|
urlPattern: values.pattern,
|
|
214
|
-
proxyType:
|
|
215
|
-
responseContent: values.responseBody,
|
|
239
|
+
proxyType: values.proxyType,
|
|
240
|
+
responseContent: values.proxyType === 'response' ? values.responseBody : '',
|
|
241
|
+
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
216
242
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
217
243
|
contentType: values.contentType,
|
|
218
244
|
responseDelay: 0,
|
|
@@ -247,29 +273,6 @@ const InterfaceManagement = () => {
|
|
|
247
273
|
}
|
|
248
274
|
};
|
|
249
275
|
|
|
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
276
|
const handleCancel = () => {
|
|
274
277
|
setModalVisible(false);
|
|
275
278
|
};
|
|
@@ -282,6 +285,45 @@ const InterfaceManagement = () => {
|
|
|
282
285
|
testForm.resetFields();
|
|
283
286
|
};
|
|
284
287
|
|
|
288
|
+
// 检查 URL 是否匹配模式
|
|
289
|
+
const isUrlMatchPattern = (url, pattern, proxyType) => {
|
|
290
|
+
if (!url || !pattern) return false;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// 对于url_redirect类型,需要完全匹配
|
|
294
|
+
if (proxyType === 'url_redirect') {
|
|
295
|
+
return url === pattern;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
|
|
299
|
+
if (proxyType === 'redirect') {
|
|
300
|
+
return url.indexOf(pattern) === 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 以下是默认的匹配逻辑(用于response类型等)
|
|
304
|
+
// 如果是正则表达式
|
|
305
|
+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
306
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
307
|
+
return regex.test(url);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 如果是通配符模式
|
|
311
|
+
if (pattern.includes('*')) {
|
|
312
|
+
const regexPattern = pattern
|
|
313
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
314
|
+
.replace(/\*/g, '.*');
|
|
315
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
316
|
+
return regex.test(url);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 精确匹配
|
|
320
|
+
return url === pattern;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.error('URL匹配检查失败:', e);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
285
327
|
const handleTestSubmit = async () => {
|
|
286
328
|
try {
|
|
287
329
|
const values = await testForm.validateFields();
|
|
@@ -289,12 +331,21 @@ const InterfaceManagement = () => {
|
|
|
289
331
|
|
|
290
332
|
try {
|
|
291
333
|
// 验证测试 URL 是否匹配当前接口的匹配规则
|
|
292
|
-
if (!isUrlMatchPattern(values.testUrl, editingInterface.urlPattern)) {
|
|
334
|
+
if (!isUrlMatchPattern(values.testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
|
|
335
|
+
let errorMessage = '测试URL与接口匹配规则不匹配';
|
|
336
|
+
|
|
337
|
+
// 根据不同的代理类型提供更具体的错误信息
|
|
338
|
+
if (editingInterface.proxyType === 'url_redirect') {
|
|
339
|
+
errorMessage = `测试URL必须完全匹配 ${editingInterface.urlPattern}`;
|
|
340
|
+
} else if (editingInterface.proxyType === 'redirect') {
|
|
341
|
+
errorMessage = `测试URL必须以 ${editingInterface.urlPattern} 开头`;
|
|
342
|
+
}
|
|
343
|
+
|
|
293
344
|
setTestResult({
|
|
294
345
|
success: false,
|
|
295
|
-
error:
|
|
346
|
+
error: errorMessage
|
|
296
347
|
});
|
|
297
|
-
message.error(
|
|
348
|
+
message.error(errorMessage);
|
|
298
349
|
return;
|
|
299
350
|
}
|
|
300
351
|
|
|
@@ -336,34 +387,6 @@ const InterfaceManagement = () => {
|
|
|
336
387
|
}
|
|
337
388
|
};
|
|
338
389
|
|
|
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
390
|
const handleSelectFeature = (featureId) => {
|
|
368
391
|
setSelectedFeatureId(featureId);
|
|
369
392
|
};
|
|
@@ -381,6 +404,11 @@ const InterfaceManagement = () => {
|
|
|
381
404
|
};
|
|
382
405
|
|
|
383
406
|
const formatJsonContent = () => {
|
|
407
|
+
const proxyType = form.getFieldValue('proxyType');
|
|
408
|
+
if (proxyType !== 'response') {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
384
412
|
const responseBody = form.getFieldValue('responseBody');
|
|
385
413
|
if (responseBody) {
|
|
386
414
|
try {
|
|
@@ -394,6 +422,11 @@ const InterfaceManagement = () => {
|
|
|
394
422
|
};
|
|
395
423
|
|
|
396
424
|
const handlePreview = () => {
|
|
425
|
+
const proxyType = form.getFieldValue('proxyType');
|
|
426
|
+
if (proxyType !== 'response') {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
397
430
|
const responseBody = form.getFieldValue('responseBody');
|
|
398
431
|
if (responseBody) {
|
|
399
432
|
try {
|
|
@@ -442,22 +475,47 @@ const InterfaceManagement = () => {
|
|
|
442
475
|
key: 'urlPattern',
|
|
443
476
|
ellipsis: true,
|
|
444
477
|
},
|
|
478
|
+
{
|
|
479
|
+
title: '处理方式',
|
|
480
|
+
dataIndex: 'proxyType',
|
|
481
|
+
key: 'proxyType',
|
|
482
|
+
width: 120,
|
|
483
|
+
render: (text) => {
|
|
484
|
+
const found = proxyTypes.find(item => item.value === text);
|
|
485
|
+
return found ? found.label : text || '模拟响应';
|
|
486
|
+
}
|
|
487
|
+
},
|
|
445
488
|
{
|
|
446
489
|
title: '状态码',
|
|
447
490
|
dataIndex: 'httpStatus',
|
|
448
491
|
key: 'httpStatus',
|
|
449
492
|
width: 100,
|
|
493
|
+
render: (text, record) => {
|
|
494
|
+
return record.proxyType === 'response' ? text : '-';
|
|
495
|
+
}
|
|
450
496
|
},
|
|
451
497
|
{
|
|
452
498
|
title: '内容类型',
|
|
453
499
|
dataIndex: 'contentType',
|
|
454
500
|
key: 'contentType',
|
|
455
501
|
width: 120,
|
|
456
|
-
render: (text) => {
|
|
502
|
+
render: (text, record) => {
|
|
503
|
+
if (record.proxyType !== 'response') {
|
|
504
|
+
return '-';
|
|
505
|
+
}
|
|
457
506
|
const found = contentTypes.find(item => item.value === text);
|
|
458
507
|
return found ? found.label : text;
|
|
459
508
|
}
|
|
460
509
|
},
|
|
510
|
+
{
|
|
511
|
+
title: '目标URL',
|
|
512
|
+
dataIndex: 'targetUrl',
|
|
513
|
+
key: 'targetUrl',
|
|
514
|
+
ellipsis: true,
|
|
515
|
+
render: (text, record) => {
|
|
516
|
+
return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
|
|
517
|
+
}
|
|
518
|
+
},
|
|
461
519
|
{
|
|
462
520
|
title: '请求方法',
|
|
463
521
|
dataIndex: 'httpMethod',
|
|
@@ -577,10 +635,12 @@ const InterfaceManagement = () => {
|
|
|
577
635
|
initialValues={{
|
|
578
636
|
name: '',
|
|
579
637
|
pattern: '',
|
|
638
|
+
proxyType: 'response',
|
|
580
639
|
statusCode: '200',
|
|
581
640
|
contentType: 'application/json; charset=utf-8',
|
|
582
641
|
responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
|
|
583
|
-
httpMethod: 'ALL'
|
|
642
|
+
httpMethod: 'ALL',
|
|
643
|
+
targetUrl: ''
|
|
584
644
|
}}
|
|
585
645
|
>
|
|
586
646
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
@@ -597,36 +657,120 @@ const InterfaceManagement = () => {
|
|
|
597
657
|
<Form.Item
|
|
598
658
|
name="pattern"
|
|
599
659
|
label="URL匹配规则"
|
|
600
|
-
rules={[
|
|
601
|
-
|
|
660
|
+
rules={[
|
|
661
|
+
{ required: true, message: '请输入URL匹配规则' },
|
|
662
|
+
({ getFieldValue }) => ({
|
|
663
|
+
validator(_, value) {
|
|
664
|
+
const proxyType = getFieldValue('proxyType');
|
|
665
|
+
if (!value) return Promise.resolve();
|
|
666
|
+
|
|
667
|
+
// URL重定向模式下,必须是完整URL
|
|
668
|
+
if (proxyType === 'url_redirect') {
|
|
669
|
+
try {
|
|
670
|
+
new URL(value);
|
|
671
|
+
return Promise.resolve();
|
|
672
|
+
} catch (e) {
|
|
673
|
+
return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL(包括http://或https://)'));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 重定向模式下,必须以http://或https://开头
|
|
678
|
+
if (proxyType === 'redirect') {
|
|
679
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
680
|
+
return Promise.resolve();
|
|
681
|
+
}
|
|
682
|
+
return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头'));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 响应模式下的验证
|
|
686
|
+
if (proxyType === 'response') {
|
|
687
|
+
// 正则表达式验证
|
|
688
|
+
if (value.startsWith('/') && value.endsWith('/')) {
|
|
689
|
+
try {
|
|
690
|
+
new RegExp(value.slice(1, -1));
|
|
691
|
+
return Promise.resolve();
|
|
692
|
+
} catch (e) {
|
|
693
|
+
return Promise.reject(new Error('无效的正则表达式格式'));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// 通配符验证
|
|
698
|
+
if (value.includes('*')) {
|
|
699
|
+
if (/^[a-zA-Z0-9\-_/.*]+$/.test(value)) {
|
|
700
|
+
return Promise.resolve();
|
|
701
|
+
}
|
|
702
|
+
return Promise.reject(new Error('通配符URL格式不正确'));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 普通路径验证
|
|
706
|
+
if (/^[a-zA-Z0-9\-_/]+$/.test(value)) {
|
|
707
|
+
return Promise.resolve();
|
|
708
|
+
}
|
|
709
|
+
return Promise.reject(new Error('URL路径格式不正确'));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return Promise.resolve();
|
|
713
|
+
},
|
|
714
|
+
}),
|
|
715
|
+
]}
|
|
716
|
+
tooltip={{
|
|
717
|
+
title: (
|
|
718
|
+
<>
|
|
719
|
+
<div>不同处理方式下URL匹配规则要求:</div>
|
|
720
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
721
|
+
<li><b>模拟响应:</b> 支持路径格式如 /api/users,通配符如 /api/*,正则如 /\/api\/\d+/</li>
|
|
722
|
+
<li><b>重定向:</b> 必须以http://或https://开头,例如:https://example.com/api</li>
|
|
723
|
+
<li><b>URL重定向:</b> 必须是完整URL,包括http://或https://,例如:https://example.com/api/users</li>
|
|
724
|
+
</ul>
|
|
725
|
+
</>
|
|
726
|
+
),
|
|
727
|
+
overlayStyle: { maxWidth: '450px' }
|
|
728
|
+
}}
|
|
602
729
|
>
|
|
603
|
-
<Input
|
|
730
|
+
<Input
|
|
731
|
+
placeholder="根据选择的处理方式输入相应格式的URL"
|
|
732
|
+
/>
|
|
604
733
|
</Form.Item>
|
|
605
734
|
</div>
|
|
606
735
|
</div>
|
|
607
|
-
|
|
736
|
+
|
|
608
737
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
609
738
|
<div style={{ flex: 1 }}>
|
|
610
739
|
<Form.Item
|
|
611
|
-
name="
|
|
612
|
-
label="
|
|
613
|
-
rules={[{ required: true, message: '
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
740
|
+
name="proxyType"
|
|
741
|
+
label="处理方式"
|
|
742
|
+
rules={[{ required: true, message: '请选择处理方式' }]}
|
|
743
|
+
tooltip={{
|
|
744
|
+
title: (
|
|
745
|
+
<>
|
|
746
|
+
<div>不同处理方式的规则说明:</div>
|
|
747
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
748
|
+
<li><b>模拟响应:</b> 返回您定义的响应内容</li>
|
|
749
|
+
<li><b>重定向:</b> 将请求重定向到其他URL,URL匹配规则必须以http://或https://开头</li>
|
|
750
|
+
<li><b>URL重定向:</b> 完全匹配URL时重定向,URL匹配规则必须是完整URL</li>
|
|
751
|
+
</ul>
|
|
752
|
+
</>
|
|
753
|
+
),
|
|
754
|
+
overlayStyle: { maxWidth: '450px' }
|
|
755
|
+
}}
|
|
627
756
|
>
|
|
628
|
-
<Select
|
|
629
|
-
|
|
757
|
+
<Select onChange={(value) => {
|
|
758
|
+
// 当切换代理类型时,清空pattern字段,并提供不同的placeholder
|
|
759
|
+
form.setFieldsValue({ pattern: '' });
|
|
760
|
+
|
|
761
|
+
// 为不同的代理类型提供不同的pattern占位符
|
|
762
|
+
const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
|
|
763
|
+
if (patternInput) {
|
|
764
|
+
if (value === 'response') {
|
|
765
|
+
patternInput.placeholder = "例如:/api/users,/api/users/*,/api\/users\/\d+/";
|
|
766
|
+
} else if (value === 'redirect') {
|
|
767
|
+
patternInput.placeholder = "例如:https://example.com/api";
|
|
768
|
+
} else if (value === 'url_redirect') {
|
|
769
|
+
patternInput.placeholder = "例如:https://example.com/api/users";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}}>
|
|
773
|
+
{proxyTypes.map(item => (
|
|
630
774
|
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
631
775
|
))}
|
|
632
776
|
</Select>
|
|
@@ -647,37 +791,131 @@ const InterfaceManagement = () => {
|
|
|
647
791
|
</div>
|
|
648
792
|
</div>
|
|
649
793
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
794
|
+
{/* 根据proxyType显示不同的表单项 */}
|
|
795
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
796
|
+
{({ getFieldValue }) => {
|
|
797
|
+
const proxyType = getFieldValue('proxyType');
|
|
798
|
+
|
|
799
|
+
if (proxyType === 'response') {
|
|
800
|
+
return (
|
|
801
|
+
<>
|
|
802
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
803
|
+
<div style={{ flex: 1 }}>
|
|
804
|
+
<Form.Item
|
|
805
|
+
name="statusCode"
|
|
806
|
+
label="状态码"
|
|
807
|
+
rules={[{ required: true, message: '请选择状态码' }]}
|
|
808
|
+
>
|
|
809
|
+
<Select>
|
|
810
|
+
{statusCodes.map(item => (
|
|
811
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
812
|
+
))}
|
|
813
|
+
</Select>
|
|
814
|
+
</Form.Item>
|
|
815
|
+
</div>
|
|
816
|
+
<div style={{ flex: 1 }}>
|
|
817
|
+
<Form.Item
|
|
818
|
+
name="contentType"
|
|
819
|
+
label="内容类型"
|
|
820
|
+
rules={[{ required: true, message: '请选择内容类型' }]}
|
|
821
|
+
>
|
|
822
|
+
<Select>
|
|
823
|
+
{contentTypes.map(item => (
|
|
824
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
825
|
+
))}
|
|
826
|
+
</Select>
|
|
827
|
+
</Form.Item>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<Form.Item
|
|
832
|
+
name="responseBody"
|
|
833
|
+
labelCol={{
|
|
834
|
+
span: 8, /* 宽度比例 */
|
|
835
|
+
}}
|
|
836
|
+
label={
|
|
837
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
838
|
+
<span>响应内容</span>
|
|
839
|
+
<Space>
|
|
840
|
+
<Button
|
|
841
|
+
type="link"
|
|
842
|
+
icon={<EyeOutlined />}
|
|
843
|
+
onClick={handlePreview}
|
|
844
|
+
style={{ padding: 0 }}
|
|
845
|
+
>
|
|
846
|
+
预览
|
|
847
|
+
</Button>
|
|
848
|
+
<Button
|
|
849
|
+
type="link"
|
|
850
|
+
icon={<FormatPainterOutlined />}
|
|
851
|
+
onClick={formatJsonContent}
|
|
852
|
+
style={{ padding: 0 }}
|
|
853
|
+
>
|
|
854
|
+
格式化JSON
|
|
855
|
+
</Button>
|
|
856
|
+
</Space>
|
|
857
|
+
</div>
|
|
858
|
+
}
|
|
859
|
+
rules={[{ required: true, message: '请输入响应内容' }]}
|
|
860
|
+
>
|
|
861
|
+
<TextArea rows={12} placeholder="请输入响应内容" style={{ fontFamily: 'monospace' }} />
|
|
862
|
+
</Form.Item>
|
|
863
|
+
</>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
868
|
+
return (
|
|
869
|
+
<Form.Item
|
|
870
|
+
name="targetUrl"
|
|
871
|
+
label="重定向目标URL"
|
|
872
|
+
rules={[
|
|
873
|
+
{ required: true, message: '请输入重定向目标URL' },
|
|
874
|
+
{
|
|
875
|
+
validator: (_, value) => {
|
|
876
|
+
if (!value) return Promise.resolve();
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
new URL(value);
|
|
880
|
+
return Promise.resolve();
|
|
881
|
+
} catch (e) {
|
|
882
|
+
return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
]}
|
|
887
|
+
tooltip={proxyType === 'redirect' ?
|
|
888
|
+
"重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
|
|
889
|
+
"URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
|
|
890
|
+
}
|
|
672
891
|
>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
892
|
+
<Input
|
|
893
|
+
placeholder={proxyType === 'redirect' ?
|
|
894
|
+
"例如:https://example.com/api" :
|
|
895
|
+
"例如:https://example.com/api/users"
|
|
896
|
+
}
|
|
897
|
+
addonBefore={
|
|
898
|
+
<Select
|
|
899
|
+
defaultValue="https://"
|
|
900
|
+
className="select-before"
|
|
901
|
+
style={{ width: 100 }}
|
|
902
|
+
onChange={(value) => {
|
|
903
|
+
const currentUrl = form.getFieldValue('targetUrl') || '';
|
|
904
|
+
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
|
|
905
|
+
form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
|
|
906
|
+
}}
|
|
907
|
+
>
|
|
908
|
+
<Option value="http://">http://</Option>
|
|
909
|
+
<Option value="https://">https://</Option>
|
|
910
|
+
</Select>
|
|
911
|
+
}
|
|
912
|
+
/>
|
|
913
|
+
</Form.Item>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return null;
|
|
918
|
+
}}
|
|
681
919
|
</Form.Item>
|
|
682
920
|
</Form>
|
|
683
921
|
</Modal>
|
|
@@ -703,14 +941,45 @@ const InterfaceManagement = () => {
|
|
|
703
941
|
description={
|
|
704
942
|
<div>
|
|
705
943
|
<p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
|
|
706
|
-
<p
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<
|
|
713
|
-
|
|
944
|
+
<p>代理类型:{
|
|
945
|
+
proxyTypes.find(item => item.value === editingInterface.proxyType)?.label ||
|
|
946
|
+
editingInterface.proxyType || '模拟响应'
|
|
947
|
+
}</p>
|
|
948
|
+
|
|
949
|
+
{editingInterface.proxyType === 'url_redirect' && (
|
|
950
|
+
<div>
|
|
951
|
+
<p><b>URL重定向模式规则:</b></p>
|
|
952
|
+
<ul>
|
|
953
|
+
<li>URL匹配规则必须是完整URL(包括http://或https://)</li>
|
|
954
|
+
<li>需要完全匹配URL路径,测试URL必须与匹配规则<b>完全一致</b></li>
|
|
955
|
+
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
956
|
+
</ul>
|
|
957
|
+
</div>
|
|
958
|
+
)}
|
|
959
|
+
|
|
960
|
+
{editingInterface.proxyType === 'redirect' && (
|
|
961
|
+
<div>
|
|
962
|
+
<p><b>重定向模式规则:</b></p>
|
|
963
|
+
<ul>
|
|
964
|
+
<li>URL匹配规则必须以http://或https://开头</li>
|
|
965
|
+
<li>测试URL必须以匹配规则开头(<b>前缀匹配</b>)</li>
|
|
966
|
+
<li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
|
|
967
|
+
</ul>
|
|
968
|
+
</div>
|
|
969
|
+
)}
|
|
970
|
+
|
|
971
|
+
{editingInterface.proxyType === 'response' && (
|
|
972
|
+
<div>
|
|
973
|
+
<p>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugins://,然后插件会根据接口的匹配规则处理请求。</p>
|
|
974
|
+
<p>插件支持以下匹配方式:</p>
|
|
975
|
+
<ul>
|
|
976
|
+
<li>精确匹配:完全匹配URL路径部分</li>
|
|
977
|
+
<li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
|
|
978
|
+
<li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
|
|
979
|
+
</ul>
|
|
980
|
+
</div>
|
|
981
|
+
)}
|
|
982
|
+
|
|
714
983
|
<p>请在下方输入完整测试URL进行测试</p>
|
|
715
984
|
</div>
|
|
716
985
|
}
|
|
@@ -761,13 +1030,35 @@ const InterfaceManagement = () => {
|
|
|
761
1030
|
<span className="value">{testResult.httpMethod || 'ALL'}</span>
|
|
762
1031
|
</div>
|
|
763
1032
|
<div className="result-item">
|
|
764
|
-
<span className="label"
|
|
765
|
-
<span className="value">
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1033
|
+
<span className="label">处理方式:</span>
|
|
1034
|
+
<span className="value">
|
|
1035
|
+
{(() => {
|
|
1036
|
+
const found = proxyTypes.find(item => item.value === editingInterface.proxyType);
|
|
1037
|
+
return found ? found.label : editingInterface.proxyType || '模拟响应';
|
|
1038
|
+
})()}
|
|
1039
|
+
</span>
|
|
770
1040
|
</div>
|
|
1041
|
+
|
|
1042
|
+
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') && (
|
|
1043
|
+
<div className="result-item">
|
|
1044
|
+
<span className="label">重定向目标:</span>
|
|
1045
|
+
<span className="value">{testResult.targetUrl || editingInterface.targetUrl}</span>
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
1048
|
+
|
|
1049
|
+
{editingInterface.proxyType === 'response' && (
|
|
1050
|
+
<>
|
|
1051
|
+
<div className="result-item">
|
|
1052
|
+
<span className="label">状态码:</span>
|
|
1053
|
+
<span className="value">{testResult.statusCode}</span>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div className="result-item">
|
|
1056
|
+
<span className="label">内容类型:</span>
|
|
1057
|
+
<span className="value">{testResult.contentType}</span>
|
|
1058
|
+
</div>
|
|
1059
|
+
</>
|
|
1060
|
+
)}
|
|
1061
|
+
|
|
771
1062
|
{testResult.mockInfo && (
|
|
772
1063
|
<>
|
|
773
1064
|
<div className="result-item">
|
|
@@ -780,10 +1071,13 @@ const InterfaceManagement = () => {
|
|
|
780
1071
|
</div>
|
|
781
1072
|
</>
|
|
782
1073
|
)}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
<
|
|
786
|
-
|
|
1074
|
+
|
|
1075
|
+
{editingInterface.proxyType === 'response' && (
|
|
1076
|
+
<div className="result-body">
|
|
1077
|
+
<div className="label">响应内容:</div>
|
|
1078
|
+
<pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
|
|
1079
|
+
</div>
|
|
1080
|
+
)}
|
|
787
1081
|
</div>
|
|
788
1082
|
) : (
|
|
789
1083
|
<div className="result-error">
|