whistle.mock-plugins 1.2.2-beta.13 → 1.2.2-beta.18

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.
@@ -31,6 +31,7 @@ import {
31
31
  generateResponseId
32
32
  } from '../components/interface-management';
33
33
  import DataManagementModal from '../components/interface-management/DataManagementModal';
34
+ import { DataManagementDrawer } from '../components/interface-management';
34
35
 
35
36
  const { Option } = Select;
36
37
  const { Search } = AntInput;
@@ -42,20 +43,17 @@ const COLUMN_CONFIG = [
42
43
  { key: 'group', title: '分组', required: false },
43
44
  { key: 'urlPattern', title: 'URL匹配规则', required: false },
44
45
  { key: 'proxyType', title: '处理方式', required: false },
45
- { key: 'currentResponse', title: '响应配置', required: false },
46
+ { key: 'currentResponse', title: '当前响应', required: false },
46
47
  { key: 'responseDelay', title: '延迟(毫秒)', required: false },
47
48
  { key: 'httpStatus', title: '状态码', required: false },
48
49
  { key: 'contentType', title: '内容类型', required: false },
49
50
  { key: 'targetUrl', title: '目标URL', required: false },
50
51
  { key: 'customHeaders', title: '自定义头', required: false },
51
- { key: 'paramMatchers', title: '匹配规则', required: false },
52
+ { key: 'paramMatchers', title: '参数匹配', required: false },
52
53
  { key: 'httpMethod', title: '请求方法', required: false },
53
54
  { key: 'action', title: '操作', required: true }
54
55
  ];
55
56
 
56
- // 透传字段组件:不渲染任何 UI,仅让 Form 能存储任意类型的值(数组/对象)
57
- const PassthroughField = ({ value, onChange }) => null;
58
-
59
57
  const InterfaceManagement = () => {
60
58
  const { featureId } = useParams();
61
59
  const history = useHistory();
@@ -92,7 +90,13 @@ const InterfaceManagement = () => {
92
90
  const [searchValue, setSearchValue] = useState('');
93
91
  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
94
92
  const [drawerVisible, setDrawerVisible] = useState(false);
95
-
93
+
94
+ // 数据管理显示模式(modal / drawer)
95
+ const [displayMode, setDisplayMode] = useState('modal');
96
+
97
+ // 接口表单显示模式(modal / drawer)
98
+ const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
99
+
96
100
  // 表单实例
97
101
  const [form] = Form.useForm();
98
102
 
@@ -126,6 +130,46 @@ const InterfaceManagement = () => {
126
130
  }
127
131
  }, [selectedFeatureId]);
128
132
 
133
+ // 监听数据管理显示模式变化
134
+ useEffect(() => {
135
+ const savedMode = localStorage.getItem('dataManagementDisplayMode');
136
+ if (savedMode) {
137
+ setDisplayMode(savedMode);
138
+ }
139
+
140
+ const handleStorageChange = () => {
141
+ const newMode = localStorage.getItem('dataManagementDisplayMode');
142
+ if (newMode) {
143
+ setDisplayMode(newMode);
144
+ }
145
+ };
146
+
147
+ window.addEventListener('storage', handleStorageChange);
148
+ return () => {
149
+ window.removeEventListener('storage', handleStorageChange);
150
+ };
151
+ }, []);
152
+
153
+ // 监听接口表单显示模式变化
154
+ useEffect(() => {
155
+ const savedMode = localStorage.getItem('interfaceFormDisplayMode');
156
+ if (savedMode) {
157
+ setInterfaceFormDisplayMode(savedMode);
158
+ }
159
+
160
+ const handleStorageChange = () => {
161
+ const newMode = localStorage.getItem('interfaceFormDisplayMode');
162
+ if (newMode) {
163
+ setInterfaceFormDisplayMode(newMode);
164
+ }
165
+ };
166
+
167
+ window.addEventListener('storage', handleStorageChange);
168
+ return () => {
169
+ window.removeEventListener('storage', handleStorageChange);
170
+ };
171
+ }, []);
172
+
129
173
  // 获取当前选中的功能模块
130
174
  const selectedFeature = features.find(f => f.id === selectedFeatureId);
131
175
 
