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.
@@ -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
- // 验证 URL 匹配规则格式
192
- if (!isValidPattern(values.pattern)) {
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
- // 清理并验证JSON
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: 'response',
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: '测试URL与接口匹配规则不匹配'
356
+ error: errorMessage
296
357
  });
297
- message.error('测试URL与接口匹配规则不匹配');
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={[{ required: true, message: '请输入URL匹配规则' }]}
601
- tooltip="支持多种匹配方式:精确匹配 - /api/users,通配符 - /api/users/*,正则表达式 - /api\/users\/\d+/"
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 placeholder="例如:/api/users,/api/users/*,/api\/users\/\d+/" />
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="statusCode"
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
- {statusCodes.map(item => (
617
- <Option key={item.value} value={item.value}>{item.label}</Option>
618
- ))}
619
- </Select>
620
- </Form.Item>
621
- </div>
622
- <div style={{ flex: 1 }}>
623
- <Form.Item
624
- name="contentType"
625
- label="内容类型"
626
- rules={[{ required: true, message: '请选择内容类型' }]}
627
- >
628
- <Select>
629
- {contentTypes.map(item => (
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
- <Form.Item
651
- name="responseBody"
652
- labelCol={{
653
- span: 8, /* 宽度比例 */
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>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugins://,然后插件会根据接口的匹配规则处理请求。</p>
707
- <p>插件支持以下匹配方式:</p>
708
- <ul>
709
- <li>精确匹配:完全匹配URL路径部分</li>
710
- <li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
711
- <li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
712
- <li>部分匹配:URL包含指定路径即匹配</li>
713
- </ul>
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">状态码:</span>
765
- <span className="value">{testResult.statusCode}</span>
766
- </div>
767
- <div className="result-item">
768
- <span className="label">内容类型:</span>
769
- <span className="value">{testResult.contentType}</span>
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
- <div className="result-body">
784
- <div className="label">响应内容:</div>
785
- <pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
786
- </div>
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">