whistle.mock-plugins 1.2.2-beta.8 → 1.3.0

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.
Files changed (38) hide show
  1. package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
  2. package/TEST_GUIDE.md +156 -0
  3. package/VERSION.md +57 -3
  4. package/app/cgi-bin/proxy-fetch.js +117 -0
  5. package/app/cgi-bin/settings.js +37 -0
  6. package/app/cgi-bin/stats.js +149 -0
  7. package/app/public/index.html +1 -1
  8. package/app/public/js/index.js +1 -1
  9. package/app/public/js/index.js.LICENSE.txt +25 -2
  10. package/lib/models.js +9 -0
  11. package/lib/routes/interfaces.js +6 -3
  12. package/lib/routes/settings.js +51 -0
  13. package/lib/ruleManager.js +122 -63
  14. package/lib/server.js +4 -0
  15. package/lib/ui-server.js +20 -4
  16. package/package.json +2 -1
  17. package/ui/src/App.css +1 -1
  18. package/ui/src/components/AppLayout.js +317 -80
  19. package/ui/src/components/FeatureCard.js +7 -2
  20. package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
  21. package/ui/src/components/VersionModal.js +4 -41
  22. package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
  23. package/ui/src/components/interface-management/PreviewModal.js +4 -6
  24. package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
  25. package/ui/src/components/interface-management/ResponseContentEditor.js +124 -165
  26. package/ui/src/components/interface-management/index.js +1 -0
  27. package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
  28. package/ui/src/index.html +5 -2
  29. package/ui/src/index.js +12 -2
  30. package/ui/src/pages/InterfaceManagementV2.js +565 -141
  31. package/ui/src/pages/MockDataV2.js +21 -13
  32. package/ui/src/pages/Settings.js +275 -8
  33. package/ui/src/pages/Sniffer.js +521 -0
  34. package/ui/src/pages/Stats.js +288 -0
  35. package/ui/src/styles/cyber-theme.css +1234 -0
  36. package/ui/src/styles/earth-theme.css +618 -0
  37. package/ui/src/styles/interface-management.css +37 -1
  38. package/ui/src/styles/settings.css +45 -1
@@ -2,7 +2,7 @@
2
2
  * InterfaceManagementV2.js - 接口管理页面
3
3
  */
4
4
 
5
- import React, { useState, useEffect } from 'react';
5
+ import React, { useState, useEffect, useRef } from 'react';
6
6
  import { useParams, useHistory } from 'react-router-dom';
7
7
  import AppLayout from '../components/AppLayout';
8
8
  import {
@@ -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,13 +43,13 @@ 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
  ];
@@ -86,10 +87,23 @@ const InterfaceManagement = () => {
86
87
  const [currentResponseId, setCurrentResponseId] = useState(null);
87
88
 
88
89
  // 搜索状态
90
+ const searchRef = useRef(null);
89
91
  const [searchValue, setSearchValue] = useState('');
90
92
  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
91
93
  const [drawerVisible, setDrawerVisible] = useState(false);
92
-
94
+
95
+ // 数据管理显示模式(modal / drawer)
96
+ const [displayMode, setDisplayMode] = useState('modal');
97
+
98
+ // 接口表单显示模式(modal / drawer)
99
+ const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
100
+
101
+ // 接口入参匹配显示开关
102
+ const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
103
+
104
+ // 响应入参匹配显示开关
105
+ const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
106
+
93
107
  // 表单实例
94
108
  const [form] = Form.useForm();
95
109
 
@@ -123,6 +137,82 @@ const InterfaceManagement = () => {
123
137
  }
124
138
  }, [selectedFeatureId]);
125
139
 
