whistle.mock-plugins 1.2.2-beta.9 → 1.3.1-beta.1

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 +27 -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 +123 -177
  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 +563 -147
  31. package/ui/src/pages/MockDataV2.js +255 -83
  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,82 +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
-
820
- // 场景一:所有响应都没有规则 → 显示 Select 快捷切换
821
- if (!hasSmartMatch) {
822
- return (
823
- <Select
824
- value={activeResponseId || record.responses[0]?.id}
825
- style={{ width: '100%' }}
826
- size="small"
827
- onChange={(value) => handleResponseSwitch(record.id, value)}
828
- >
829
- {record.responses.map(resp => (
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 => {
925
+ return (
830
926
  <Option key={resp.id} value={resp.id}>
831
927
  {resp.name}
832
928
  </Option>
833
- ))}
834
- </Select>
835
- );
836
- }
837
-
838
- // 场景二/三:有响应含规则 → 显示 Tag 摘要
839
- const tooltipContent = (
840
- <div style={{ maxWidth: 260 }}>
841
- {record.responses.map((resp) => {
842
- const matchers = resp.paramMatchers || [];
843
- return (
844
- <div key={resp.id} style={{ marginBottom: 6 }}>
845
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
846
- {matchers.length > 0 ? (
847
- <Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>{matchers.length}条规则</Tag>
848
- ) : (
849
- <Tag color="default" style={{ fontSize: '11px', margin: 0 }}>兜底</Tag>
850
- )}
851
- <span style={{ fontWeight: resp.id === activeResponseId ? 'bold' : 'normal' }}>
852
- {resp.name}
853
- </span>
854
- </div>
855
- {matchers.length > 0 && (
856
- <div style={{ paddingLeft: 8, marginTop: 2 }}>
857
- {matchers.map((m, i) => (
858
- <div key={i} style={{ fontSize: '11px', color: '#666' }}>
859
- {m.paramPath} = {m.paramValue}
860
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
861
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
862
- </span>
863
- </div>
864
- ))}
865
- </div>
866
- )}
867
- </div>
868
929
  );
869
930
  })}
870
- </div>
871
- );
872
-
873
- return (
874
- <Tooltip title={tooltipContent} placement="left">
875
- <Tag color="blue" style={{ cursor: 'default' }}>
876
- 智能匹配 · {total}个响应
877
- </Tag>
878
- </Tooltip>
931
+ </Select>
879
932
  );
880
933
  },
881
934
  },
@@ -983,7 +1036,7 @@ const InterfaceManagement = () => {
983
1036
  ))}
984
1037
  </div>
985
1038
  }>
986
- <span style={{ color: '#1890ff' }}>
1039
+ <span style={{ color: '#1677ff' }}>
987
1040
  {count}个
988
1041
  {hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
989
1042
  </span>
@@ -992,75 +1045,81 @@ const InterfaceManagement = () => {
992
1045
  }
993
1046
  },