@@ -182,7 +226,8 @@ const InterfaceManagement = () => {
182
226
  id: resp.id || `resp-${index}-${Date.now()}`,
183
227
  name: resp.name || `响应 ${index + 1}`,
184
228
  description: resp.description || '',
185
- content: resp.content || '{}'
229
+ content: resp.content || '{}',
230
+ paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
186
231
  }));
187
232
 
188
233
  return item;
@@ -388,7 +433,8 @@ const InterfaceManagement = () => {
388
433
  id: newId,
389
434
  name: resp.name || '未命名响应',
390
435
  description: resp.description || '',
391
- content: resp.content || '{}'
436
+ content: resp.content || '{}',
437
+ paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers.map(m => ({ ...m })) : []
392
438
  };
393
439
  });
394
440
  const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
@@ -504,6 +550,14 @@ const InterfaceManagement = () => {
504
550
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
505
551
  }
506
552
 
553
+ // 处理 responses,清理每个响应中的空 paramMatchers 规则
554
+ const cleanedResponses = (values.responses || (editingInterface ? editingInterface.responses : [])).map(resp => ({
555
+ ...resp,
556
+ paramMatchers: Array.isArray(resp.paramMatchers)
557
+ ? resp.paramMatchers.filter(m => m && m.paramPath && m.paramValue)
558
+ : []
559
+ }));
560
+
507
561
  // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
