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.
- package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
- package/TEST_GUIDE.md +156 -0
- package/VERSION.md +57 -3
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/settings.js +37 -0
- package/app/cgi-bin/stats.js +149 -0
- package/app/public/index.html +1 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +27 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +122 -63
- package/lib/server.js +4 -0
- package/lib/ui-server.js +20 -4
- package/package.json +2 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +317 -80
- package/ui/src/components/FeatureCard.js +7 -2
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/VersionModal.js +4 -41
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/PreviewModal.js +4 -6
- package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +123 -177
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +12 -2
- package/ui/src/pages/InterfaceManagementV2.js +563 -147
- package/ui/src/pages/MockDataV2.js +255 -83
- package/ui/src/pages/Settings.js +275 -8
- package/ui/src/pages/Sniffer.js +521 -0
- package/ui/src/pages/Stats.js +288 -0
- package/ui/src/styles/cyber-theme.css +1234 -0
- package/ui/src/styles/earth-theme.css +618 -0
- package/ui/src/styles/interface-management.css +37 -1
- 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: '
|
|
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: '
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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:
|
|
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:
|
|
910
|
+
width: 200,
|
|
807
911
|
render: (activeResponseId, record) => {
|
|
808
|
-
|
|
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
|
|
914
|
+
return <span className="no-response">无响应数据</span>;
|
|
813
915
|
}
|
|
814
916
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
</
|
|
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: '#
|
|
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: '
|
|
1048
|
+
title: '参数匹配',
|
|
1049
|
+
dataIndex: 'paramMatchers',
|
|
997
1050
|
key: 'paramMatchers',
|
|
998
|
-
width:
|
|
999
|
-
render: (
|
|
1051
|
+
width: 120,
|
|
1052
|
+
render: (_, record) => {
|
|
1000
1053
|
if (record.proxyType !== 'response') {
|
|
1001
|
-
return
|
|
1054
|
+
return '-';
|
|
1002
1055
|
}
|
|
1003
1056
|
|
|
1004
|
-
|
|
1005
|
-
const
|
|
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
|
-
|
|
1011
|
-
const
|
|
1060
|
+
const interfaceMatchers = record.paramMatchers || [];
|
|
1061
|
+
const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
|
|
1012
1062
|
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
1052
|
-
<div
|
|
1053
|
-
{
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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}
|
|
1063
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
);
|