994
1047
  {
995
- title: '匹配规则',
996
- dataIndex: 'responses',
1048
+ title: '参数匹配',
1049
+ dataIndex: 'paramMatchers',
997
1050
  key: 'paramMatchers',
998
- width: 100,
999
- render: (responses, record) => {
1051
+ width: 120,
1052
+ render: (_, record) => {
1000
1053
  if (record.proxyType !== 'response') {
1001
- return <span style={{ color: '#ccc' }}>-</span>;
1054
+ return '-';
1002
1055
  }
1003
1056
 
1004
- // 汇总所有响应的 paramMatchers(响应级),兼容旧数据接口级 paramMatchers
1005
- const responsesWithMatchers = Array.isArray(responses)
1006
- ? responses.filter(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0)
1007
- : [];
1008
- const totalRules = responsesWithMatchers.reduce((sum, r) => sum + r.paramMatchers.length, 0);
1057
+ const showInterface = interfaceParamMatcherEnabled;
1058
+ const showResponse = responseParamMatcherEnabled;
1009
1059
 
1010
- // 兼容旧数据:接口级 paramMatchers
1011
- const legacyMatchers = record.paramMatchers || [];
1060
+ const interfaceMatchers = record.paramMatchers || [];
1061
+ const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
1012
1062
 
1013
- if (totalRules === 0 && legacyMatchers.length === 0) {
1014
- return <span style={{ color: '#ccc' }}>-</span>;
1015
- }
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
+ );
1016
1072
 
1017
- // 新数据:按响应分组展示
1018
- if (totalRules > 0) {
1019
- const tooltipContent = (
1020
- <div style={{ maxWidth: 280 }}>
1021
- {responsesWithMatchers.map(resp => (
1022
- <div key={resp.id} style={{ marginBottom: 6 }}>
1023
- <div style={{ fontWeight: 'bold', marginBottom: 2 }}>{resp.name}:</div>
1024
- {resp.paramMatchers.map((m, i) => (
1025
- <div key={i} style={{ paddingLeft: 8, fontSize: '12px' }}>
1026
- {m.paramPath} = {m.paramValue}
1027
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1028
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1029
- </span>
1030
- </div>
1031
- ))}
1032
- </div>
1033
- ))}
1034
- {Array.isArray(responses) && responses.some(r => !r.paramMatchers || r.paramMatchers.length === 0) && (
1035
- <div style={{ color: '#999', fontSize: '12px', marginTop: 4 }}>
1036
- + 兜底响应(无规则)
1037
- </div>
1038
- )}
1039
- </div>
1040
- );
1041
- return (
1042
- <Tooltip title={tooltipContent} placement="left">
1043
- <span style={{ color: '#1890ff', cursor: 'default' }}>{totalRules}条规则</span>
1044
- </Tooltip>
1045
- );
1073
+ if (interfaceCount === 0 && responseMatcherTotal === 0) {
1074
+ return '-';
1046
1075
  }
1047
1076
 
1048
- // 旧数据兼容展示
1049
1077
  const tooltipContent = (
1050
- <div>
1051
- {legacyMatchers.map((matcher, index) => (
1052
- <div key={index}>
1053
- {matcher.paramPath}: {matcher.paramValue}
1054
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1055
- ({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
1056
- </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
+ ))}
1090
+ </div>
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
+ ))}
1057
1108
  </div>
1058
- ))}
1109
+ )}
1059
1110
  </div>
1060
1111
  );
1112
+
1061
1113
  return (
1062
- <Tooltip title={tooltipContent} placement="left">
1063
- <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>
1064
1123
  </Tooltip>
1065
1124
  );
1066
1125
  }
@@ -1196,6 +1255,7 @@ const InterfaceManagement = () => {
1196
1255
  </div>
1197
1256
 
1198
1257
  <Search
1258
+ ref={searchRef}
1199
1259
  placeholder="搜索接口名称或URL"
1200
1260
  onSearch={handleSearch}
1201
1261
  value={searchValue}
@@ -1434,8 +1494,249 @@ const InterfaceManagement = () => {
1434
1494
  </div>
1435
1495
  </Card>
1436
1496
 
1437
- {/* 接口编辑/创建模态框 */}
1438
- <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
1439
1740
  title={
1440
1741
  <div style={{
1441
1742
  fontSize: '18px',
@@ -1467,8 +1768,7 @@ const InterfaceManagement = () => {
1467
1768
  bodyStyle={{
1468
1769
  maxHeight: '75vh',
1469
1770
  overflow: 'auto',
1470
- padding: '0',
1471
- background: '#fafafa'
1771
+ padding: '0'
1472
1772
  }}
1473
1773
  className="interface-modal"
1474
1774
  >
@@ -1782,10 +2082,114 @@ const InterfaceManagement = () => {
1782
2082
  </div>
1783
2083
  </div>
1784
2084
 
1785
- {/* 入参匹配规则已移至各响应内部配置,此处保留隐藏字段向后兼容旧数据 */}
1786
- <Form.Item name="paramMatchers" hidden initialValue={[]}>
1787
- <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>
1788
2191
  </Form.Item>
2192
+ )}
1789
2193
 
1790
2194
  {/* 隐藏的表单字段用于存储响应数据 */}
1791
2195
  <Form.Item
@@ -1910,6 +2314,7 @@ const InterfaceManagement = () => {
1910
2314
  </Form>
1911
2315
  </div>
1912
2316
  </Modal>
2317
+ )}
1913
2318
 
1914
2319
  {/* 使用拆分后的预览模态框组件 */}
1915
2320
  <PreviewModal
@@ -1918,15 +2323,26 @@ const InterfaceManagement = () => {
1918
2323
  previewContent={previewContent}
1919
2324
  />
1920
2325
 
1921
- {/* 数据管理弹窗 */}
1922
- <DataManagementModal
1923
- visible={dataModalVisible}
1924
- onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
1925
- interfaceItem={dataModalRecord}
1926
- onSaved={() => {
1927
- fetchInterfaces();
1928
- }}
1929
- />
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
+ )}
1930
2346
  </div>
1931
2347
  </AppLayout>
1932
2348
  );