508
562
  const interfaceData = {
509
563
  name: values.name || (editingInterface ? editingInterface.name : ''),
@@ -511,7 +565,7 @@ const InterfaceManagement = () => {
511
565
  urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
512
566
  proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
513
567
  featureId: selectedFeatureId,
514
- responses: values.responses || (editingInterface ? editingInterface.responses : []),
568
+ responses: cleanedResponses,
515
569
  activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
516
570
  httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
517
571
  contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
@@ -803,74 +857,47 @@ const InterfaceManagement = () => {
803
857
  },
804
858
  },
805
859
  {
806
- title: '响应配置',
860
+ title: '当前响应',
807
861
  dataIndex: 'activeResponseId',
808
862
  key: 'currentResponse',
809
- width: 160,
863
+ width: 200,
810
864
  render: (activeResponseId, record) => {
811
- if (record.proxyType !== 'response') {
812
- return <span style={{ color: '#ccc' }}>-</span>;
813
- }
865
+ // 如果没有响应数据,返回空
814
866
  if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
815
- return <span style={{ color: '#ff4d4f', fontSize: '12px' }}>无响应数据</span>;
867
+ return <span className="no-response">无响应数据</span>;
816
868
  }
817
869
 
818
- const total = record.responses.length;
819
- const hasSmartMatch = record.responses.some(
820
- r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0
821
- );
822
- const fallbackResp = record.responses.find(r => r.id === activeResponseId) || record.responses[0];
823
-
824
- const tooltipContent = (
825
- <div style={{ maxWidth: 260 }}>
826
- {record.responses.map((resp) => {
827
- const matchers = resp.paramMatchers || [];
870
+ return (
871
+ <Select
872
+ value={activeResponseId || record.responses[0]?.id}
873
+ style={{ width: '100%' }}
874
+ onChange={(value) => handleResponseSwitch(record.id, value)}
875
+ disabled={record.proxyType !== 'response'}
876
+ optionLabelProp="label"
877
+ >
878
+ {record.responses.map(resp => {
879
+ const matcherCount = (resp.paramMatchers || []).filter(m => m.paramPath).length;
880
+ const isDefault = resp.id === activeResponseId;
881
+ const labelContent = (
882
+ <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
883
+ <span>{resp.name}</span>
884
+ {isDefault && <Tag color="green" style={{ fontSize: '11px', padding: '0 4px', marginLeft: 2 }}>默认</Tag>}
885
+ {matcherCount > 0 && <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px', marginLeft: 2 }}>{matcherCount}条规则</Tag>}
886
+ </span>
887
+ );
828
888
  return (
829
- <div key={resp.id} style={{ marginBottom: 6 }}>
830
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
831
- {matchers.length > 0 ? (
832
- <Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>{matchers.length}条规则</Tag>
833
- ) : (
834
- <Tag color="default" style={{ fontSize: '11px', margin: 0 }}>兜底</Tag>
835
- )}
836
- <span style={{ fontWeight: resp.id === activeResponseId ? 'bold' : 'normal' }}>
837
- {resp.name}
838
- </span>
889
+ <Option key={resp.id} value={resp.id} label={labelContent}>
890
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
891
+ <span>{resp.name}</span>
892
+ <Space size={4}>
893
+ {isDefault && <Tag color="green" style={{ fontSize: '11px', padding: '0 4px' }}>默认</Tag>}
894
+ {matcherCount > 0 && <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matcherCount}条规则</Tag>}
895
+ </Space>
839
896
  </div>
840
- {matchers.length > 0 && (
841
- <div style={{ paddingLeft: 8, marginTop: 2 }}>
842
- {matchers.map((m, i) => (
843
- <div key={i} style={{ fontSize: '11px', color: '#666' }}>
844
- {m.paramPath} = {m.paramValue}
845
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
846
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
847
- </span>
848
- </div>
849
- ))}
850
- </div>
851
- )}
852
- </div>
897
+ </Option>
853
898
  );
854
899
  })}
855
- </div>
856
- );
857
-
858
- return (
859
- <Tooltip title={tooltipContent} placement="left">
860
- {hasSmartMatch ? (
861
- <Tag color="blue" style={{ cursor: 'default' }}>
862
- 智能匹配 · {total}个响应
863
- </Tag>
864
- ) : total > 1 ? (
865
- <Tag color="orange" style={{ cursor: 'default' }}>
866
- 手动切换 · {total}个响应
867
- </Tag>
868
- ) : (
869
- <span style={{ color: '#999', fontSize: '12px' }}>
870
- {fallbackResp?.name || '1个响应'}
871
- </span>
872
- )}
873
- </Tooltip>
900
+ </Select>
874
901
  );
875
902
  },
876
903
  },
@@ -987,75 +1014,76 @@ const InterfaceManagement = () => {
987
1014
  }
988
1015
  },
989
1016
  {
990
- title: '匹配规则',
991
- dataIndex: 'responses',
1017
+ title: '参数匹配',
1018
+ dataIndex: 'paramMatchers',
992
1019
  key: 'paramMatchers',
993
- width: 100,
994
- render: (responses, record) => {
1020
+ width: 120,
1021
+ render: (_, record) => {
995
1022
  if (record.proxyType !== 'response') {
996
- return <span style={{ color: '#ccc' }}>-</span>;
1023
+ return '-';
997
1024
  }
998
1025
 
999
- // 汇总所有响应的 paramMatchers(响应级),兼容旧数据接口级 paramMatchers
1000
- const responsesWithMatchers = Array.isArray(responses)
1001
- ? responses.filter(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0)
1002
- : [];
1003
- const totalRules = responsesWithMatchers.reduce((sum, r) => sum + r.paramMatchers.length, 0);
1026
+ const interfaceMatchers = record.paramMatchers || [];
1027
+ const interfaceCount = interfaceMatchers.filter(m => m.paramPath).length;
1004
1028
 
1005
- // 兼容旧数据:接口级 paramMatchers
1006
- const legacyMatchers = record.paramMatchers || [];
1007
-
1008
- if (totalRules === 0 && legacyMatchers.length === 0) {
1009
- return <span style={{ color: '#ccc' }}>-</span>;
1010
- }
1029
+ // 统计所有响应级规则
1030
+ const responsesWithMatchers = (record.responses || []).filter(resp =>
1031
+ Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
1032
+ );
1033
+ const responseMatcherTotal = responsesWithMatchers.reduce(
1034
+ (sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
1035
+ );
1011
1036
 
1012
- // 新数据:按响应分组展示
1013
- if (totalRules > 0) {
1014
- const tooltipContent = (
1015
- <div style={{ maxWidth: 280 }}>
1016
- {responsesWithMatchers.map(resp => (
1017
- <div key={resp.id} style={{ marginBottom: 6 }}>
1018
- <div style={{ fontWeight: 'bold', marginBottom: 2 }}>{resp.name}:</div>
1019
- {resp.paramMatchers.map((m, i) => (
1020
- <div key={i} style={{ paddingLeft: 8, fontSize: '12px' }}>
1021
- {m.paramPath} = {m.paramValue}
1022
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1023
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1024
- </span>
1025
- </div>
1026
- ))}
1027
- </div>
1028
- ))}
1029
- {Array.isArray(responses) && responses.some(r => !r.paramMatchers || r.paramMatchers.length === 0) && (
1030
- <div style={{ color: '#999', fontSize: '12px', marginTop: 4 }}>
1031
- + 兜底响应(无规则)
1032
- </div>
1033
- )}
1034
- </div>
1035
- );
1036
- return (
1037
- <Tooltip title={tooltipContent} placement="left">
1038
- <span style={{ color: '#1890ff', cursor: 'default' }}>{totalRules}条规则</span>
1039
- </Tooltip>
1040
- );
1037
+ if (interfaceCount === 0 && responseMatcherTotal === 0) {
1038
+ return '-';
1041
1039
  }
1042
1040
 
1043
- // 旧数据兼容展示
1044
1041
  const tooltipContent = (
1045
- <div>
1046
- {legacyMatchers.map((matcher, index) => (
1047
- <div key={index}>
1048
- {matcher.paramPath}: {matcher.paramValue}
1049
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1050
- ({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
1051
- </span>
1042
+ <div style={{ maxWidth: 280 }}>
1043
+ {interfaceCount > 0 && (
1044
+ <div style={{ marginBottom: responsesWithMatchers.length > 0 ? 8 : 0 }}>
1045
+ <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#faad14' }}>接口级条件({interfaceCount}条)</div>
1046
+ {interfaceMatchers.filter(m => m.paramPath).map((m, i) => (
1047
+ <div key={i} style={{ fontSize: '12px' }}>
1048
+ {m.paramPath}: {m.paramValue}
1049
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1050
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1051
+ </span>
1052
+ </div>
1053
+ ))}
1052
1054
  </div>
1053
- ))}
1055
+ )}
1056
+ {responsesWithMatchers.length > 0 && (
1057
+ <div>
1058
+ <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1890ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
1059
+ {responsesWithMatchers.map((resp, ri) => (
1060
+ <div key={ri} style={{ marginBottom: 4 }}>
1061
+ <div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
1062
+ {resp.paramMatchers.filter(m => m.paramPath).map((m, mi) => (
1063
+ <div key={mi} style={{ fontSize: '12px', paddingLeft: 8 }}>
1064
+ {m.paramPath}: {m.paramValue}
1065
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1066
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1067
+ </span>
1068
+ </div>
1069
+ ))}
1070
+ </div>
1071
+ ))}
1072
+ </div>
1073
+ )}
1054
1074
  </div>
1055
1075
  );
1076
+
1056
1077
  return (
1057
- <Tooltip title={tooltipContent} placement="left">
1058
- <span style={{ color: '#fa8c16', cursor: 'default' }}>{legacyMatchers.length}条(旧)</span>
1078
+ <Tooltip title={tooltipContent}>
1079
+ <Space size={4} direction="vertical" style={{ lineHeight: 1.4 }}>
1080
+ {interfaceCount > 0 && (
1081
+ <Tag color="orange" style={{ fontSize: '11px', padding: '0 4px' }}>接口:{interfaceCount}条</Tag>
1082
+ )}
1083
+ {responseMatcherTotal > 0 && (
1084
+ <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>响应:{responseMatcherTotal}条</Tag>
1085
+ )}
1086
+ </Space>
1059
1087
  </Tooltip>
1060
1088
  );
1061
1089
  }
@@ -1429,8 +1457,234 @@ const InterfaceManagement = () => {
1429
1457
  </div>
1430
1458
  </Card>
1431
1459
 
1432
- {/* 接口编辑/创建模态框 */}
1433
- <Modal
1460
+ {/* 接口编辑/创建模态框/抽屉 */}
1461
+ {interfaceFormDisplayMode === 'drawer' ? (
1462
+ <Drawer
1463
+ title={
1464
+ <div style={{
1465
+ fontSize: '18px',
1466
+ fontWeight: 600,
1467
+ color: '#262626',
1468
+ display: 'flex',
1469
+ alignItems: 'center',
1470
+ gap: '8px'
1471
+ }}>
1472
+ <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1473
+ <Badge
1474
+ count="配置向导"
1475
+ style={{
1476
+ backgroundColor: '#f0f9ff',
1477
+ color: '#1677ff',
1478
+ fontSize: '11px',
1479
+ fontWeight: 'normal'
1480
+ }}
1481
+ />
1482
+ </div>
1483
+ }
1484
+ placement="right"
1485
+ width={1040}
1486
+ open={modalVisible}
1487
+ onClose={handleCancel}
1488
+ destroyOnClose
1489
+ bodyStyle={{
1490
+ padding: '0',
1491
+ background: '#fafafa',
1492
+ overflowY: 'auto'
1493
+ }}
1494
+ footer={
1495
+ <div style={{ textAlign: 'right' }}>
1496
+ <Space>
1497
+ <Button onClick={handleCancel}>取消</Button>
1498
+ <Button type="primary" onClick={handleSubmit}>
1499
+ {editingInterface ? '保存' : '创建'}
1500
+ </Button>
1501
+ </Space>
1502
+ </div>
1503
+ }
1504
+ className="interface-drawer"
1505
+ >
1506
+ <div style={{ padding: '24px' }}>
1507
+ <Form
1508
+ form={form}
1509
+ layout="vertical"
1510
+ initialValues={{
1511
+ name: '',
1512
+ group: '',
1513
+ pattern: '',
1514
+ proxyType: 'response',
1515
+ statusCode: '200',
1516
+ contentType: 'application/json; charset=utf-8',
1517
+ responses: [],
1518
+ httpMethod: 'ALL',
1519
+ targetUrl: '',
1520
+ headerItems: [],
1521
+ paramMatchers: [],
1522
+ responseDelay: '0'
1523
+ }}
1524
+ >
1525
+ <Collapse
1526
+ defaultActiveKey={
1527
+ editingInterface && editingInterface.proxyType === 'response'
1528
+ ? ['3']
1529
+ : ['1', '2', '3']
1530
+ }
1531
+ style={{ marginBottom: '16px' }}
1532
+ ghost
1533
+ >
1534
+ {/* 第一步:基础信息 */}
1535
+ <Collapse.Panel
1536
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="1" style={{ backgroundColor: '#1677ff' }} /><span>基础信息</span></div>}
1537
+ key="1"
1538
+ >
1539
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1540
+ <div style={{ flex: 1 }}>
1541
+ <Form.Item name="name" label="接口名称" rules={[{ required: true, message: '请输入接口名称' }]}>
1542
+ <Input placeholder="请输入接口名称" />
1543
+ </Form.Item>
1544
+ </div>
1545
+ <div style={{ flex: 1 }}>
1546
+ <Form.Item name="group" label="分组" tooltip="为接口设置分组,便于管理和批量操作">
1547
+ <Select placeholder="选择或创建分组" showSearch allowClear style={{ width: '100%' }} mode="tags" maxTagCount="responsive" maxTagTextLength={10}
1548
+ onChange={(value) => {
1549
+ if (Array.isArray(value) && value.length > 0) {
1550
+ const groupValue = value[0] === '' ? undefined : value[0];
1551
+ form.setFieldsValue({ group: groupValue });
1552
+ }
1553
+ }}
1554
+ onInputKeyDown={(e) => { if (e.key === 'Enter') e.stopPropagation(); }}
1555
+ >
1556
+ {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1557
+ <Option key={group} value={group}>{group}</Option>
1558
+ ))}
1559
+ </Select>
1560
+ </Form.Item>
1561
+ </div>
1562
+ </div>
1563
+ </Collapse.Panel>
1564
+
1565
+ {/* 第二步:URL配置(复用 Modal 的内容) */}
1566
+ <Collapse.Panel
1567
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="2" style={{ backgroundColor: '#1677ff' }} /><span>URL配置</span></div>}
1568
+ key="2"
1569
+ >
1570
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1571
+ <div style={{ flex: 1 }}>
1572
+ <Form.Item name="pattern" label="URL匹配规则" rules={[{ required: true, message: '请输入URL匹配规则' }, ({ getFieldValue }) => ({ validator(_, value) { const proxyType = getFieldValue('proxyType'); if (!value) return Promise.resolve(); if (proxyType === 'url_redirect') { try { new URL(value); return Promise.resolve(); } catch (e) { return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL')); } } if (proxyType === 'redirect') { if (value.startsWith('http://') || value.startsWith('https://')) return Promise.resolve(); return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头')); } return Promise.resolve(); } })]}>
1573
+ <Input placeholder="根据选择的处理方式输入相应格式的URL" />
1574
+ </Form.Item>
1575
+ </div>
1576
+ <div style={{ flex: 1 }}>
1577
+ <Form.Item name="proxyType" label="处理方式" rules={[{ required: true, message: '请选择处理方式' }]}>
1578
+ <Select onChange={() => form.setFieldsValue({ pattern: '' })}>
1579
+ {proxyTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}
1580
+ </Select>
1581
+ </Form.Item>
1582
+ </div>
1583
+ </div>
1584
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1585
+ <div style={{ flex: 1 }}>
1586
+ <Form.Item name="httpMethod" label="请求方法" rules={[{ required: true, message: '请选择请求方法' }]}>
1587
+ <Select>{httpMethods.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1588
+ </Form.Item>
1589
+ </div>
1590
+ <div style={{ flex: 1 }} />
1591
+ </div>
1592
+ </Collapse.Panel>
1593
+
1594
+ {/* 第三步:高级配置 */}
1595
+ <Collapse.Panel
1596
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="3" style={{ backgroundColor: '#1677ff' }} /><span>高级配置</span></div>}
1597
+ key="3"
1598
+ >
1599
+ <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
1600
+ {({ getFieldValue, setFieldsValue }) => {
1601
+ const proxyType = getFieldValue('proxyType');
1602
+ if (proxyType === 'response') {
1603
+ return (
1604
+ <>
1605
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1606
+ <div style={{ flex: 1 }}>
1607
+ <Form.Item name="statusCode" label="状态码" rules={[{ required: true, message: '请选择状态码' }]}>
1608
+ <Select>{statusCodes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1609
+ </Form.Item>
1610
+ </div>
1611
+ <div style={{ flex: 1 }}>
1612
+ <Form.Item name="contentType" label="内容类型" rules={[{ required: true, message: '请选择内容类型' }]}>
1613
+ <Select>{contentTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1614
+ </Form.Item>
1615
+ </div>
1616
+ <div style={{ flex: 1 }}>
1617
+ <Form.Item name="responseDelay" label="延迟响应(毫秒)" rules={[{ pattern: /^\d*$/, message: '请输入有效的数字' }]}>
1618
+ <Input placeholder="例如:500" />
1619
+ </Form.Item>
1620
+ </div>
1621
+ </div>
1622
+ <Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。若需要同一接口内不同入参返回不同响应,请在下方「响应内容编辑」中为每个响应单独配置匹配规则。">
1623
+ <Form.List name="paramMatchers">
1624
+ {(fields, { add, remove }) => (<>
1625
+ {fields.map(({ key, name, ...restField }) => (
1626
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1627
+ <Form.Item {...restField} name={[name, 'paramPath']} rules={[{ required: true, message: '请输入参数路径' }]} style={{ flex: 1, margin: 0 }}>
1628
+ <Input placeholder="参数路径(如:userId 或 data.user.id)" />
1629
+ </Form.Item>
1630
+ <Form.Item {...restField} name={[name, 'paramValue']} rules={[{ required: true, message: '请输入期望值' }]} style={{ flex: 1, margin: 0 }}>
1631
+ <Input placeholder="期望值(如:123 或 admin)" />
1632
+ </Form.Item>
1633
+ <Form.Item {...restField} name={[name, 'matchType']} style={{ width: 100, margin: 0 }} initialValue="exact">
1634
+ <Select><Option value="exact">精确匹配</Option><Option value="contains">包含</Option><Option value="regex">正则</Option></Select>
1635
+ </Form.Item>
1636
+ <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1637
+ </div>
1638
+ ))}
1639
+ <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%' }}>添加参数匹配规则</Button>
1640
+ </>)}
1641
+ </Form.List>
1642
+ </Form.Item>
1643
+ <Form.Item name="responses" initialValue={[]} hidden><Input /></Form.Item>
1644
+ <Form.Item name="activeResponseId" hidden><Input /></Form.Item>
1645
+ <Form.Item noStyle shouldUpdate>
1646
+ {({ getFieldValue }) => {
1647
+ let responses = getFieldValue('responses') || [];
1648
+ const activeResponseId = getFieldValue('activeResponseId');
1649
+ if (!Array.isArray(responses)) { try { responses = JSON.parse(responses); } catch (e) { responses = []; } }
1650
+ return (<ResponseContentEditor form={form} responses={responses} activeResponseId={activeResponseId} onPreview={handlePreview} />);
1651
+ }}
1652
+ </Form.Item>
1653
+ </>
1654
+ );
1655
+ } else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
1656
+ return (
1657
+ <>
1658
+ <Form.Item name="targetUrl" label="重定向目标URL" rules={[{ required: true, message: '请输入重定向目标URL' }, { validator(_, value) { if (!value) return Promise.resolve(); try { new URL(value); return Promise.resolve(); } catch (e) { return Promise.reject(new Error('请输入有效的URL格式')); } } }]} tooltip="重定向的目标URL,必须是完整的URL格式">
1659
+ <Input placeholder="例如:https://api.example.com/users" />
1660
+ </Form.Item>
1661
+ <Form.Item label="自定义请求头">
1662
+ <Form.List name="headerItems">
1663
+ {(fields, { add, remove }) => (<>
1664
+ {fields.map(({ key, name, ...restField }) => (
1665
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1666
+ <Form.Item {...restField} name={[name, 'headerName']} rules={[{ required: true, message: '请输入请求头名称' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头名称(如:Authorization)" /></Form.Item>
1667
+ <Form.Item {...restField} name={[name, 'headerValue']} rules={[{ required: true, message: '请输入请求头值' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头值(如:Bearer token123)" /></Form.Item>
1668
+ <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1669
+ </div>
1670
+ ))}
1671
+ <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%', marginTop: '8px' }}>添加请求头</Button>
1672
+ </>)}
1673
+ </Form.List>
1674
+ </Form.Item>
1675
+ </>
1676
+ );
1677
+ }
1678
+ return null;
1679
+ }}
1680
+ </Form.Item>
1681
+ </Collapse.Panel>
1682
+ </Collapse>
1683
+ </Form>
1684
+ </div>
1685
+ </Drawer>
1686
+ ) : (
1687
+ <Modal
1434
1688
  title={
1435
1689
  <div style={{
1436
1690
  fontSize: '18px',
@@ -1777,17 +2031,109 @@ const InterfaceManagement = () => {
1777
2031
  </div>
1778
2032
  </div>
1779
2033
 
1780
- {/* 入参匹配规则已移至各响应内部配置,此处保留隐藏字段向后兼容旧数据 */}
1781
- <Form.Item name="paramMatchers" hidden initialValue={[]}>
1782
- <Input />
2034
+ {/* 请求入参匹配设置(接口级,用于同URL多接口区分) */}
2035
+ <Form.Item
2036
+ label="接口匹配条件"
2037
+ tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。满足条件时此接口才会被命中。若需要同一接口内不同入参返回不同响应,请在下方「响应内容编辑」中为每个响应单独配置匹配规则。支持嵌套属性路径,如:a.b.c"
2038
+ >
2039
+ <Form.List name="paramMatchers">
2040
+ {(fields, { add, remove }) => (
2041
+ <>
2042
+ {fields.map(({ key, name, ...restField }) => (
2043
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
2044
+ <Form.Item
2045
+ {...restField}
2046
+ name={[name, 'paramPath']}
2047
+ rules={[
2048
+ { required: true, message: '请输入参数路径' },
2049
+ {
2050
+ pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
2051
+ message: '请输入有效的参数路径,如:userId 或 data.user.id'
2052
+ }
2053
+ ]}
2054
+ style={{ flex: 1, margin: 0 }}
2055
+ >
2056
+ <Input placeholder="参数路径(如:userId 或 data.user.id)" />
2057
+ </Form.Item>
2058
+ <Form.Item
2059
+ {...restField}
2060
+ name={[name, 'paramValue']}
2061
+ rules={[{ required: true, message: '请输入期望值' }]}
2062
+ style={{ flex: 1, margin: 0 }}
2063
+ >
2064
+ <Input placeholder="期望值(如:123 或 admin)" />
2065
+ </Form.Item>
2066
+ <Form.Item
2067
+ {...restField}
2068
+ name={[name, 'matchType']}
2069
+ style={{ width: 100, margin: 0 }}
2070
+ initialValue="exact"
2071
+ >
2072
+ <Select placeholder="匹配类型">
2073
+ <Option value="exact">精确匹配</Option>
2074
+ <Option value="contains">包含</Option>
2075
+ <Option value="regex">正则</Option>
2076
+ </Select>
2077
+ </Form.Item>
2078
+ <Button
2079
+ type="text"
2080
+ danger
2081
+ icon={<DeleteOutlined />}
2082
+ onClick={() => remove(name)}
2083
+ />
2084
+ </div>
2085
+ ))}
2086
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
2087
+ <Button
2088
+ type="dashed"
2089
+ onClick={() => add()}
2090
+ icon={<PlusOutlined />}
2091
+ style={{ flex: 1 }}
2092
+ >
2093
+ 添加参数匹配规则
2094
+ </Button>
2095
+ <Popover
2096
+ content={
2097
+ <div style={{ maxWidth: '300px' }}>
2098
+ <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
2099
+ <div style={{ lineHeight: '1.6' }}>
2100
+ <div>• <strong>精确匹配</strong>:参数值完全相等</div>
2101
+ <div>• <strong>包含</strong>:参数值包含指定内容</div>
2102
+ <div>• <strong>正则</strong>:参数值符合正则表达式</div>
2103
+ <div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
2104
+ </div>
2105
+ </div>
2106
+ }
2107
+ title="参数匹配规则"
2108
+ trigger="click"
2109
+ placement="top"
2110
+ >
2111
+ <Button
2112
+ type="text"
2113
+ icon={<QuestionCircleOutlined />}
2114
+ style={{
2115
+ color: '#999',
2116
+ padding: '4px'
2117
+ }}
2118
+ size="small"
2119
+ />
2120
+ </Popover>
2121
+ </div>
2122
+ </>
2123
+ )}
2124
+ </Form.List>
1783
2125
  </Form.Item>
1784
2126
 
1785
- {/* 隐藏的表单字段用于存储响应数据(透传任意值,不渲染UI) */}
1786
- <Form.Item name="responses" initialValue={[]} noStyle>
1787
- <PassthroughField />
2127
+ {/* 隐藏的表单字段用于存储响应数据 */}
2128
+ <Form.Item
2129
+ name="responses"
2130
+ initialValue={[]}
2131
+ hidden
2132
+ >
2133
+ <Input />
1788
2134
  </Form.Item>
1789
- <Form.Item name="activeResponseId" noStyle>
1790
- <PassthroughField />
2135
+ <Form.Item name="activeResponseId" hidden>
2136
+ <Input />
1791
2137
  </Form.Item>
1792
2138
 
1793
2139
  {/* 使用新的合并组件 */}
@@ -1901,6 +2247,7 @@ const InterfaceManagement = () => {
1901
2247
  </Form>
1902
2248
  </div>
1903
2249
  </Modal>
2250
+ )}
1904
2251
 
1905
2252
  {/* 使用拆分后的预览模态框组件 */}
1906
2253
  <PreviewModal
@@ -1909,15 +2256,26 @@ const InterfaceManagement = () => {
1909
2256
  previewContent={previewContent}
1910
2257
  />
1911
2258
 
1912
- {/* 数据管理弹窗 */}
1913
- <DataManagementModal
1914
- visible={dataModalVisible}
1915
- onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
1916
- interfaceItem={dataModalRecord}
1917
- onSaved={() => {
1918
- fetchInterfaces();
1919
- }}
1920
- />
2259
+ {/* 数据管理弹窗/抽屉 */}
2260
+ {displayMode === 'drawer' ? (
2261
+ <DataManagementDrawer
2262
+ visible={dataModalVisible}
2263
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2264
+ interfaceItem={dataModalRecord}
2265
+ onSaved={() => {
2266
+ fetchInterfaces();
2267
+ }}
2268
+ />
2269
+ ) : (
2270
+ <DataManagementModal
2271
+ visible={dataModalVisible}
2272
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2273
+ interfaceItem={dataModalRecord}
2274
+ onSaved={() => {
2275
+ fetchInterfaces();
2276
+ }}
2277
+ />
2278
+ )}
1921
2279
  </div>
1922
2280
  </AppLayout>
1923
2281
  );