140
+ // 键盘快捷键监听
141
+ useEffect(() => {
142
+ const handleSave = () => {
143
+ if (modalVisible) {
144
+ handleSubmit();
145
+ }
146
+ };
147
+ const handleNew = () => {
148
+ handleAddInterface();
149
+ };
150
+ const handleFocusSearch = () => {
151
+ searchRef.current?.focus();
152
+ };
153
+ window.addEventListener('shortcut-save', handleSave);
154
+ window.addEventListener('shortcut-new-interface', handleNew);
155
+ window.addEventListener('shortcut-focus-search', handleFocusSearch);
156
+ return () => {
157
+ window.removeEventListener('shortcut-save', handleSave);
158
+ window.removeEventListener('shortcut-new-interface', handleNew);
159
+ window.removeEventListener('shortcut-focus-search', handleFocusSearch);
160
+ };
161
+ }, [modalVisible]);
162
+
163
+ // 监听数据管理显示模式变化
164
+ useEffect(() => {
165
+ const savedMode = localStorage.getItem('dataManagementDisplayMode');
166
+ if (savedMode) {
167
+ setDisplayMode(savedMode);
168
+ }
169
+
170
+ const handleStorageChange = () => {
171
+ const newMode = localStorage.getItem('dataManagementDisplayMode');
172
+ if (newMode) {
173
+ setDisplayMode(newMode);
174
+ }
175
+ };
176
+
177
+ window.addEventListener('storage', handleStorageChange);
178
+ return () => {
179
+ window.removeEventListener('storage', handleStorageChange);
180
+ };
181
+ }, []);
182
+
183
+ // 监听接口表单显示模式变化
184
+ useEffect(() => {
185
+ const savedMode = localStorage.getItem('interfaceFormDisplayMode');
186
+ if (savedMode) {
187
+ setInterfaceFormDisplayMode(savedMode);
188
+ }
189
+
190
+ const handleStorageChange = () => {
191
+ const newMode = localStorage.getItem('interfaceFormDisplayMode');
192
+ if (newMode) {
193
+ setInterfaceFormDisplayMode(newMode);
194
+ }
195
+ };
196
+
197
+ window.addEventListener('storage', handleStorageChange);
198
+ return () => {
199
+ window.removeEventListener('storage', handleStorageChange);
200
+ };
201
+ }, []);
202
+
203
+ // 监听接口入参匹配 / 响应入参匹配开关变化
204
+ useEffect(() => {
205
+ const readSwitches = () => {
206
+ const v1 = localStorage.getItem('interfaceParamMatcherEnabled');
207
+ setInterfaceParamMatcherEnabled(v1 === null ? true : v1 !== 'false');
208
+ const v2 = localStorage.getItem('responseParamMatcherEnabled');
209
+ setResponseParamMatcherEnabled(v2 === null ? true : v2 !== 'false');
210
+ };
211
+ readSwitches();
212
+ window.addEventListener('storage', readSwitches);
213
+ return () => window.removeEventListener('storage', readSwitches);
214
+ }, []);
215
+
126
216
  // 获取当前选中的功能模块
127
217
  const selectedFeature = features.find(f => f.id === selectedFeatureId);
128
218
 
@@ -131,9 +221,12 @@ const InterfaceManagement = () => {
131
221
  setFeaturesLoading(true);
132
222
  const response = await axios.get('/cgi-bin/features');
133
223
  if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
134
- setFeatures(response.data.data);
135
- // 如果URL中有featureId参数,使用它
136
- const initialFeatureId = featureId || (response.data.data[0]?.id);
224
+ const featureList = response.data.data;
225
+ setFeatures(featureList);
226
+ // 优先级:URL 参数 > localStorage 上次选择 > 第一个功能模块
227
+ const lastSelected = localStorage.getItem('selectedFeatureId');
228
+ const validLastSelected = lastSelected && featureList.find(f => f.id === lastSelected) ? lastSelected : null;
229
+ const initialFeatureId = featureId || validLastSelected || featureList[0]?.id;
137
230
  if (initialFeatureId) {
138
231
  setSelectedFeatureId(initialFeatureId);
139
232
  }
@@ -179,7 +272,8 @@ const InterfaceManagement = () => {
179
272
  id: resp.id || `resp-${index}-${Date.now()}`,
180
273
  name: resp.name || `响应 ${index + 1}`,
181
274
  description: resp.description || '',
182
- content: resp.content || '{}'
275
+ content: resp.content || '{}',
276
+ paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
183
277
  }));
184
278
 
185
279
  return item;
@@ -385,7 +479,8 @@ const InterfaceManagement = () => {
385
479
  id: newId,
386
480
  name: resp.name || '未命名响应',
387
481
  description: resp.description || '',
388
- content: resp.content || '{}'
482
+ content: resp.content || '{}',
483
+ paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers.map(m => ({ ...m })) : []
389
484
  };
390
485
  });
391
486
  const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
@@ -501,6 +596,14 @@ const InterfaceManagement = () => {
501
596
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
502
597
  }
503
598
 
599
+ // 处理 responses,清理每个响应中的空 paramMatchers 规则
600
+ const cleanedResponses = (values.responses || (editingInterface ? editingInterface.responses : [])).map(resp => ({
601
+ ...resp,
602
+ paramMatchers: Array.isArray(resp.paramMatchers)
603
+ ? resp.paramMatchers.filter(m => m && m.paramPath && m.paramValue)
604
+ : []
605
+ }));
606
+
504
607
  // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
