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