whistle.mock-plugins 1.2.2-beta.2 → 1.2.2-beta.22
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/VERSION.md +50 -1
- package/app/cgi-bin/settings.js +37 -0
- package/app/public/js/index.js +1 -1
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +105 -41
- package/lib/server.js +4 -0
- package/package.json +1 -1
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +146 -36
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/pages/InterfaceManagementV2.js +448 -145
- package/ui/src/pages/MockDataV2.js +1 -1
- package/ui/src/pages/Settings.js +143 -6
- package/ui/src/styles/interface-management.css +3 -12
- package/ui/src/styles/settings.css +29 -0
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
FileTextOutlined, PlusCircleOutlined, SettingOutlined,
|
|
16
16
|
SearchOutlined, FilterOutlined, ReloadOutlined, DownOutlined, QuestionCircleOutlined, CopyOutlined
|
|
17
17
|
} from '@ant-design/icons';
|
|
18
|
-
// 移除 react-resizable,改用原生实现
|
|
19
18
|
import '../styles/interface-management.css';
|
|
20
19
|
import axios from 'axios';
|
|
21
20
|
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
generateResponseId
|
|
33
32
|
} from '../components/interface-management';
|
|
34
33
|
import DataManagementModal from '../components/interface-management/DataManagementModal';
|
|
34
|
+
import { DataManagementDrawer } from '../components/interface-management';
|
|
35
35
|
|
|
36
36
|
const { Option } = Select;
|
|
37
37
|
const { Search } = AntInput;
|
|
@@ -90,7 +90,19 @@ const InterfaceManagement = () => {
|
|
|
90
90
|
const [searchValue, setSearchValue] = useState('');
|
|
91
91
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
92
92
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
// 数据管理显示模式(modal / drawer)
|
|
95
|
+
const [displayMode, setDisplayMode] = useState('modal');
|
|
96
|
+
|
|
97
|
+
// 接口表单显示模式(modal / drawer)
|
|
98
|
+
const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
|
|
99
|
+
|
|
100
|
+
// 接口入参匹配显示开关
|
|
101
|
+
const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
|
|
102
|
+
|
|
103
|
+
// 响应入参匹配显示开关
|
|
104
|
+
const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
|
|
105
|
+
|
|
94
106
|
// 表单实例
|
|
95
107
|
const [form] = Form.useForm();
|
|
96
108
|
|
|
@@ -98,27 +110,13 @@ const InterfaceManagement = () => {
|
|
|
98
110
|
const [tableConfig, setTableConfig] = useState(() => {
|
|
99
111
|
const cached = localStorage.getItem('interface-table-config');
|
|
100
112
|
const defaultVisibleColumns = COLUMN_CONFIG.map(col => col.key);
|
|
101
|
-
|
|
113
|
+
return cached ? JSON.parse(cached) : {
|
|
102
114
|
sortOrder: null,
|
|
103
115
|
sortField: null,
|
|
104
116
|
pageSize: 10,
|
|
105
117
|
current: 1,
|
|
106
|
-
visibleColumns: defaultVisibleColumns
|
|
107
|
-
columnWidths: {
|
|
108
|
-
currentResponse: 180
|
|
109
|
-
}
|
|
118
|
+
visibleColumns: defaultVisibleColumns
|
|
110
119
|
};
|
|
111
|
-
|
|
112
|
-
if (cached) {
|
|
113
|
-
const parsedConfig = JSON.parse(cached);
|
|
114
|
-
// 确保 columnWidths 存在
|
|
115
|
-
if (!parsedConfig.columnWidths) {
|
|
116
|
-
parsedConfig.columnWidths = { currentResponse: 180 };
|
|
117
|
-
}
|
|
118
|
-
return parsedConfig;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return defaultConfig;
|
|
122
120
|
});
|
|
123
121
|
|
|
124
122
|
// 保存表格配置到localStorage
|
|
@@ -138,6 +136,59 @@ const InterfaceManagement = () => {
|
|
|
138
136
|
}
|
|
139
137
|
}, [selectedFeatureId]);
|
|
140
138
|
|
|
139
|
+
// 监听数据管理显示模式变化
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const savedMode = localStorage.getItem('dataManagementDisplayMode');
|
|
142
|
+
if (savedMode) {
|
|
143
|
+
setDisplayMode(savedMode);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleStorageChange = () => {
|
|
147
|
+
const newMode = localStorage.getItem('dataManagementDisplayMode');
|
|
148
|
+
if (newMode) {
|
|
149
|
+
setDisplayMode(newMode);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
window.addEventListener('storage', handleStorageChange);
|
|
154
|
+
return () => {
|
|
155
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
156
|
+
};
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
// 监听接口表单显示模式变化
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const savedMode = localStorage.getItem('interfaceFormDisplayMode');
|
|
162
|
+
if (savedMode) {
|
|
163
|
+
setInterfaceFormDisplayMode(savedMode);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const handleStorageChange = () => {
|
|
167
|
+
const newMode = localStorage.getItem('interfaceFormDisplayMode');
|
|
168
|
+
if (newMode) {
|
|
169
|
+
setInterfaceFormDisplayMode(newMode);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
window.addEventListener('storage', handleStorageChange);
|
|
174
|
+
return () => {
|
|
175
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
176
|
+
};
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
// 监听接口入参匹配 / 响应入参匹配开关变化
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const readSwitches = () => {
|
|
182
|
+
const v1 = localStorage.getItem('interfaceParamMatcherEnabled');
|
|
183
|
+
setInterfaceParamMatcherEnabled(v1 === null ? true : v1 !== 'false');
|
|
184
|
+
const v2 = localStorage.getItem('responseParamMatcherEnabled');
|
|
185
|
+
setResponseParamMatcherEnabled(v2 === null ? true : v2 !== 'false');
|
|
186
|
+
};
|
|
187
|
+
readSwitches();
|
|
188
|
+
window.addEventListener('storage', readSwitches);
|
|
189
|
+
return () => window.removeEventListener('storage', readSwitches);
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
141
192
|
// 获取当前选中的功能模块
|
|
142
193
|
const selectedFeature = features.find(f => f.id === selectedFeatureId);
|
|
143
194
|
|
|
@@ -146,9 +197,12 @@ const InterfaceManagement = () => {
|
|
|
146
197
|
setFeaturesLoading(true);
|
|
147
198
|
const response = await axios.get('/cgi-bin/features');
|
|
148
199
|
if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
const featureList = response.data.data;
|
|
201
|
+
setFeatures(featureList);
|
|
202
|
+
// 优先级:URL 参数 > localStorage 上次选择 > 第一个功能模块
|
|
203
|
+
const lastSelected = localStorage.getItem('selectedFeatureId');
|
|
204
|
+
const validLastSelected = lastSelected && featureList.find(f => f.id === lastSelected) ? lastSelected : null;
|
|
205
|
+
const initialFeatureId = featureId || validLastSelected || featureList[0]?.id;
|
|
152
206
|
if (initialFeatureId) {
|
|
153
207
|
setSelectedFeatureId(initialFeatureId);
|
|
154
208
|
}
|
|
@@ -194,7 +248,8 @@ const InterfaceManagement = () => {
|
|
|
194
248
|
id: resp.id || `resp-${index}-${Date.now()}`,
|
|
195
249
|
name: resp.name || `响应 ${index + 1}`,
|
|
196
250
|
description: resp.description || '',
|
|
197
|
-
content: resp.content || '{}'
|
|
251
|
+
content: resp.content || '{}',
|
|
252
|
+
paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
|
|
198
253
|
}));
|
|
199
254
|
|
|
200
255
|
return item;
|
|
@@ -400,7 +455,8 @@ const InterfaceManagement = () => {
|
|
|
400
455
|
id: newId,
|
|
401
456
|
name: resp.name || '未命名响应',
|
|
402
457
|
description: resp.description || '',
|
|
403
|
-
content: resp.content || '{}'
|
|
458
|
+
content: resp.content || '{}',
|
|
459
|
+
paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers.map(m => ({ ...m })) : []
|
|
404
460
|
};
|
|
405
461
|
});
|
|
406
462
|
const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
|
|
@@ -516,6 +572,14 @@ const InterfaceManagement = () => {
|
|
|
516
572
|
groupValue = groupValue.length > 0 ? groupValue[0] : '';
|
|
517
573
|
}
|
|
518
574
|
|
|
575
|
+
// 处理 responses,清理每个响应中的空 paramMatchers 规则
|
|
576
|
+
const cleanedResponses = (values.responses || (editingInterface ? editingInterface.responses : [])).map(resp => ({
|
|
577
|
+
...resp,
|
|
578
|
+
paramMatchers: Array.isArray(resp.paramMatchers)
|
|
579
|
+
? resp.paramMatchers.filter(m => m && m.paramPath && m.paramValue)
|
|
580
|
+
: []
|
|
581
|
+
}));
|
|
582
|
+
|
|
519
583
|
// 构建接口数据 - 在编辑模式下,确保关键字段不丢失
|
|
520
584
|
const interfaceData = {
|
|
521
585
|
name: values.name || (editingInterface ? editingInterface.name : ''),
|
|
@@ -523,7 +587,7 @@ const InterfaceManagement = () => {
|
|
|
523
587
|
urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
|
|
524
588
|
proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
|
|
525
589
|
featureId: selectedFeatureId,
|
|
526
|
-
responses:
|
|
590
|
+
responses: cleanedResponses,
|
|
527
591
|
activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
|
|
528
592
|
httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
|
|
529
593
|
contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
|
|
@@ -590,6 +654,7 @@ const InterfaceManagement = () => {
|
|
|
590
654
|
|
|
591
655
|
const handleSelectFeature = (featureId) => {
|
|
592
656
|
setSelectedFeatureId(featureId);
|
|
657
|
+
localStorage.setItem('selectedFeatureId', featureId);
|
|
593
658
|
// 切换功能模块时重置分页到第一页,但保留其他配置
|
|
594
659
|
saveTableConfig({ current: 1 });
|
|
595
660
|
};
|
|
@@ -690,12 +755,7 @@ const InterfaceManagement = () => {
|
|
|
690
755
|
// 重置列配置
|
|
691
756
|
const handleResetColumns = () => {
|
|
692
757
|
const defaultColumns = COLUMN_CONFIG.map(col => col.key);
|
|
693
|
-
saveTableConfig({
|
|
694
|
-
visibleColumns: defaultColumns,
|
|
695
|
-
columnWidths: {
|
|
696
|
-
currentResponse: 180
|
|
697
|
-
}
|
|
698
|
-
});
|
|
758
|
+
saveTableConfig({ visibleColumns: defaultColumns });
|
|
699
759
|
};
|
|
700
760
|
|
|
701
761
|
// 切换列配置面板显示
|
|
@@ -726,61 +786,6 @@ const InterfaceManagement = () => {
|
|
|
726
786
|
message.success('数据已刷新');
|
|
727
787
|
};
|
|
728
788
|
|
|
729
|
-
// 可调整大小的表头组件(使用原生实现)
|
|
730
|
-
const ResizableTitle = (props) => {
|
|
731
|
-
const { onResize, width, columnKey, ...restProps } = props;
|
|
732
|
-
|
|
733
|
-
if (!width || !onResize) {
|
|
734
|
-
return <th {...restProps} />;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const handleMouseDown = (e) => {
|
|
738
|
-
e.preventDefault();
|
|
739
|
-
e.stopPropagation();
|
|
740
|
-
|
|
741
|
-
const startX = e.clientX;
|
|
742
|
-
const startWidth = width;
|
|
743
|
-
|
|
744
|
-
const handleMouseMove = (moveEvent) => {
|
|
745
|
-
const deltaX = moveEvent.clientX - startX;
|
|
746
|
-
const newWidth = Math.max(50, startWidth + deltaX); // 最小宽度 50px
|
|
747
|
-
onResize(columnKey, newWidth);
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
const handleMouseUp = () => {
|
|
751
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
752
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
753
|
-
document.body.style.cursor = '';
|
|
754
|
-
document.body.style.userSelect = '';
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
758
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
759
|
-
document.body.style.cursor = 'col-resize';
|
|
760
|
-
document.body.style.userSelect = 'none';
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
return (
|
|
764
|
-
<th {...restProps} style={{ ...restProps.style, position: 'relative' }}>
|
|
765
|
-
{restProps.children}
|
|
766
|
-
<span
|
|
767
|
-
className="column-resize-handle"
|
|
768
|
-
onMouseDown={handleMouseDown}
|
|
769
|
-
onClick={(e) => e.stopPropagation()}
|
|
770
|
-
/>
|
|
771
|
-
</th>
|
|
772
|
-
);
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
// 处理列宽调整
|
|
776
|
-
const handleResize = (columnKey, newWidth) => {
|
|
777
|
-
const newColumnWidths = {
|
|
778
|
-
...tableConfig.columnWidths,
|
|
779
|
-
[columnKey]: newWidth
|
|
780
|
-
};
|
|
781
|
-
saveTableConfig({ columnWidths: newColumnWidths });
|
|
782
|
-
};
|
|
783
|
-
|
|
784
789
|
// 批量操作接口状态
|
|
785
790
|
const handleBatchOperation = async (active) => {
|
|
786
791
|
if (!selectedRowKeys || selectedRowKeys.length === 0) {
|
|
@@ -878,37 +883,27 @@ const InterfaceManagement = () => {
|
|
|
878
883
|
title: '当前响应',
|
|
879
884
|
dataIndex: 'activeResponseId',
|
|
880
885
|
key: 'currentResponse',
|
|
881
|
-
width:
|
|
882
|
-
onHeaderCell: (column) => ({
|
|
883
|
-
width: column.width,
|
|
884
|
-
columnKey: 'currentResponse',
|
|
885
|
-
onResize: handleResize,
|
|
886
|
-
}),
|
|
886
|
+
width: 200,
|
|
887
887
|
render: (activeResponseId, record) => {
|
|
888
888
|
// 如果没有响应数据,返回空
|
|
889
889
|
if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
|
|
890
890
|
return <span className="no-response">无响应数据</span>;
|
|
891
891
|
}
|
|
892
|
-
|
|
893
|
-
// 找到当前激活的响应
|
|
894
|
-
const activeResponse = record.responses.find(resp => resp.id === activeResponseId);
|
|
895
|
-
|
|
896
|
-
// 如果没有找到激活的响应,使用第一个
|
|
897
|
-
const currentResponse = activeResponse || record.responses[0];
|
|
898
|
-
|
|
892
|
+
|
|
899
893
|
return (
|
|
900
894
|
<Select
|
|
901
895
|
value={activeResponseId || record.responses[0]?.id}
|
|
902
896
|
style={{ width: '100%' }}
|
|
903
897
|
onChange={(value) => handleResponseSwitch(record.id, value)}
|
|
904
898
|
disabled={record.proxyType !== 'response'}
|
|
905
|
-
getPopupContainer={(trigger) => trigger.parentNode}
|
|
906
899
|
>
|
|
907
|
-
{record.responses.map(resp =>
|
|
908
|
-
|
|
909
|
-
{resp.
|
|
910
|
-
|
|
911
|
-
|
|
900
|
+
{record.responses.map(resp => {
|
|
901
|
+
return (
|
|
902
|
+
<Option key={resp.id} value={resp.id}>
|
|
903
|
+
{resp.name}
|
|
904
|
+
</Option>
|
|
905
|
+
);
|
|
906
|
+
})}
|
|
912
907
|
</Select>
|
|
913
908
|
);
|
|
914
909
|
},
|
|
@@ -1029,35 +1024,78 @@ const InterfaceManagement = () => {
|
|
|
1029
1024
|
title: '参数匹配',
|
|
1030
1025
|
dataIndex: 'paramMatchers',
|
|
1031
1026
|
key: 'paramMatchers',
|
|
1032
|
-
width:
|
|
1027
|
+
width: 120,
|
|
1033
1028
|
render: (_, record) => {
|
|
1034
1029
|
if (record.proxyType !== 'response') {
|
|
1035
1030
|
return '-';
|
|
1036
1031
|
}
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1032
|
+
|
|
1033
|
+
const showInterface = interfaceParamMatcherEnabled;
|
|
1034
|
+
const showResponse = responseParamMatcherEnabled;
|
|
1035
|
+
|
|
1036
|
+
const interfaceMatchers = record.paramMatchers || [];
|
|
1037
|
+
const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
|
|
1038
|
+
|
|
1039
|
+
// 统计所有响应级规则
|
|
1040
|
+
const responsesWithMatchers = showResponse
|
|
1041
|
+
? (record.responses || []).filter(resp =>
|
|
1042
|
+
Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
|
|
1043
|
+
)
|
|
1044
|
+
: [];
|
|
1045
|
+
const responseMatcherTotal = responsesWithMatchers.reduce(
|
|
1046
|
+
(sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
if (interfaceCount === 0 && responseMatcherTotal === 0) {
|
|
1042
1050
|
return '-';
|
|
1043
1051
|
}
|
|
1044
|
-
|
|
1052
|
+
|
|
1053
|
+
const tooltipContent = (
|
|
1054
|
+
<div style={{ maxWidth: 280 }}>
|
|
1055
|
+
{interfaceCount > 0 && (
|
|
1056
|
+
<div style={{ marginBottom: responsesWithMatchers.length > 0 ? 8 : 0 }}>
|
|
1057
|
+
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#faad14' }}>接口级条件({interfaceCount}条)</div>
|
|
1058
|
+
{interfaceMatchers.filter(m => m.paramPath).map((m, i) => (
|
|
1059
|
+
<div key={i} style={{ fontSize: '12px' }}>
|
|
1060
|
+
{m.paramPath}: {m.paramValue}
|
|
1061
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
1062
|
+
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
|
|
1063
|
+
</span>
|
|
1064
|
+
</div>
|
|
1065
|
+
))}
|
|
1066
|
+
</div>
|
|
1067
|
+
)}
|
|
1068
|
+
{responsesWithMatchers.length > 0 && (
|
|
1069
|
+
<div>
|
|
1070
|
+
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1890ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
|
|
1071
|
+
{responsesWithMatchers.map((resp, ri) => (
|
|
1072
|
+
<div key={ri} style={{ marginBottom: 4 }}>
|
|
1073
|
+
<div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
|
|
1074
|
+
{resp.paramMatchers.filter(m => m.paramPath).map((m, mi) => (
|
|
1075
|
+
<div key={mi} style={{ fontSize: '12px', paddingLeft: 8 }}>
|
|
1076
|
+
{m.paramPath}: {m.paramValue}
|
|
1077
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
1078
|
+
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
|
|
1079
|
+
</span>
|
|
1080
|
+
</div>
|
|
1081
|
+
))}
|
|
1082
|
+
</div>
|
|
1083
|
+
))}
|
|
1084
|
+
</div>
|
|
1085
|
+
)}
|
|
1086
|
+
</div>
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1045
1089
|
return (
|
|
1046
|
-
<Tooltip title={
|
|
1047
|
-
<
|
|
1048
|
-
{
|
|
1049
|
-
<
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
))}
|
|
1056
|
-
</div>
|
|
1057
|
-
}>
|
|
1058
|
-
<span style={{ color: '#1890ff' }}>
|
|
1059
|
-
{count}条规则
|
|
1060
|
-
</span>
|
|
1090
|
+
<Tooltip title={tooltipContent}>
|
|
1091
|
+
<Space size={4} direction="vertical" style={{ lineHeight: 1.4 }}>
|
|
1092
|
+
{interfaceCount > 0 && (
|
|
1093
|
+
<Tag color="orange" style={{ fontSize: '11px', padding: '0 4px' }}>接口:{interfaceCount}条</Tag>
|
|
1094
|
+
)}
|
|
1095
|
+
{responseMatcherTotal > 0 && (
|
|
1096
|
+
<Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>响应:{responseMatcherTotal}条</Tag>
|
|
1097
|
+
)}
|
|
1098
|
+
</Space>
|
|
1061
1099
|
</Tooltip>
|
|
1062
1100
|
);
|
|
1063
1101
|
}
|
|
@@ -1414,11 +1452,6 @@ const InterfaceManagement = () => {
|
|
|
1414
1452
|
selectedRowKeys,
|
|
1415
1453
|
onChange: onSelectChange,
|
|
1416
1454
|
}}
|
|
1417
|
-
components={{
|
|
1418
|
-
header: {
|
|
1419
|
-
cell: ResizableTitle,
|
|
1420
|
-
},
|
|
1421
|
-
}}
|
|
1422
1455
|
size="middle"
|
|
1423
1456
|
pagination={{
|
|
1424
1457
|
current: tableConfig.current,
|
|
@@ -1436,8 +1469,250 @@ const InterfaceManagement = () => {
|
|
|
1436
1469
|
</div>
|
|
1437
1470
|
</Card>
|
|
1438
1471
|
|
|
1439
|
-
{/*
|
|
1440
|
-
|
|
1472
|
+
{/* 接口编辑/创建模态框/抽屉 */}
|
|
1473
|
+
{interfaceFormDisplayMode === 'drawer' ? (
|
|
1474
|
+
<Drawer
|
|
1475
|
+
title={
|
|
1476
|
+
<div style={{
|
|
1477
|
+
fontSize: '18px',
|
|
1478
|
+
fontWeight: 600,
|
|
1479
|
+
color: '#262626',
|
|
1480
|
+
display: 'flex',
|
|
1481
|
+
alignItems: 'center',
|
|
1482
|
+
gap: '8px'
|
|
1483
|
+
}}>
|
|
1484
|
+
<span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
|
|
1485
|
+
<Badge
|
|
1486
|
+
count="配置向导"
|
|
1487
|
+
style={{
|
|
1488
|
+
backgroundColor: '#f0f9ff',
|
|
1489
|
+
color: '#1677ff',
|
|
1490
|
+
fontSize: '11px',
|
|
1491
|
+
fontWeight: 'normal'
|
|
1492
|
+
}}
|
|
1493
|
+
/>
|
|
1494
|
+
</div>
|
|
1495
|
+
}
|
|
1496
|
+
placement="right"
|
|
1497
|
+
width={1040}
|
|
1498
|
+
open={modalVisible}
|
|
1499
|
+
onClose={handleCancel}
|
|
1500
|
+
destroyOnClose
|
|
1501
|
+
bodyStyle={{
|
|
1502
|
+
padding: '0',
|
|
1503
|
+
background: '#fafafa',
|
|
1504
|
+
overflowY: 'auto'
|
|
1505
|
+
}}
|
|
1506
|
+
footer={
|
|
1507
|
+
<div style={{ textAlign: 'right' }}>
|
|
1508
|
+
<Space>
|
|
1509
|
+
<Button onClick={handleCancel}>取消</Button>
|
|
1510
|
+
<Button type="primary" onClick={handleSubmit}>
|
|
1511
|
+
{editingInterface ? '保存' : '创建'}
|
|
1512
|
+
</Button>
|
|
1513
|
+
</Space>
|
|
1514
|
+
</div>
|
|
1515
|
+
}
|
|
1516
|
+
className="interface-drawer"
|
|
1517
|
+
>
|
|
1518
|
+
<div style={{ padding: '24px' }}>
|
|
1519
|
+
<Form
|
|
1520
|
+
form={form}
|
|
1521
|
+
layout="vertical"
|
|
1522
|
+
initialValues={{
|
|
1523
|
+
name: '',
|
|
1524
|
+
group: '',
|
|
1525
|
+
pattern: '',
|
|
1526
|
+
proxyType: 'response',
|
|
1527
|
+
statusCode: '200',
|
|
1528
|
+
contentType: 'application/json; charset=utf-8',
|
|
1529
|
+
responses: [],
|
|
1530
|
+
httpMethod: 'ALL',
|
|
1531
|
+
targetUrl: '',
|
|
1532
|
+
headerItems: [],
|
|
1533
|
+
paramMatchers: [],
|
|
1534
|
+
responseDelay: '0'
|
|
1535
|
+
}}
|
|
1536
|
+
>
|
|
1537
|
+
<Collapse
|
|
1538
|
+
defaultActiveKey={
|
|
1539
|
+
editingInterface && editingInterface.proxyType === 'response'
|
|
1540
|
+
? ['3']
|
|
1541
|
+
: ['1', '2', '3']
|
|
1542
|
+
}
|
|
1543
|
+
style={{ marginBottom: '16px' }}
|
|
1544
|
+
ghost
|
|
1545
|
+
>
|
|
1546
|
+
{/* 第一步:基础信息 */}
|
|
1547
|
+
<Collapse.Panel
|
|
1548
|
+
header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="1" style={{ backgroundColor: '#1677ff' }} /><span>基础信息</span></div>}
|
|
1549
|
+
key="1"
|
|
1550
|
+
>
|
|
1551
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1552
|
+
<div style={{ flex: 1 }}>
|
|
1553
|
+
<Form.Item name="name" label="接口名称" rules={[{ required: true, message: '请输入接口名称' }]}>
|
|
1554
|
+
<Input placeholder="请输入接口名称" />
|
|
1555
|
+
</Form.Item>
|
|
1556
|
+
</div>
|
|
1557
|
+
<div style={{ flex: 1 }}>
|
|
1558
|
+
<Form.Item name="group" label="分组" tooltip="为接口设置分组,便于管理和批量操作">
|
|
1559
|
+
<Select placeholder="选择或创建分组" showSearch allowClear style={{ width: '100%' }} mode="tags" maxTagCount="responsive" maxTagTextLength={10}
|
|
1560
|
+
onChange={(value) => {
|
|
1561
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
1562
|
+
const groupValue = value[0] === '' ? undefined : value[0];
|
|
1563
|
+
form.setFieldsValue({ group: groupValue });
|
|
1564
|
+
}
|
|
1565
|
+
}}
|
|
1566
|
+
onInputKeyDown={(e) => { if (e.key === 'Enter') e.stopPropagation(); }}
|
|
1567
|
+
>
|
|
1568
|
+
{Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
|
|
1569
|
+
<Option key={group} value={group}>{group}</Option>
|
|
1570
|
+
))}
|
|
1571
|
+
</Select>
|
|
1572
|
+
</Form.Item>
|
|
1573
|
+
</div>
|
|
1574
|
+
</div>
|
|
1575
|
+
</Collapse.Panel>
|
|
1576
|
+
|
|
1577
|
+
{/* 第二步:URL配置(复用 Modal 的内容) */}
|
|
1578
|
+
<Collapse.Panel
|
|
1579
|
+
header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="2" style={{ backgroundColor: '#1677ff' }} /><span>URL配置</span></div>}
|
|
1580
|
+
key="2"
|
|
1581
|
+
>
|
|
1582
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1583
|
+
<div style={{ flex: 1 }}>
|
|
1584
|
+
<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(); } })]}>
|
|
1585
|
+
<Input placeholder="根据选择的处理方式输入相应格式的URL" />
|
|
1586
|
+
</Form.Item>
|
|
1587
|
+
</div>
|
|
1588
|
+
<div style={{ flex: 1 }}>
|
|
1589
|
+
<Form.Item name="proxyType" label="处理方式" rules={[{ required: true, message: '请选择处理方式' }]}>
|
|
1590
|
+
<Select onChange={() => form.setFieldsValue({ pattern: '' })}>
|
|
1591
|
+
{proxyTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}
|
|
1592
|
+
</Select>
|
|
1593
|
+
</Form.Item>
|
|
1594
|
+
</div>
|
|
1595
|
+
</div>
|
|
1596
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1597
|
+
<div style={{ flex: 1 }}>
|
|
1598
|
+
<Form.Item name="httpMethod" label="请求方法" rules={[{ required: true, message: '请选择请求方法' }]}>
|
|
1599
|
+
<Select>{httpMethods.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
|
|
1600
|
+
</Form.Item>
|
|
1601
|
+
</div>
|
|
1602
|
+
<div style={{ flex: 1 }} />
|
|
1603
|
+
</div>
|
|
1604
|
+
</Collapse.Panel>
|
|
1605
|
+
|
|
1606
|
+
{/* 第三步:高级配置 */}
|
|
1607
|
+
<Collapse.Panel
|
|
1608
|
+
header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="3" style={{ backgroundColor: '#1677ff' }} /><span>高级配置</span></div>}
|
|
1609
|
+
key="3"
|
|
1610
|
+
>
|
|
1611
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
1612
|
+
{({ getFieldValue, setFieldsValue }) => {
|
|
1613
|
+
const proxyType = getFieldValue('proxyType');
|
|
1614
|
+
if (proxyType === 'response') {
|
|
1615
|
+
return (
|
|
1616
|
+
<>
|
|
1617
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1618
|
+
<div style={{ flex: 1 }}>
|
|
1619
|
+
<Form.Item name="statusCode" label="状态码" rules={[{ required: true, message: '请选择状态码' }]}>
|
|
1620
|
+
<Select>{statusCodes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
|
|
1621
|
+
</Form.Item>
|
|
1622
|
+
</div>
|
|
1623
|
+
<div style={{ flex: 1 }}>
|
|
1624
|
+
<Form.Item name="contentType" label="内容类型" rules={[{ required: true, message: '请选择内容类型' }]}>
|
|
1625
|
+
<Select>{contentTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
|
|
1626
|
+
</Form.Item>
|
|
1627
|
+
</div>
|
|
1628
|
+
<div style={{ flex: 1 }}>
|
|
1629
|
+
<Form.Item name="responseDelay" label="延迟响应(毫秒)" rules={[{ pattern: /^\d*$/, message: '请输入有效的数字' }]}>
|
|
1630
|
+
<Input placeholder="例如:500" />
|
|
1631
|
+
</Form.Item>
|
|
1632
|
+
</div>
|
|
1633
|
+
</div>
|
|
1634
|
+
{interfaceParamMatcherEnabled && (
|
|
1635
|
+
<Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。">
|
|
1636
|
+
<Form.List name="paramMatchers">
|
|
1637
|
+
{(fields, { add, remove }) => (<>
|
|
1638
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1639
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
1640
|
+
<Form.Item {...restField} name={[name, 'paramPath']} rules={[{ required: true, message: '请输入参数路径' }]} style={{ flex: 1, margin: 0 }}>
|
|
1641
|
+
<Input placeholder="参数路径(如:userId 或 data.user.id)" />
|
|
1642
|
+
</Form.Item>
|
|
1643
|
+
<Form.Item {...restField} name={[name, 'paramValue']} rules={[{ required: true, message: '请输入期望值' }]} style={{ flex: 1, margin: 0 }}>
|
|
1644
|
+
<Input placeholder="期望值(如:123 或 admin)" />
|
|
1645
|
+
</Form.Item>
|
|
1646
|
+
<Form.Item {...restField} name={[name, 'matchType']} style={{ width: 90, margin: 0 }} initialValue="exact">
|
|
1647
|
+
<Select><Option value="exact">精确匹配</Option><Option value="contains">包含</Option><Option value="regex">正则</Option></Select>
|
|
1648
|
+
</Form.Item>
|
|
1649
|
+
<Form.Item noStyle shouldUpdate>
|
|
1650
|
+
{({ getFieldValue }) => {
|
|
1651
|
+
const responses = getFieldValue('responses') || [];
|
|
1652
|
+
return (
|
|
1653
|
+
<Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 130, margin: 0 }}>
|
|
1654
|
+
<Select allowClear placeholder="指定响应(可选)">
|
|
1655
|
+
{responses.filter(r => r && r.id).map(r => (
|
|
1656
|
+
<Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
|
|
1657
|
+
))}
|
|
1658
|
+
</Select>
|
|
1659
|
+
</Form.Item>
|
|
1660
|
+
);
|
|
1661
|
+
}}
|
|
1662
|
+
</Form.Item>
|
|
1663
|
+
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
|
1664
|
+
</div>
|
|
1665
|
+
))}
|
|
1666
|
+
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%' }}>添加参数匹配规则</Button>
|
|
1667
|
+
</>)}
|
|
1668
|
+
</Form.List>
|
|
1669
|
+
</Form.Item>
|
|
1670
|
+
)}
|
|
1671
|
+
<Form.Item name="responses" initialValue={[]} hidden><Input /></Form.Item>
|
|
1672
|
+
<Form.Item name="activeResponseId" hidden><Input /></Form.Item>
|
|
1673
|
+
<Form.Item noStyle shouldUpdate>
|
|
1674
|
+
{({ getFieldValue }) => {
|
|
1675
|
+
let responses = getFieldValue('responses') || [];
|
|
1676
|
+
const activeResponseId = getFieldValue('activeResponseId');
|
|
1677
|
+
if (!Array.isArray(responses)) { try { responses = JSON.parse(responses); } catch (e) { responses = []; } }
|
|
1678
|
+
return (<ResponseContentEditor form={form} responses={responses} activeResponseId={activeResponseId} onPreview={handlePreview} />);
|
|
1679
|
+
}}
|
|
1680
|
+
</Form.Item>
|
|
1681
|
+
</>
|
|
1682
|
+
);
|
|
1683
|
+
} else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
1684
|
+
return (
|
|
1685
|
+
<>
|
|
1686
|
+
<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格式">
|
|
1687
|
+
<Input placeholder="例如:https://api.example.com/users" />
|
|
1688
|
+
</Form.Item>
|
|
1689
|
+
<Form.Item label="自定义请求头">
|
|
1690
|
+
<Form.List name="headerItems">
|
|
1691
|
+
{(fields, { add, remove }) => (<>
|
|
1692
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1693
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
1694
|
+
<Form.Item {...restField} name={[name, 'headerName']} rules={[{ required: true, message: '请输入请求头名称' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头名称(如:Authorization)" /></Form.Item>
|
|
1695
|
+
<Form.Item {...restField} name={[name, 'headerValue']} rules={[{ required: true, message: '请输入请求头值' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头值(如:Bearer token123)" /></Form.Item>
|
|
1696
|
+
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
|
1697
|
+
</div>
|
|
1698
|
+
))}
|
|
1699
|
+
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%', marginTop: '8px' }}>添加请求头</Button>
|
|
1700
|
+
</>)}
|
|
1701
|
+
</Form.List>
|
|
1702
|
+
</Form.Item>
|
|
1703
|
+
</>
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1707
|
+
}}
|
|
1708
|
+
</Form.Item>
|
|
1709
|
+
</Collapse.Panel>
|
|
1710
|
+
</Collapse>
|
|
1711
|
+
</Form>
|
|
1712
|
+
</div>
|
|
1713
|
+
</Drawer>
|
|
1714
|
+
) : (
|
|
1715
|
+
<Modal
|
|
1441
1716
|
title={
|
|
1442
1717
|
<div style={{
|
|
1443
1718
|
fontSize: '18px',
|
|
@@ -1784,10 +2059,11 @@ const InterfaceManagement = () => {
|
|
|
1784
2059
|
</div>
|
|
1785
2060
|
</div>
|
|
1786
2061
|
|
|
1787
|
-
{/*
|
|
2062
|
+
{/* 请求入参匹配设置(接口级,用于同URL多接口区分) */}
|
|
2063
|
+
{interfaceParamMatcherEnabled && (
|
|
1788
2064
|
<Form.Item
|
|
1789
|
-
label="
|
|
1790
|
-
tooltip="
|
|
2065
|
+
label="接口匹配条件"
|
|
2066
|
+
tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。支持嵌套属性路径,如:a.b.c"
|
|
1791
2067
|
>
|
|
1792
2068
|
<Form.List name="paramMatchers">
|
|
1793
2069
|
{(fields, { add, remove }) => (
|
|
@@ -1828,6 +2104,20 @@ const InterfaceManagement = () => {
|
|
|
1828
2104
|
<Option value="regex">正则</Option>
|
|
1829
2105
|
</Select>
|
|
1830
2106
|
</Form.Item>
|
|
2107
|
+
<Form.Item noStyle shouldUpdate>
|
|
2108
|
+
{({ getFieldValue }) => {
|
|
2109
|
+
const responses = getFieldValue('responses') || [];
|
|
2110
|
+
return (
|
|
2111
|
+
<Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 140, margin: 0 }}>
|
|
2112
|
+
<Select allowClear placeholder="指定响应(可选)">
|
|
2113
|
+
{responses.filter(r => r && r.id).map(r => (
|
|
2114
|
+
<Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
|
|
2115
|
+
))}
|
|
2116
|
+
</Select>
|
|
2117
|
+
</Form.Item>
|
|
2118
|
+
);
|
|
2119
|
+
}}
|
|
2120
|
+
</Form.Item>
|
|
1831
2121
|
<Button
|
|
1832
2122
|
type="text"
|
|
1833
2123
|
danger
|
|
@@ -1876,6 +2166,7 @@ const InterfaceManagement = () => {
|
|
|
1876
2166
|
)}
|
|
1877
2167
|
</Form.List>
|
|
1878
2168
|
</Form.Item>
|
|
2169
|
+
)}
|
|
1879
2170
|
|
|
1880
2171
|
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
1881
2172
|
<Form.Item
|
|
@@ -2000,6 +2291,7 @@ const InterfaceManagement = () => {
|
|
|
2000
2291
|
</Form>
|
|
2001
2292
|
</div>
|
|
2002
2293
|
</Modal>
|
|
2294
|
+
)}
|
|
2003
2295
|
|
|
2004
2296
|
{/* 使用拆分后的预览模态框组件 */}
|
|
2005
2297
|
<PreviewModal
|
|
@@ -2008,15 +2300,26 @@ const InterfaceManagement = () => {
|
|
|
2008
2300
|
previewContent={previewContent}
|
|
2009
2301
|
/>
|
|
2010
2302
|
|
|
2011
|
-
{/*
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2303
|
+
{/* 数据管理弹窗/抽屉 */}
|
|
2304
|
+
{displayMode === 'drawer' ? (
|
|
2305
|
+
<DataManagementDrawer
|
|
2306
|
+
visible={dataModalVisible}
|
|
2307
|
+
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
2308
|
+
interfaceItem={dataModalRecord}
|
|
2309
|
+
onSaved={() => {
|
|
2310
|
+
fetchInterfaces();
|
|
2311
|
+
}}
|
|
2312
|
+
/>
|
|
2313
|
+
) : (
|
|
2314
|
+
<DataManagementModal
|
|
2315
|
+
visible={dataModalVisible}
|
|
2316
|
+
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
2317
|
+
interfaceItem={dataModalRecord}
|
|
2318
|
+
onSaved={() => {
|
|
2319
|
+
fetchInterfaces();
|
|
2320
|
+
}}
|
|
2321
|
+
/>
|
|
2322
|
+
)}
|
|
2020
2323
|
</div>
|
|
2021
2324
|
</AppLayout>
|
|
2022
2325
|
);
|