505
608
  const interfaceData = {
506
609
  name: values.name || (editingInterface ? editingInterface.name : ''),
@@ -508,7 +611,7 @@ const InterfaceManagement = () => {
508
611
  urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
509
612
  proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
510
613
  featureId: selectedFeatureId,
511
- responses: values.responses || (editingInterface ? editingInterface.responses : []),
614
+ responses: cleanedResponses,
512
615
  activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
513
616
  httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
514
617
  contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
@@ -575,6 +678,7 @@ const InterfaceManagement = () => {
575
678
 
576
679
  const handleSelectFeature = (featureId) => {
577
680
  setSelectedFeatureId(featureId);
681
+ localStorage.setItem('selectedFeatureId', featureId);
578
682
  // 切换功能模块时重置分页到第一页,但保留其他配置
579
683
  saveTableConfig({ current: 1 });
580
684
  };
@@ -800,74 +904,31 @@ const InterfaceManagement = () => {
800
904
  },
801
905
  },
802
906
  {
803
- title: '响应配置',
907
+ title: '当前响应',
804
908
  dataIndex: 'activeResponseId',
805
909
  key: 'currentResponse',
806
- width: 160,
910
+ width: 200,
807
911
  render: (activeResponseId, record) => {
808
- if (record.proxyType !== 'response') {
809
- return <span style={{ color: '#ccc' }}>-</span>;
810
- }
912
+ // 如果没有响应数据,返回空
811
913
  if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
812
- return <span style={{ color: '#ff4d4f', fontSize: '12px' }}>无响应数据</span>;
914
+ return <span className="no-response">无响应数据</span>;
813
915
  }
814
916
 
815
- const total = record.responses.length;
816
- const hasSmartMatch = record.responses.some(
817
- r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0
818
- );
819
- const fallbackResp = record.responses.find(r => r.id === activeResponseId) || record.responses[0];
820
-
821
- const tooltipContent = (
822
- <div style={{ maxWidth: 260 }}>
823
- {record.responses.map((resp) => {
824
- const matchers = resp.paramMatchers || [];
917
+ return (
918
+ <Select
919
+ value={activeResponseId || record.responses[0]?.id}
920
+ style={{ width: '100%' }}
921
+ onChange={(value) => handleResponseSwitch(record.id, value)}
922
+ disabled={record.proxyType !== 'response'}
923
+ >
924
+ {record.responses.map(resp => {
825
925
  return (
826
- <div key={resp.id} style={{ marginBottom: 6 }}>
827
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
828
- {matchers.length > 0 ? (
829
- <Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>{matchers.length}条规则</Tag>
830
- ) : (
831
- <Tag color="default" style={{ fontSize: '11px', margin: 0 }}>兜底</Tag>
832
- )}
833
- <span style={{ fontWeight: resp.id === activeResponseId ? 'bold' : 'normal' }}>
834
- {resp.name}
835
- </span>
836
- </div>
837
- {matchers.length > 0 && (
838
- <div style={{ paddingLeft: 8, marginTop: 2 }}>
839
- {matchers.map((m, i) => (
840
- <div key={i} style={{ fontSize: '11px', color: '#666' }}>
841
- {m.paramPath} = {m.paramValue}
842
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
843
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
844
- </span>
845
- </div>
846
- ))}
847
- </div>
848
- )}
849
- </div>
926
+ <Option key={resp.id} value={resp.id}>
927
+ {resp.name}
928
+ </Option>
850
929
  );
851
930
  })}
852
- </div>
853
- );
854
-
855
- return (
856
- <Tooltip title={tooltipContent} placement="left">
857
- {hasSmartMatch ? (
858
- <Tag color="blue" style={{ cursor: 'default' }}>
859
- 智能匹配 · {total}个响应
860
- </Tag>
861
- ) : total > 1 ? (
862
- <Tag color="orange" style={{ cursor: 'default' }}>
863
- 手动切换 · {total}个响应
864
- </Tag>
865
- ) : (
866
- <span style={{ color: '#999', fontSize: '12px' }}>
867
- {fallbackResp?.name || '1个响应'}
868
- </span>
869
- )}
870
- </Tooltip>
931
+ </Select>
871
932
  );
872
933
  },
873
934
  },
@@ -975,7 +1036,7 @@ const InterfaceManagement = () => {
975
1036
  ))}
976
1037
  </div>
977
1038
  }>
978
- <span style={{ color: '#1890ff' }}>
1039
+ <span style={{ color: '#1677ff' }}>
979
1040
  {count}个
980
1041
  {hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
981
1042
  </span>
@@ -984,75 +1045,81 @@ const InterfaceManagement = () => {
984
1045
  }
985
1046
  },
986
1047
  {
987
- title: '匹配规则',
988
- dataIndex: 'responses',
1048
+ title: '参数匹配',
1049
+ dataIndex: 'paramMatchers',
989
1050
  key: 'paramMatchers',
990
- width: 100,
991
- render: (responses, record) => {
1051
+ width: 120,
1052
+ render: (_, record) => {
992
1053
  if (record.proxyType !== 'response') {
993
- return <span style={{ color: '#ccc' }}>-</span>;
1054
+ return '-';
994
1055
  }
995
1056
 
996
- // 汇总所有响应的 paramMatchers(响应级),兼容旧数据接口级 paramMatchers
997
- const responsesWithMatchers = Array.isArray(responses)
998
- ? responses.filter(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0)
999
- : [];
1000
- const totalRules = responsesWithMatchers.reduce((sum, r) => sum + r.paramMatchers.length, 0);
1057
+ const showInterface = interfaceParamMatcherEnabled;
1058
+ const showResponse = responseParamMatcherEnabled;
1001
1059
 
1002
- // 兼容旧数据:接口级 paramMatchers
1003
- const legacyMatchers = record.paramMatchers || [];
1060
+ const interfaceMatchers = record.paramMatchers || [];
1061
+ const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
1004
1062
 
1005
- if (totalRules === 0 && legacyMatchers.length === 0) {
1006
- return <span style={{ color: '#ccc' }}>-</span>;
1007
- }
1063
+ // 统计所有响应级规则
1064
+ const responsesWithMatchers = showResponse
1065
+ ? (record.responses || []).filter(resp =>
1066
+ Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
1067
+ )
1068
+ : [];
1069
+ const responseMatcherTotal = responsesWithMatchers.reduce(
1070
+ (sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
1071
+ );
1008
1072
 
1009
- // 新数据:按响应分组展示
1010
- if (totalRules > 0) {
1011
- const tooltipContent = (
1012
- <div style={{ maxWidth: 280 }}>
1013
- {responsesWithMatchers.map(resp => (
1014
- <div key={resp.id} style={{ marginBottom: 6 }}>
1015
- <div style={{ fontWeight: 'bold', marginBottom: 2 }}>{resp.name}:</div>
1016
- {resp.paramMatchers.map((m, i) => (
1017
- <div key={i} style={{ paddingLeft: 8, fontSize: '12px' }}>
1018
- {m.paramPath} = {m.paramValue}
1019
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1020
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1021
- </span>
1022
- </div>
1023
- ))}
1024
- </div>
1025
- ))}
1026
- {Array.isArray(responses) && responses.some(r => !r.paramMatchers || r.paramMatchers.length === 0) && (
1027
- <div style={{ color: '#999', fontSize: '12px', marginTop: 4 }}>
1028
- + 兜底响应(无规则)
1029
- </div>
1030
- )}
1031
- </div>
1032
- );
1033
- return (
1034
- <Tooltip title={tooltipContent} placement="left">
1035
- <span style={{ color: '#1890ff', cursor: 'default' }}>{totalRules}条规则</span>
1036
- </Tooltip>
1037
- );
1073
+ if (interfaceCount === 0 && responseMatcherTotal === 0) {
1074
+ return '-';
1038
1075
  }
1039
1076
 
1040
- // 旧数据兼容展示
1041
1077
  const tooltipContent = (
1042
- <div>
1043
- {legacyMatchers.map((matcher, index) => (
1044
- <div key={index}>
1045
- {matcher.paramPath}: {matcher.paramValue}
1046
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1047
- ({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
1048
- </span>
1078
+ <div style={{ maxWidth: 280 }}>
1079
+ {interfaceCount > 0 && (
1080
+ <div style={{ marginBottom: responsesWithMatchers.length > 0 ? 8 : 0 }}>
1081
+ <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#faad14' }}>接口级条件({interfaceCount}条)</div>
1082
+ {interfaceMatchers.filter(m => m.paramPath).map((m, i) => (
1083
+ <div key={i} style={{ fontSize: '12px' }}>
1084
+ {m.paramPath}: {m.paramValue}
1085
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1086
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1087
+ </span>
1088
+ </div>
1089
+ ))}
1049
1090
  </div>
1050
- ))}
1091
+ )}
1092
+ {responsesWithMatchers.length > 0 && (
1093
+ <div>
1094
+ <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1677ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
1095
+ {responsesWithMatchers.map((resp, ri) => (
1096
+ <div key={ri} style={{ marginBottom: 4 }}>
1097
+ <div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
1098
+ {resp.paramMatchers.filter(m => m.paramPath).map((m, mi) => (
1099
+ <div key={mi} style={{ fontSize: '12px', paddingLeft: 8 }}>
1100
+ {m.paramPath}: {m.paramValue}
1101
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1102
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1103
+ </span>
1104
+ </div>
1105
+ ))}
1106
+ </div>
1107
+ ))}
1108
+ </div>
1109
+ )}
1051
1110
  </div>
1052
1111
  );
1112
+
1053
1113
  return (
1054
- <Tooltip title={tooltipContent} placement="left">
1055
- <span style={{ color: '#fa8c16', cursor: 'default' }}>{legacyMatchers.length}条(旧)</span>
1114
+ <Tooltip title={tooltipContent}>
1115
+ <Space size={4} direction="vertical" style={{ lineHeight: 1.4 }}>
1116
+ {interfaceCount > 0 && (
1117
+ <Tag color="orange" style={{ fontSize: '11px', padding: '0 4px' }}>接口:{interfaceCount}条</Tag>
1118
+ )}
1119
+ {responseMatcherTotal > 0 && (
1120
+ <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>响应:{responseMatcherTotal}条</Tag>
1121
+ )}
1122
+ </Space>
1056
1123
  </Tooltip>
1057
1124
  );
1058
1125
  }
@@ -1188,6 +1255,7 @@ const InterfaceManagement = () => {
1188
1255
  </div>
1189
1256
 
1190
1257
  <Search
1258
+ ref={searchRef}
1191
1259
  placeholder="搜索接口名称或URL"
1192
1260
  onSearch={handleSearch}
1193
1261
  value={searchValue}
@@ -1426,8 +1494,249 @@ const InterfaceManagement = () => {
1426
1494
  </div>
1427
1495
  </Card>
1428
1496
 
1429
- {/* 接口编辑/创建模态框 */}
1430
- <Modal
1497
+ {/* 接口编辑/创建模态框/抽屉 */}
1498
+ {interfaceFormDisplayMode === 'drawer' ? (
1499
+ <Drawer
1500
+ title={
1501
+ <div style={{
1502
+ fontSize: '18px',
1503
+ fontWeight: 600,
1504
+ color: '#262626',
1505
+ display: 'flex',
1506
+ alignItems: 'center',
1507
+ gap: '8px'
1508
+ }}>
1509
+ <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1510
+ <Badge
1511
+ count="配置向导"
1512
+ style={{
1513
+ backgroundColor: '#f0f9ff',
1514
+ color: '#1677ff',
1515
+ fontSize: '11px',
1516
+ fontWeight: 'normal'
1517
+ }}
1518
+ />
1519
+ </div>
1520
+ }
1521
+ placement="right"
1522
+ width={1040}
1523
+ open={modalVisible}
1524
+ onClose={handleCancel}
1525
+ destroyOnClose
1526
+ bodyStyle={{
1527
+ padding: '0',
1528
+ overflowY: 'auto'
1529
+ }}
1530
+ footer={
1531
+ <div style={{ textAlign: 'right' }}>
1532
+ <Space>
1533
+ <Button onClick={handleCancel}>取消</Button>
1534
+ <Button type="primary" onClick={handleSubmit}>
1535
+ {editingInterface ? '保存' : '创建'}
1536
+ </Button>
1537
+ </Space>
1538
+ </div>
1539
+ }
1540
+ className="interface-drawer"
1541
+ >
1542
+ <div style={{ padding: '24px' }}>
1543
+ <Form
1544
+ form={form}
1545
+ layout="vertical"
1546
+ initialValues={{
1547
+ name: '',
1548
+ group: '',
1549
+ pattern: '',
1550
+ proxyType: 'response',
1551
+ statusCode: '200',
1552
+ contentType: 'application/json; charset=utf-8',
1553
+ responses: [],
1554
+ httpMethod: 'ALL',
1555
+ targetUrl: '',
1556
+ headerItems: [],
1557
+ paramMatchers: [],
1558
+ responseDelay: '0'
1559
+ }}
1560
+ >
1561
+ <Collapse
1562
+ defaultActiveKey={
1563
+ editingInterface && editingInterface.proxyType === 'response'
1564
+ ? ['3']
1565
+ : ['1', '2', '3']
1566
+ }
1567
+ style={{ marginBottom: '16px' }}
1568
+ ghost
1569
+ >
1570
+ {/* 第一步:基础信息 */}
1571
+ <Collapse.Panel
1572
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="1" style={{ backgroundColor: '#1677ff' }} /><span>基础信息</span></div>}
1573
+ key="1"
1574
+ >
1575
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1576
+ <div style={{ flex: 1 }}>
1577
+ <Form.Item name="name" label="接口名称" rules={[{ required: true, message: '请输入接口名称' }]}>
1578
+ <Input placeholder="请输入接口名称" />
1579
+ </Form.Item>
1580
+ </div>
1581
+ <div style={{ flex: 1 }}>
1582
+ <Form.Item name="group" label="分组" tooltip="为接口设置分组,便于管理和批量操作">
1583
+ <Select placeholder="选择或创建分组" showSearch allowClear style={{ width: '100%' }} mode="tags" maxTagCount="responsive" maxTagTextLength={10}
1584
+ onChange={(value) => {
1585
+ if (Array.isArray(value) && value.length > 0) {
1586
+ const groupValue = value[0] === '' ? undefined : value[0];
1587
+ form.setFieldsValue({ group: groupValue });
1588
+ }
1589
+ }}
1590
+ onInputKeyDown={(e) => { if (e.key === 'Enter') e.stopPropagation(); }}
1591
+ >
1592
+ {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1593
+ <Option key={group} value={group}>{group}</Option>
1594
+ ))}
1595
+ </Select>
1596
+ </Form.Item>
1597
+ </div>
1598
+ </div>
1599
+ </Collapse.Panel>
1600
+
1601
+ {/* 第二步:URL配置(复用 Modal 的内容) */}
1602
+ <Collapse.Panel
1603
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="2" style={{ backgroundColor: '#1677ff' }} /><span>URL配置</span></div>}
1604
+ key="2"
1605
+ >
1606
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1607
+ <div style={{ flex: 1 }}>
1608
+ <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(); } })]}>
1609
+ <Input placeholder="根据选择的处理方式输入相应格式的URL" />
1610
+ </Form.Item>
1611
+ </div>
1612
+ <div style={{ flex: 1 }}>
1613
+ <Form.Item name="proxyType" label="处理方式" rules={[{ required: true, message: '请选择处理方式' }]}>
1614
+ <Select onChange={() => form.setFieldsValue({ pattern: '' })}>
1615
+ {proxyTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}
1616
+ </Select>
1617
+ </Form.Item>
1618
+ </div>
1619
+ </div>
1620
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1621
+ <div style={{ flex: 1 }}>
1622
+ <Form.Item name="httpMethod" label="请求方法" rules={[{ required: true, message: '请选择请求方法' }]}>
1623
+ <Select>{httpMethods.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1624
+ </Form.Item>
1625
+ </div>
1626
+ <div style={{ flex: 1 }} />
1627
+ </div>
1628
+ </Collapse.Panel>
1629
+
1630
+ {/* 第三步:高级配置 */}
1631
+ <Collapse.Panel
1632
+ header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="3" style={{ backgroundColor: '#1677ff' }} /><span>高级配置</span></div>}
1633
+ key="3"
1634
+ >
1635
+ <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
1636
+ {({ getFieldValue, setFieldsValue }) => {
1637
+ const proxyType = getFieldValue('proxyType');
1638
+ if (proxyType === 'response') {
1639
+ return (
1640
+ <>
1641
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1642
+ <div style={{ flex: 1 }}>
1643
+ <Form.Item name="statusCode" label="状态码" rules={[{ required: true, message: '请选择状态码' }]}>
1644
+ <Select>{statusCodes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1645
+ </Form.Item>
1646
+ </div>
1647
+ <div style={{ flex: 1 }}>
1648
+ <Form.Item name="contentType" label="内容类型" rules={[{ required: true, message: '请选择内容类型' }]}>
1649
+ <Select>{contentTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1650
+ </Form.Item>
1651
+ </div>
1652
+ <div style={{ flex: 1 }}>
1653
+ <Form.Item name="responseDelay" label="延迟响应(毫秒)" rules={[{ pattern: /^\d*$/, message: '请输入有效的数字' }]}>
1654
+ <Input placeholder="例如:500" />
1655
+ </Form.Item>
1656
+ </div>
1657
+ </div>
1658
+ {interfaceParamMatcherEnabled && (
1659
+ <Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。">
1660
+ <Form.List name="paramMatchers">
1661
+ {(fields, { add, remove }) => (<>
1662
+ {fields.map(({ key, name, ...restField }) => (
1663
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1664
+ <Form.Item {...restField} name={[name, 'paramPath']} rules={[{ required: true, message: '请输入参数路径' }]} style={{ flex: 1, margin: 0 }}>
1665
+ <Input placeholder="参数路径(如:userId 或 data.user.id)" />
1666
+ </Form.Item>
1667
+ <Form.Item {...restField} name={[name, 'paramValue']} rules={[{ required: true, message: '请输入期望值' }]} style={{ flex: 1, margin: 0 }}>
1668
+ <Input placeholder="期望值(如:123 或 admin)" />
1669
+ </Form.Item>
1670
+ <Form.Item {...restField} name={[name, 'matchType']} style={{ width: 90, margin: 0 }} initialValue="exact">
1671
+ <Select><Option value="exact">精确匹配</Option><Option value="contains">包含</Option><Option value="regex">正则</Option></Select>
1672
+ </Form.Item>
1673
+ <Form.Item noStyle shouldUpdate>
1674
+ {({ getFieldValue }) => {
1675
+ const responses = getFieldValue('responses') || [];
1676
+ return (
1677
+ <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 130, margin: 0 }}>
1678
+ <Select allowClear placeholder="指定响应(可选)">
1679
+ {responses.filter(r => r && r.id).map(r => (
1680
+ <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
1681
+ ))}
1682
+ </Select>
1683
+ </Form.Item>
1684
+ );
1685
+ }}
1686
+ </Form.Item>
1687
+ <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1688
+ </div>
1689
+ ))}
1690
+ <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%' }}>添加参数匹配规则</Button>
1691
+ </>)}
1692
+ </Form.List>
1693
+ </Form.Item>
1694
+ )}
1695
+ <Form.Item name="responses" initialValue={[]} hidden><Input /></Form.Item>
1696
+ <Form.Item name="activeResponseId" hidden><Input /></Form.Item>
1697
+ <Form.Item noStyle shouldUpdate>
1698
+ {({ getFieldValue }) => {
1699
+ let responses = getFieldValue('responses') || [];
1700
+ const activeResponseId = getFieldValue('activeResponseId');
1701
+ if (!Array.isArray(responses)) { try { responses = JSON.parse(responses); } catch (e) { responses = []; } }
1702
+ return (<ResponseContentEditor form={form} responses={responses} activeResponseId={activeResponseId} onPreview={handlePreview} />);
1703
+ }}
1704
+ </Form.Item>
1705
+ </>
1706
+ );
1707
+ } else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
1708
+ return (
1709
+ <>
1710
+ <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格式">
1711
+ <Input placeholder="例如:https://api.example.com/users" />
1712
+ </Form.Item>
1713
+ <Form.Item label="自定义请求头">
1714
+ <Form.List name="headerItems">
1715
+ {(fields, { add, remove }) => (<>
1716
+ {fields.map(({ key, name, ...restField }) => (
1717
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1718
+ <Form.Item {...restField} name={[name, 'headerName']} rules={[{ required: true, message: '请输入请求头名称' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头名称(如:Authorization)" /></Form.Item>
1719
+ <Form.Item {...restField} name={[name, 'headerValue']} rules={[{ required: true, message: '请输入请求头值' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头值(如:Bearer token123)" /></Form.Item>
1720
+ <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1721
+ </div>
1722
+ ))}
1723
+ <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%', marginTop: '8px' }}>添加请求头</Button>
1724
+ </>)}
1725
+ </Form.List>
1726
+ </Form.Item>
1727
+ </>
1728
+ );
1729
+ }
1730
+ return null;
1731
+ }}
1732
+ </Form.Item>
1733
+ </Collapse.Panel>
1734
+ </Collapse>
1735
+ </Form>
1736
+ </div>
1737
+ </Drawer>
1738
+ ) : (
1739
+ <Modal
1431
1740
  title={
1432
1741
  <div style={{
1433
1742
  fontSize: '18px',
@@ -1459,8 +1768,7 @@ const InterfaceManagement = () => {
1459
1768
  bodyStyle={{
1460
1769
  maxHeight: '75vh',
1461
1770
  overflow: 'auto',
1462
- padding: '0',
1463
- background: '#fafafa'
1771
+ padding: '0'
1464
1772
  }}
1465
1773
  className="interface-modal"
1466
1774
  >
@@ -1774,10 +2082,114 @@ const InterfaceManagement = () => {
1774
2082
  </div>
1775
2083
  </div>
1776
2084
 
1777
- {/* 入参匹配规则已移至各响应内部配置,此处保留隐藏字段向后兼容旧数据 */}
1778
- <Form.Item name="paramMatchers" hidden initialValue={[]}>
1779
- <Input />
2085
+ {/* 请求入参匹配设置(接口级,用于同URL多接口区分) */}
2086
+ {interfaceParamMatcherEnabled && (
2087
+ <Form.Item
2088
+ label="接口匹配条件"
2089
+ tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。支持嵌套属性路径,如:a.b.c"
2090
+ >
2091
+ <Form.List name="paramMatchers">
2092
+ {(fields, { add, remove }) => (
2093
+ <>
2094
+ {fields.map(({ key, name, ...restField }) => (
2095
+ <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
2096
+ <Form.Item
2097
+ {...restField}
2098
+ name={[name, 'paramPath']}
2099
+ rules={[
2100
+ { required: true, message: '请输入参数路径' },
2101
+ {
2102
+ pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
2103
+ message: '请输入有效的参数路径,如:userId 或 data.user.id'
2104
+ }
2105
+ ]}
2106
+ style={{ flex: 1, margin: 0 }}
2107
+ >
2108
+ <Input placeholder="参数路径(如:userId 或 data.user.id)" />
2109
+ </Form.Item>
2110
+ <Form.Item
2111
+ {...restField}
2112
+ name={[name, 'paramValue']}
2113
+ rules={[{ required: true, message: '请输入期望值' }]}
2114
+ style={{ flex: 1, margin: 0 }}
2115
+ >
2116
+ <Input placeholder="期望值(如:123 或 admin)" />
2117
+ </Form.Item>
2118
+ <Form.Item
2119
+ {...restField}
2120
+ name={[name, 'matchType']}
2121
+ style={{ width: 100, margin: 0 }}
2122
+ initialValue="exact"
2123
+ >
2124
+ <Select placeholder="匹配类型">
2125
+ <Option value="exact">精确匹配</Option>
2126
+ <Option value="contains">包含</Option>
2127
+ <Option value="regex">正则</Option>
2128
+ </Select>
2129
+ </Form.Item>
2130
+ <Form.Item noStyle shouldUpdate>
2131
+ {({ getFieldValue }) => {
2132
+ const responses = getFieldValue('responses') || [];
2133
+ return (
2134
+ <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 140, margin: 0 }}>
2135
+ <Select allowClear placeholder="指定响应(可选)">
2136
+ {responses.filter(r => r && r.id).map(r => (
2137
+ <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
2138
+ ))}
2139
+ </Select>
2140
+ </Form.Item>
2141
+ );
2142
+ }}
2143
+ </Form.Item>
2144
+ <Button
2145
+ type="text"
2146
+ danger
2147
+ icon={<DeleteOutlined />}
2148
+ onClick={() => remove(name)}
2149
+ />
2150
+ </div>
2151
+ ))}
2152
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
2153
+ <Button
2154
+ type="dashed"
2155
+ onClick={() => add()}
2156
+ icon={<PlusOutlined />}
2157
+ style={{ flex: 1 }}
2158
+ >
2159
+ 添加参数匹配规则
2160
+ </Button>
2161
+ <Popover
2162
+ content={
2163
+ <div style={{ maxWidth: '300px' }}>
2164
+ <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
2165
+ <div style={{ lineHeight: '1.6' }}>
2166
+ <div>• <strong>精确匹配</strong>:参数值完全相等</div>
2167
+ <div>• <strong>包含</strong>:参数值包含指定内容</div>
2168
+ <div>• <strong>正则</strong>:参数值符合正则表达式</div>
2169
+ <div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
2170
+ </div>
2171
+ </div>
2172
+ }
2173
+ title="参数匹配规则"
2174
+ trigger="click"
2175
+ placement="top"
2176
+ >
2177
+ <Button
2178
+ type="text"
2179
+ icon={<QuestionCircleOutlined />}
2180
+ style={{
2181
+ color: '#999',
2182
+ padding: '4px'
2183
+ }}
2184
+ size="small"
2185
+ />
2186
+ </Popover>
2187
+ </div>
2188
+ </>
2189
+ )}
2190
+ </Form.List>
1780
2191
  </Form.Item>
2192
+ )}
1781
2193
 
1782
2194
  {/* 隐藏的表单字段用于存储响应数据 */}
1783
2195
  <Form.Item
@@ -1902,6 +2314,7 @@ const InterfaceManagement = () => {
1902
2314
  </Form>
1903
2315
  </div>
1904
2316
  </Modal>
2317
+ )}
1905
2318
 
1906
2319
  {/* 使用拆分后的预览模态框组件 */}
1907
2320
  <PreviewModal
@@ -1910,15 +2323,26 @@ const InterfaceManagement = () => {
1910
2323
  previewContent={previewContent}
1911
2324
  />
1912
2325
 
1913
- {/* 数据管理弹窗 */}
1914
- <DataManagementModal
1915
- visible={dataModalVisible}
1916
- onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
1917
- interfaceItem={dataModalRecord}
1918
- onSaved={() => {
1919
- fetchInterfaces();
1920
- }}
1921
- />
2326
+ {/* 数据管理弹窗/抽屉 */}
2327
+ {displayMode === 'drawer' ? (
2328
+ <DataManagementDrawer
2329
+ visible={dataModalVisible}
2330
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2331
+ interfaceItem={dataModalRecord}
2332
+ onSaved={() => {
2333
+ fetchInterfaces();
2334
+ }}
2335
+ />
2336
+ ) : (
2337
+ <DataManagementModal
2338
+ visible={dataModalVisible}
2339
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2340
+ interfaceItem={dataModalRecord}
2341
+ onSaved={() => {
2342
+ fetchInterfaces();
2343
+ }}
2344
+ />
2345
+ )}
1922
2346
  </div>
1923
2347
  </AppLayout>
1924
2348
  );