whistle.mock-plugins 1.2.2-beta.6 → 1.2.2-beta.8
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 +0 -34
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/ruleManager.js +55 -23
- package/package.json +1 -2
- package/ui/src/components/interface-management/ResponseContentEditor.js +178 -30
- package/ui/src/components/interface-management/index.js +0 -1
- package/ui/src/pages/InterfaceManagementV2.js +205 -985
- package/ui/src/pages/Settings.js +6 -75
- package/ui/src/styles/interface-management.css +0 -37
- package/ui/src/styles/settings.css +0 -29
- package/COLUMN_RESIZE_IMPLEMENTATION.md +0 -162
- package/TEST_GUIDE.md +0 -156
- package/ui/src/components/interface-management/DataManagementDrawer.js +0 -115
|
@@ -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,7 +31,6 @@ import {
|
|
|
32
31
|
generateResponseId
|
|
33
32
|
} from '../components/interface-management';
|
|
34
33
|
import DataManagementModal from '../components/interface-management/DataManagementModal';
|
|
35
|
-
import { DataManagementDrawer } from '../components/interface-management';
|
|
36
34
|
|
|
37
35
|
const { Option } = Select;
|
|
38
36
|
const { Search } = AntInput;
|
|
@@ -44,13 +42,13 @@ const COLUMN_CONFIG = [
|
|
|
44
42
|
{ key: 'group', title: '分组', required: false },
|
|
45
43
|
{ key: 'urlPattern', title: 'URL匹配规则', required: false },
|
|
46
44
|
{ key: 'proxyType', title: '处理方式', required: false },
|
|
47
|
-
{ key: 'currentResponse', title: '
|
|
45
|
+
{ key: 'currentResponse', title: '响应配置', required: false },
|
|
48
46
|
{ key: 'responseDelay', title: '延迟(毫秒)', required: false },
|
|
49
47
|
{ key: 'httpStatus', title: '状态码', required: false },
|
|
50
48
|
{ key: 'contentType', title: '内容类型', required: false },
|
|
51
49
|
{ key: 'targetUrl', title: '目标URL', required: false },
|
|
52
50
|
{ key: 'customHeaders', title: '自定义头', required: false },
|
|
53
|
-
{ key: 'paramMatchers', title: '
|
|
51
|
+
{ key: 'paramMatchers', title: '匹配规则', required: false },
|
|
54
52
|
{ key: 'httpMethod', title: '请求方法', required: false },
|
|
55
53
|
{ key: 'action', title: '操作', required: true }
|
|
56
54
|
];
|
|
@@ -92,12 +90,6 @@ const InterfaceManagement = () => {
|
|
|
92
90
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
93
91
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
94
92
|
|
|
95
|
-
// 数据管理显示模式
|
|
96
|
-
const [displayMode, setDisplayMode] = useState('modal');
|
|
97
|
-
|
|
98
|
-
// 接口表单显示模式
|
|
99
|
-
const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
|
|
100
|
-
|
|
101
93
|
// 表单实例
|
|
102
94
|
const [form] = Form.useForm();
|
|
103
95
|
|
|
@@ -105,27 +97,13 @@ const InterfaceManagement = () => {
|
|
|
105
97
|
const [tableConfig, setTableConfig] = useState(() => {
|
|
106
98
|
const cached = localStorage.getItem('interface-table-config');
|
|
107
99
|
const defaultVisibleColumns = COLUMN_CONFIG.map(col => col.key);
|
|
108
|
-
|
|
100
|
+
return cached ? JSON.parse(cached) : {
|
|
109
101
|
sortOrder: null,
|
|
110
102
|
sortField: null,
|
|
111
103
|
pageSize: 10,
|
|
112
104
|
current: 1,
|
|
113
|
-
visibleColumns: defaultVisibleColumns
|
|
114
|
-
columnWidths: {
|
|
115
|
-
currentResponse: 180
|
|
116
|
-
}
|
|
105
|
+
visibleColumns: defaultVisibleColumns
|
|
117
106
|
};
|
|
118
|
-
|
|
119
|
-
if (cached) {
|
|
120
|
-
const parsedConfig = JSON.parse(cached);
|
|
121
|
-
// 确保 columnWidths 存在
|
|
122
|
-
if (!parsedConfig.columnWidths) {
|
|
123
|
-
parsedConfig.columnWidths = { currentResponse: 180 };
|
|
124
|
-
}
|
|
125
|
-
return parsedConfig;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return defaultConfig;
|
|
129
107
|
});
|
|
130
108
|
|
|
131
109
|
// 保存表格配置到localStorage
|
|
@@ -145,52 +123,6 @@ const InterfaceManagement = () => {
|
|
|
145
123
|
}
|
|
146
124
|
}, [selectedFeatureId]);
|
|
147
125
|
|
|
148
|
-
// 监听数据管理显示模式变化
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
// 初始化时从 localStorage 读取配置
|
|
151
|
-
const savedMode = localStorage.getItem('dataManagementDisplayMode');
|
|
152
|
-
if (savedMode) {
|
|
153
|
-
setDisplayMode(savedMode);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 监听 storage 事件,实现设置立即生效
|
|
157
|
-
const handleStorageChange = () => {
|
|
158
|
-
const newMode = localStorage.getItem('dataManagementDisplayMode');
|
|
159
|
-
if (newMode) {
|
|
160
|
-
setDisplayMode(newMode);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
window.addEventListener('storage', handleStorageChange);
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
window.removeEventListener('storage', handleStorageChange);
|
|
168
|
-
};
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
// 监听接口表单显示模式变化
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
// 初始化时从 localStorage 读取配置
|
|
174
|
-
const savedMode = localStorage.getItem('interfaceFormDisplayMode');
|
|
175
|
-
if (savedMode) {
|
|
176
|
-
setInterfaceFormDisplayMode(savedMode);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 监听 storage 事件,实现设置立即生效
|
|
180
|
-
const handleStorageChange = () => {
|
|
181
|
-
const newMode = localStorage.getItem('interfaceFormDisplayMode');
|
|
182
|
-
if (newMode) {
|
|
183
|
-
setInterfaceFormDisplayMode(newMode);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
window.addEventListener('storage', handleStorageChange);
|
|
188
|
-
|
|
189
|
-
return () => {
|
|
190
|
-
window.removeEventListener('storage', handleStorageChange);
|
|
191
|
-
};
|
|
192
|
-
}, []);
|
|
193
|
-
|
|
194
126
|
// 获取当前选中的功能模块
|
|
195
127
|
const selectedFeature = features.find(f => f.id === selectedFeatureId);
|
|
196
128
|
|
|
@@ -743,22 +675,7 @@ const InterfaceManagement = () => {
|
|
|
743
675
|
// 重置列配置
|
|
744
676
|
const handleResetColumns = () => {
|
|
745
677
|
const defaultColumns = COLUMN_CONFIG.map(col => col.key);
|
|
746
|
-
saveTableConfig({
|
|
747
|
-
visibleColumns: defaultColumns,
|
|
748
|
-
columnWidths: {
|
|
749
|
-
active: 80,
|
|
750
|
-
name: 150,
|
|
751
|
-
group: 120,
|
|
752
|
-
urlPattern: 250,
|
|
753
|
-
proxyType: 120,
|
|
754
|
-
currentResponse: 180,
|
|
755
|
-
responseDelay: 100,
|
|
756
|
-
httpStatus: 100,
|
|
757
|
-
contentType: 120,
|
|
758
|
-
targetUrl: 200,
|
|
759
|
-
customHeaders: 100
|
|
760
|
-
}
|
|
761
|
-
});
|
|
678
|
+
saveTableConfig({ visibleColumns: defaultColumns });
|
|
762
679
|
};
|
|
763
680
|
|
|
764
681
|
// 切换列配置面板显示
|
|
@@ -789,74 +706,6 @@ const InterfaceManagement = () => {
|
|
|
789
706
|
message.success('数据已刷新');
|
|
790
707
|
};
|
|
791
708
|
|
|
792
|
-
// 可调整大小的表头组件(使用原生实现)
|
|
793
|
-
const ResizableTitle = (props) => {
|
|
794
|
-
const { onResize, width, columnKey, ...restProps } = props;
|
|
795
|
-
|
|
796
|
-
if (!width || !onResize) {
|
|
797
|
-
return <th {...restProps} />;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const handleMouseDown = (e) => {
|
|
801
|
-
e.preventDefault();
|
|
802
|
-
e.stopPropagation();
|
|
803
|
-
|
|
804
|
-
const startX = e.clientX;
|
|
805
|
-
const startWidth = width;
|
|
806
|
-
let currentWidth = startWidth;
|
|
807
|
-
|
|
808
|
-
const handleMouseMove = (moveEvent) => {
|
|
809
|
-
const deltaX = moveEvent.clientX - startX;
|
|
810
|
-
currentWidth = Math.max(50, startWidth + deltaX); // 最小宽度 50px
|
|
811
|
-
|
|
812
|
-
// 实时更新列宽(不保存到 localStorage)
|
|
813
|
-
const newColumnWidths = {
|
|
814
|
-
...tableConfig.columnWidths,
|
|
815
|
-
[columnKey]: currentWidth
|
|
816
|
-
};
|
|
817
|
-
setTableConfig(prev => ({
|
|
818
|
-
...prev,
|
|
819
|
-
columnWidths: newColumnWidths
|
|
820
|
-
}));
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
const handleMouseUp = () => {
|
|
824
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
825
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
826
|
-
document.body.style.cursor = '';
|
|
827
|
-
document.body.style.userSelect = '';
|
|
828
|
-
|
|
829
|
-
// 拖动结束后保存到 localStorage
|
|
830
|
-
onResize(columnKey, currentWidth);
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
834
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
835
|
-
document.body.style.cursor = 'col-resize';
|
|
836
|
-
document.body.style.userSelect = 'none';
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
return (
|
|
840
|
-
<th {...restProps} style={{ ...restProps.style, position: 'relative' }}>
|
|
841
|
-
{restProps.children}
|
|
842
|
-
<span
|
|
843
|
-
className="column-resize-handle"
|
|
844
|
-
onMouseDown={handleMouseDown}
|
|
845
|
-
onClick={(e) => e.stopPropagation()}
|
|
846
|
-
/>
|
|
847
|
-
</th>
|
|
848
|
-
);
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
// 处理列宽调整
|
|
852
|
-
const handleResize = (columnKey, newWidth) => {
|
|
853
|
-
const newColumnWidths = {
|
|
854
|
-
...tableConfig.columnWidths,
|
|
855
|
-
[columnKey]: newWidth
|
|
856
|
-
};
|
|
857
|
-
saveTableConfig({ columnWidths: newColumnWidths });
|
|
858
|
-
};
|
|
859
|
-
|
|
860
709
|
// 批量操作接口状态
|
|
861
710
|
const handleBatchOperation = async (active) => {
|
|
862
711
|
if (!selectedRowKeys || selectedRowKeys.length === 0) {
|
|
@@ -891,12 +740,7 @@ const InterfaceManagement = () => {
|
|
|
891
740
|
title: '状态',
|
|
892
741
|
dataIndex: 'active',
|
|
893
742
|
key: 'active',
|
|
894
|
-
width:
|
|
895
|
-
onHeaderCell: (column) => ({
|
|
896
|
-
width: column.width,
|
|
897
|
-
columnKey: 'active',
|
|
898
|
-
onResize: handleResize,
|
|
899
|
-
}),
|
|
743
|
+
width: 80,
|
|
900
744
|
render: (active, record) => (
|
|
901
745
|
<Switch
|
|
902
746
|
checked={active !== false}
|
|
@@ -909,12 +753,6 @@ const InterfaceManagement = () => {
|
|
|
909
753
|
title: '名称',
|
|
910
754
|
dataIndex: 'name',
|
|
911
755
|
key: 'name',
|
|
912
|
-
width: tableConfig.columnWidths?.name || 150,
|
|
913
|
-
onHeaderCell: (column) => ({
|
|
914
|
-
width: column.width,
|
|
915
|
-
columnKey: 'name',
|
|
916
|
-
onResize: handleResize,
|
|
917
|
-
}),
|
|
918
756
|
sorter: (a, b) => a.name.localeCompare(b.name),
|
|
919
757
|
render: (text) => <span className="interface-name">{text}</span>,
|
|
920
758
|
},
|
|
@@ -922,12 +760,7 @@ const InterfaceManagement = () => {
|
|
|
922
760
|
title: '分组',
|
|
923
761
|
dataIndex: 'group',
|
|
924
762
|
key: 'group',
|
|
925
|
-
width:
|
|
926
|
-
onHeaderCell: (column) => ({
|
|
927
|
-
width: column.width,
|
|
928
|
-
columnKey: 'group',
|
|
929
|
-
onResize: handleResize,
|
|
930
|
-
}),
|
|
763
|
+
width: 120,
|
|
931
764
|
sorter: (a, b) => {
|
|
932
765
|
const groupA = a.group || '';
|
|
933
766
|
const groupB = b.group || '';
|
|
@@ -948,12 +781,6 @@ const InterfaceManagement = () => {
|
|
|
948
781
|
title: 'URL匹配规则',
|
|
949
782
|
dataIndex: 'urlPattern',
|
|
950
783
|
key: 'urlPattern',
|
|
951
|
-
width: tableConfig.columnWidths?.urlPattern || 250,
|
|
952
|
-
onHeaderCell: (column) => ({
|
|
953
|
-
width: column.width,
|
|
954
|
-
columnKey: 'urlPattern',
|
|
955
|
-
onResize: handleResize,
|
|
956
|
-
}),
|
|
957
784
|
ellipsis: true,
|
|
958
785
|
render: (text) => <span className="url-pattern">{text}</span>,
|
|
959
786
|
},
|
|
@@ -961,12 +788,7 @@ const InterfaceManagement = () => {
|
|
|
961
788
|
title: '处理方式',
|
|
962
789
|
dataIndex: 'proxyType',
|
|
963
790
|
key: 'proxyType',
|
|
964
|
-
width:
|
|
965
|
-
onHeaderCell: (column) => ({
|
|
966
|
-
width: column.width,
|
|
967
|
-
columnKey: 'proxyType',
|
|
968
|
-
onResize: handleResize,
|
|
969
|
-
}),
|
|
791
|
+
width: 120,
|
|
970
792
|
render: (text) => {
|
|
971
793
|
const proxyType = proxyTypes.find(item => item.value === text);
|
|
972
794
|
return (
|
|
@@ -978,45 +800,74 @@ const InterfaceManagement = () => {
|
|
|
978
800
|
},
|
|
979
801
|
},
|
|
980
802
|
{
|
|
981
|
-
title: '
|
|
803
|
+
title: '响应配置',
|
|
982
804
|
dataIndex: 'activeResponseId',
|
|
983
805
|
key: 'currentResponse',
|
|
984
|
-
width:
|
|
985
|
-
className: 'current-response-column',
|
|
986
|
-
onHeaderCell: (column) => ({
|
|
987
|
-
width: column.width,
|
|
988
|
-
columnKey: 'currentResponse',
|
|
989
|
-
onResize: handleResize,
|
|
990
|
-
}),
|
|
991
|
-
onCell: () => ({
|
|
992
|
-
className: 'current-response-cell',
|
|
993
|
-
}),
|
|
806
|
+
width: 160,
|
|
994
807
|
render: (activeResponseId, record) => {
|
|
995
|
-
|
|
808
|
+
if (record.proxyType !== 'response') {
|
|
809
|
+
return <span style={{ color: '#ccc' }}>-</span>;
|
|
810
|
+
}
|
|
996
811
|
if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
|
|
997
|
-
return <span
|
|
812
|
+
return <span style={{ color: '#ff4d4f', fontSize: '12px' }}>无响应数据</span>;
|
|
998
813
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const
|
|
1005
|
-
|
|
814
|
+
|
|
815
|
+
const total = record.responses.length;
|
|
816
|
+
const hasSmartMatch = record.responses.some(
|
|
817
|
+
r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0
|
|
818
|
+
);
|
|
819
|
+
const fallbackResp = record.responses.find(r => r.id === activeResponseId) || record.responses[0];
|
|
820
|
+
|
|
821
|
+
const tooltipContent = (
|
|
822
|
+
<div style={{ maxWidth: 260 }}>
|
|
823
|
+
{record.responses.map((resp) => {
|
|
824
|
+
const matchers = resp.paramMatchers || [];
|
|
825
|
+
return (
|
|
826
|
+
<div key={resp.id} style={{ marginBottom: 6 }}>
|
|
827
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
828
|
+
{matchers.length > 0 ? (
|
|
829
|
+
<Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>{matchers.length}条规则</Tag>
|
|
830
|
+
) : (
|
|
831
|
+
<Tag color="default" style={{ fontSize: '11px', margin: 0 }}>兜底</Tag>
|
|
832
|
+
)}
|
|
833
|
+
<span style={{ fontWeight: resp.id === activeResponseId ? 'bold' : 'normal' }}>
|
|
834
|
+
{resp.name}
|
|
835
|
+
</span>
|
|
836
|
+
</div>
|
|
837
|
+
{matchers.length > 0 && (
|
|
838
|
+
<div style={{ paddingLeft: 8, marginTop: 2 }}>
|
|
839
|
+
{matchers.map((m, i) => (
|
|
840
|
+
<div key={i} style={{ fontSize: '11px', color: '#666' }}>
|
|
841
|
+
{m.paramPath} = {m.paramValue}
|
|
842
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
843
|
+
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
|
|
844
|
+
</span>
|
|
845
|
+
</div>
|
|
846
|
+
))}
|
|
847
|
+
</div>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
);
|
|
851
|
+
})}
|
|
852
|
+
</div>
|
|
853
|
+
);
|
|
854
|
+
|
|
1006
855
|
return (
|
|
1007
|
-
<
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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>
|
|
1020
871
|
);
|
|
1021
872
|
},
|
|
1022
873
|
},
|
|
@@ -1024,12 +875,7 @@ const InterfaceManagement = () => {
|
|
|
1024
875
|
title: '延迟(毫秒)',
|
|
1025
876
|
dataIndex: 'responseDelay',
|
|
1026
877
|
key: 'responseDelay',
|
|
1027
|
-
width:
|
|
1028
|
-
onHeaderCell: (column) => ({
|
|
1029
|
-
width: column.width,
|
|
1030
|
-
columnKey: 'responseDelay',
|
|
1031
|
-
onResize: handleResize,
|
|
1032
|
-
}),
|
|
878
|
+
width: 100,
|
|
1033
879
|
sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
|
|
1034
880
|
render: (delay) => {
|
|
1035
881
|
const delayValue = parseInt(delay, 10) || 0;
|
|
@@ -1044,12 +890,7 @@ const InterfaceManagement = () => {
|
|
|
1044
890
|
title: '状态码',
|
|
1045
891
|
dataIndex: 'httpStatus',
|
|
1046
892
|
key: 'httpStatus',
|
|
1047
|
-
width:
|
|
1048
|
-
onHeaderCell: (column) => ({
|
|
1049
|
-
width: column.width,
|
|
1050
|
-
columnKey: 'httpStatus',
|
|
1051
|
-
onResize: handleResize,
|
|
1052
|
-
}),
|
|
893
|
+
width: 100,
|
|
1053
894
|
render: (status) => {
|
|
1054
895
|
const statusCode = status || 200;
|
|
1055
896
|
let statusClass = 'status-success';
|
|
@@ -1065,12 +906,7 @@ const InterfaceManagement = () => {
|
|
|
1065
906
|
title: '内容类型',
|
|
1066
907
|
dataIndex: 'contentType',
|
|
1067
908
|
key: 'contentType',
|
|
1068
|
-
width:
|
|
1069
|
-
onHeaderCell: (column) => ({
|
|
1070
|
-
width: column.width,
|
|
1071
|
-
columnKey: 'contentType',
|
|
1072
|
-
onResize: handleResize,
|
|
1073
|
-
}),
|
|
909
|
+
width: 120,
|
|
1074
910
|
sorter: (a, b) => {
|
|
1075
911
|
const aType = a.contentType || '';
|
|
1076
912
|
const bType = b.contentType || '';
|
|
@@ -1089,12 +925,6 @@ const InterfaceManagement = () => {
|
|
|
1089
925
|
title: '目标URL',
|
|
1090
926
|
dataIndex: 'targetUrl',
|
|
1091
927
|
key: 'targetUrl',
|
|
1092
|
-
width: tableConfig.columnWidths?.targetUrl || 200,
|
|
1093
|
-
onHeaderCell: (column) => ({
|
|
1094
|
-
width: column.width,
|
|
1095
|
-
columnKey: 'targetUrl',
|
|
1096
|
-
onResize: handleResize,
|
|
1097
|
-
}),
|
|
1098
928
|
ellipsis: true,
|
|
1099
929
|
sorter: (a, b) => {
|
|
1100
930
|
const aUrl = a.targetUrl || '';
|
|
@@ -1110,12 +940,7 @@ const InterfaceManagement = () => {
|
|
|
1110
940
|
title: '自定义头',
|
|
1111
941
|
dataIndex: 'customHeaders',
|
|
1112
942
|
key: 'customHeaders',
|
|
1113
|
-
width:
|
|
1114
|
-
onHeaderCell: (column) => ({
|
|
1115
|
-
width: column.width,
|
|
1116
|
-
columnKey: 'customHeaders',
|
|
1117
|
-
onResize: handleResize,
|
|
1118
|
-
}),
|
|
943
|
+
width: 100,
|
|
1119
944
|
sorter: (a, b) => {
|
|
1120
945
|
const aCount = Object.keys(a.customHeaders || {}).length;
|
|
1121
946
|
const bCount = Object.keys(b.customHeaders || {}).length;
|
|
@@ -1159,38 +984,75 @@ const InterfaceManagement = () => {
|
|
|
1159
984
|
}
|
|
1160
985
|
},
|
|
1161
986
|
{
|
|
1162
|
-
title: '
|
|
1163
|
-
dataIndex: '
|
|
987
|
+
title: '匹配规则',
|
|
988
|
+
dataIndex: 'responses',
|
|
1164
989
|
key: 'paramMatchers',
|
|
1165
990
|
width: 100,
|
|
1166
|
-
render: (
|
|
991
|
+
render: (responses, record) => {
|
|
1167
992
|
if (record.proxyType !== 'response') {
|
|
1168
|
-
return '
|
|
993
|
+
return <span style={{ color: '#ccc' }}>-</span>;
|
|
1169
994
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
995
|
+
|
|
996
|
+
// 汇总所有响应的 paramMatchers(响应级),兼容旧数据接口级 paramMatchers
|
|
997
|
+
const responsesWithMatchers = Array.isArray(responses)
|
|
998
|
+
? responses.filter(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0)
|
|
999
|
+
: [];
|
|
1000
|
+
const totalRules = responsesWithMatchers.reduce((sum, r) => sum + r.paramMatchers.length, 0);
|
|
1001
|
+
|
|
1002
|
+
// 兼容旧数据:接口级 paramMatchers
|
|
1003
|
+
const legacyMatchers = record.paramMatchers || [];
|
|
1004
|
+
|
|
1005
|
+
if (totalRules === 0 && legacyMatchers.length === 0) {
|
|
1006
|
+
return <span style={{ color: '#ccc' }}>-</span>;
|
|
1176
1007
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
<
|
|
1185
|
-
|
|
1186
|
-
|
|
1008
|
+
|
|
1009
|
+
// 新数据:按响应分组展示
|
|
1010
|
+
if (totalRules > 0) {
|
|
1011
|
+
const tooltipContent = (
|
|
1012
|
+
<div style={{ maxWidth: 280 }}>
|
|
1013
|
+
{responsesWithMatchers.map(resp => (
|
|
1014
|
+
<div key={resp.id} style={{ marginBottom: 6 }}>
|
|
1015
|
+
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>{resp.name}:</div>
|
|
1016
|
+
{resp.paramMatchers.map((m, i) => (
|
|
1017
|
+
<div key={i} style={{ paddingLeft: 8, fontSize: '12px' }}>
|
|
1018
|
+
{m.paramPath} = {m.paramValue}
|
|
1019
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
1020
|
+
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
|
|
1021
|
+
</span>
|
|
1022
|
+
</div>
|
|
1023
|
+
))}
|
|
1187
1024
|
</div>
|
|
1188
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
|
+
)}
|
|
1189
1031
|
</div>
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1032
|
+
);
|
|
1033
|
+
return (
|
|
1034
|
+
<Tooltip title={tooltipContent} placement="left">
|
|
1035
|
+
<span style={{ color: '#1890ff', cursor: 'default' }}>{totalRules}条规则</span>
|
|
1036
|
+
</Tooltip>
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// 旧数据兼容展示
|
|
1041
|
+
const tooltipContent = (
|
|
1042
|
+
<div>
|
|
1043
|
+
{legacyMatchers.map((matcher, index) => (
|
|
1044
|
+
<div key={index}>
|
|
1045
|
+
{matcher.paramPath}: {matcher.paramValue}
|
|
1046
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
1047
|
+
({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
|
|
1048
|
+
</span>
|
|
1049
|
+
</div>
|
|
1050
|
+
))}
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
return (
|
|
1054
|
+
<Tooltip title={tooltipContent} placement="left">
|
|
1055
|
+
<span style={{ color: '#fa8c16', cursor: 'default' }}>{legacyMatchers.length}条(旧)</span>
|
|
1194
1056
|
</Tooltip>
|
|
1195
1057
|
);
|
|
1196
1058
|
}
|
|
@@ -1547,11 +1409,6 @@ const InterfaceManagement = () => {
|
|
|
1547
1409
|
selectedRowKeys,
|
|
1548
1410
|
onChange: onSelectChange,
|
|
1549
1411
|
}}
|
|
1550
|
-
components={{
|
|
1551
|
-
header: {
|
|
1552
|
-
cell: ResizableTitle,
|
|
1553
|
-
},
|
|
1554
|
-
}}
|
|
1555
1412
|
size="middle"
|
|
1556
1413
|
pagination={{
|
|
1557
1414
|
current: tableConfig.current,
|
|
@@ -1569,70 +1426,63 @@ const InterfaceManagement = () => {
|
|
|
1569
1426
|
</div>
|
|
1570
1427
|
</Card>
|
|
1571
1428
|
|
|
1572
|
-
{/*
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
httpMethod: 'ALL',
|
|
1630
|
-
targetUrl: '',
|
|
1631
|
-
headerItems: [],
|
|
1632
|
-
paramMatchers: [],
|
|
1633
|
-
responseDelay: '0'
|
|
1634
|
-
}}
|
|
1635
|
-
>
|
|
1429
|
+
{/* 接口编辑/创建模态框 */}
|
|
1430
|
+
<Modal
|
|
1431
|
+
title={
|
|
1432
|
+
<div style={{
|
|
1433
|
+
fontSize: '18px',
|
|
1434
|
+
fontWeight: 600,
|
|
1435
|
+
color: '#262626',
|
|
1436
|
+
display: 'flex',
|
|
1437
|
+
alignItems: 'center',
|
|
1438
|
+
gap: '8px'
|
|
1439
|
+
}}>
|
|
1440
|
+
<span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
|
|
1441
|
+
<Badge
|
|
1442
|
+
count="配置向导"
|
|
1443
|
+
style={{
|
|
1444
|
+
backgroundColor: '#f0f9ff',
|
|
1445
|
+
color: '#1677ff',
|
|
1446
|
+
fontSize: '11px',
|
|
1447
|
+
fontWeight: 'normal'
|
|
1448
|
+
}}
|
|
1449
|
+
/>
|
|
1450
|
+
</div>
|
|
1451
|
+
}
|
|
1452
|
+
open={modalVisible}
|
|
1453
|
+
onOk={handleSubmit}
|
|
1454
|
+
onCancel={handleCancel}
|
|
1455
|
+
width={1200}
|
|
1456
|
+
destroyOnClose
|
|
1457
|
+
okText={editingInterface ? '保存' : '创建'}
|
|
1458
|
+
cancelText="取消"
|
|
1459
|
+
bodyStyle={{
|
|
1460
|
+
maxHeight: '75vh',
|
|
1461
|
+
overflow: 'auto',
|
|
1462
|
+
padding: '0',
|
|
1463
|
+
background: '#fafafa'
|
|
1464
|
+
}}
|
|
1465
|
+
className="interface-modal"
|
|
1466
|
+
>
|
|
1467
|
+
<div style={{ padding: '24px' }}>
|
|
1468
|
+
<Form
|
|
1469
|
+
form={form}
|
|
1470
|
+
layout="vertical"
|
|
1471
|
+
initialValues={{
|
|
1472
|
+
name: '',
|
|
1473
|
+
group: '',
|
|
1474
|
+
pattern: '',
|
|
1475
|
+
proxyType: 'response',
|
|
1476
|
+
statusCode: '200',
|
|
1477
|
+
contentType: 'application/json; charset=utf-8',
|
|
1478
|
+
responses: [],
|
|
1479
|
+
httpMethod: 'ALL',
|
|
1480
|
+
targetUrl: '',
|
|
1481
|
+
headerItems: [],
|
|
1482
|
+
paramMatchers: [],
|
|
1483
|
+
responseDelay: '0'
|
|
1484
|
+
}}
|
|
1485
|
+
>
|
|
1636
1486
|
<Collapse
|
|
1637
1487
|
defaultActiveKey={
|
|
1638
1488
|
editingInterface && editingInterface.proxyType === 'response'
|
|
@@ -1924,97 +1774,9 @@ const InterfaceManagement = () => {
|
|
|
1924
1774
|
</div>
|
|
1925
1775
|
</div>
|
|
1926
1776
|
|
|
1927
|
-
{/*
|
|
1928
|
-
<Form.Item
|
|
1929
|
-
|
|
1930
|
-
tooltip="设置请求参数的匹配条件,只有当请求参数满足条件时才返回对应的响应。支持嵌套属性路径,如:a.b.c"
|
|
1931
|
-
>
|
|
1932
|
-
<Form.List name="paramMatchers">
|
|
1933
|
-
{(fields, { add, remove }) => (
|
|
1934
|
-
<>
|
|
1935
|
-
{fields.map(({ key, name, ...restField }) => (
|
|
1936
|
-
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
1937
|
-
<Form.Item
|
|
1938
|
-
{...restField}
|
|
1939
|
-
name={[name, 'paramPath']}
|
|
1940
|
-
rules={[
|
|
1941
|
-
{ required: true, message: '请输入参数路径' },
|
|
1942
|
-
{
|
|
1943
|
-
pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
|
|
1944
|
-
message: '请输入有效的参数路径,如:userId 或 data.user.id'
|
|
1945
|
-
}
|
|
1946
|
-
]}
|
|
1947
|
-
style={{ flex: 1, margin: 0 }}
|
|
1948
|
-
>
|
|
1949
|
-
<Input placeholder="参数路径(如:userId 或 data.user.id)" />
|
|
1950
|
-
</Form.Item>
|
|
1951
|
-
<Form.Item
|
|
1952
|
-
{...restField}
|
|
1953
|
-
name={[name, 'paramValue']}
|
|
1954
|
-
rules={[{ required: true, message: '请输入期望值' }]}
|
|
1955
|
-
style={{ flex: 1, margin: 0 }}
|
|
1956
|
-
>
|
|
1957
|
-
<Input placeholder="期望值(如:123 或 admin)" />
|
|
1958
|
-
</Form.Item>
|
|
1959
|
-
<Form.Item
|
|
1960
|
-
{...restField}
|
|
1961
|
-
name={[name, 'matchType']}
|
|
1962
|
-
style={{ width: 100, margin: 0 }}
|
|
1963
|
-
initialValue="exact"
|
|
1964
|
-
>
|
|
1965
|
-
<Select placeholder="匹配类型">
|
|
1966
|
-
<Option value="exact">精确匹配</Option>
|
|
1967
|
-
<Option value="contains">包含</Option>
|
|
1968
|
-
<Option value="regex">正则</Option>
|
|
1969
|
-
</Select>
|
|
1970
|
-
</Form.Item>
|
|
1971
|
-
<Button
|
|
1972
|
-
type="text"
|
|
1973
|
-
danger
|
|
1974
|
-
icon={<DeleteOutlined />}
|
|
1975
|
-
onClick={() => remove(name)}
|
|
1976
|
-
/>
|
|
1977
|
-
</div>
|
|
1978
|
-
))}
|
|
1979
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
|
|
1980
|
-
<Button
|
|
1981
|
-
type="dashed"
|
|
1982
|
-
onClick={() => add()}
|
|
1983
|
-
icon={<PlusOutlined />}
|
|
1984
|
-
style={{ flex: 1 }}
|
|
1985
|
-
>
|
|
1986
|
-
添加参数匹配规则
|
|
1987
|
-
</Button>
|
|
1988
|
-
<Popover
|
|
1989
|
-
content={
|
|
1990
|
-
<div style={{ maxWidth: '300px' }}>
|
|
1991
|
-
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
|
|
1992
|
-
<div style={{ lineHeight: '1.6' }}>
|
|
1993
|
-
<div>• <strong>精确匹配</strong>:参数值完全相等</div>
|
|
1994
|
-
<div>• <strong>包含</strong>:参数值包含指定内容</div>
|
|
1995
|
-
<div>• <strong>正则</strong>:参数值符合正则表达式</div>
|
|
1996
|
-
<div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
|
|
1997
|
-
</div>
|
|
1998
|
-
</div>
|
|
1999
|
-
}
|
|
2000
|
-
title="参数匹配规则"
|
|
2001
|
-
trigger="click"
|
|
2002
|
-
placement="top"
|
|
2003
|
-
>
|
|
2004
|
-
<Button
|
|
2005
|
-
type="text"
|
|
2006
|
-
icon={<QuestionCircleOutlined />}
|
|
2007
|
-
style={{
|
|
2008
|
-
color: '#999',
|
|
2009
|
-
padding: '4px'
|
|
2010
|
-
}}
|
|
2011
|
-
size="small"
|
|
2012
|
-
/>
|
|
2013
|
-
</Popover>
|
|
2014
|
-
</div>
|
|
2015
|
-
</>
|
|
2016
|
-
)}
|
|
2017
|
-
</Form.List>
|
|
1777
|
+
{/* 入参匹配规则已移至各响应内部配置,此处保留隐藏字段向后兼容旧数据 */}
|
|
1778
|
+
<Form.Item name="paramMatchers" hidden initialValue={[]}>
|
|
1779
|
+
<Input />
|
|
2018
1780
|
</Form.Item>
|
|
2019
1781
|
|
|
2020
1782
|
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
@@ -2139,538 +1901,7 @@ const InterfaceManagement = () => {
|
|
|
2139
1901
|
</Collapse>
|
|
2140
1902
|
</Form>
|
|
2141
1903
|
</div>
|
|
2142
|
-
|
|
2143
|
-
) : (
|
|
2144
|
-
<Modal
|
|
2145
|
-
title={
|
|
2146
|
-
<div style={{
|
|
2147
|
-
fontSize: '18px',
|
|
2148
|
-
fontWeight: 600,
|
|
2149
|
-
color: '#262626',
|
|
2150
|
-
display: 'flex',
|
|
2151
|
-
alignItems: 'center',
|
|
2152
|
-
gap: '8px'
|
|
2153
|
-
}}>
|
|
2154
|
-
<span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
|
|
2155
|
-
<Badge
|
|
2156
|
-
count="配置向导"
|
|
2157
|
-
style={{
|
|
2158
|
-
backgroundColor: '#f0f9ff',
|
|
2159
|
-
color: '#1677ff',
|
|
2160
|
-
fontSize: '11px',
|
|
2161
|
-
fontWeight: 'normal'
|
|
2162
|
-
}}
|
|
2163
|
-
/>
|
|
2164
|
-
</div>
|
|
2165
|
-
}
|
|
2166
|
-
open={modalVisible}
|
|
2167
|
-
onCancel={handleCancel}
|
|
2168
|
-
onOk={handleSubmit}
|
|
2169
|
-
width={1040}
|
|
2170
|
-
destroyOnClose
|
|
2171
|
-
okText={editingInterface ? '保存' : '创建'}
|
|
2172
|
-
cancelText="取消"
|
|
2173
|
-
bodyStyle={{
|
|
2174
|
-
maxHeight: '70vh',
|
|
2175
|
-
overflow: 'auto',
|
|
2176
|
-
background: '#fafafa'
|
|
2177
|
-
}}
|
|
2178
|
-
>
|
|
2179
|
-
<div style={{ padding: '0' }}>
|
|
2180
|
-
<Form
|
|
2181
|
-
form={form}
|
|
2182
|
-
layout="vertical"
|
|
2183
|
-
initialValues={{
|
|
2184
|
-
name: '',
|
|
2185
|
-
group: '',
|
|
2186
|
-
pattern: '',
|
|
2187
|
-
proxyType: 'response',
|
|
2188
|
-
statusCode: '200',
|
|
2189
|
-
contentType: 'application/json; charset=utf-8',
|
|
2190
|
-
responses: [],
|
|
2191
|
-
httpMethod: 'ALL',
|
|
2192
|
-
targetUrl: '',
|
|
2193
|
-
headerItems: [],
|
|
2194
|
-
paramMatchers: [],
|
|
2195
|
-
responseDelay: '0'
|
|
2196
|
-
}}
|
|
2197
|
-
>
|
|
2198
|
-
{/* Modal 模式下使用相同的表单内容,但去掉 Collapse 包装 */}
|
|
2199
|
-
<div style={{ padding: '24px' }}>
|
|
2200
|
-
{/* 基础信息 */}
|
|
2201
|
-
<div style={{ marginBottom: '24px' }}>
|
|
2202
|
-
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
2203
|
-
<Badge count="1" style={{ backgroundColor: '#1677ff' }} />
|
|
2204
|
-
<span>基础信息</span>
|
|
2205
|
-
</div>
|
|
2206
|
-
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
2207
|
-
<div style={{ flex: 1 }}>
|
|
2208
|
-
<Form.Item
|
|
2209
|
-
name="name"
|
|
2210
|
-
label="接口名称"
|
|
2211
|
-
rules={[{ required: true, message: '请输入接口名称' }]}
|
|
2212
|
-
>
|
|
2213
|
-
<Input placeholder="请输入接口名称" />
|
|
2214
|
-
</Form.Item>
|
|
2215
|
-
</div>
|
|
2216
|
-
<div style={{ flex: 1 }}>
|
|
2217
|
-
<Form.Item
|
|
2218
|
-
name="group"
|
|
2219
|
-
label="分组"
|
|
2220
|
-
tooltip="为接口设置分组,便于管理和批量操作"
|
|
2221
|
-
>
|
|
2222
|
-
<Select
|
|
2223
|
-
placeholder="选择或创建分组"
|
|
2224
|
-
showSearch
|
|
2225
|
-
allowClear
|
|
2226
|
-
style={{ width: '100%' }}
|
|
2227
|
-
mode="tags"
|
|
2228
|
-
maxTagCount="responsive"
|
|
2229
|
-
maxTagTextLength={10}
|
|
2230
|
-
onChange={(value) => {
|
|
2231
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
2232
|
-
const groupValue = value[0] === '' ? undefined : value[0];
|
|
2233
|
-
form.setFieldsValue({ group: groupValue });
|
|
2234
|
-
}
|
|
2235
|
-
}}
|
|
2236
|
-
onInputKeyDown={(e) => {
|
|
2237
|
-
if (e.key === 'Enter') {
|
|
2238
|
-
e.stopPropagation();
|
|
2239
|
-
}
|
|
2240
|
-
}}
|
|
2241
|
-
>
|
|
2242
|
-
{Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
|
|
2243
|
-
<Option key={group} value={group}>{group}</Option>
|
|
2244
|
-
))}
|
|
2245
|
-
</Select>
|
|
2246
|
-
</Form.Item>
|
|
2247
|
-
</div>
|
|
2248
|
-
</div>
|
|
2249
|
-
</div>
|
|
2250
|
-
|
|
2251
|
-
{/* URL配置 */}
|
|
2252
|
-
<div style={{ marginBottom: '24px' }}>
|
|
2253
|
-
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
2254
|
-
<Badge count="2" style={{ backgroundColor: '#1677ff' }} />
|
|
2255
|
-
<span>URL配置</span>
|
|
2256
|
-
</div>
|
|
2257
|
-
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
2258
|
-
<div style={{ flex: 1 }}>
|
|
2259
|
-
<Form.Item
|
|
2260
|
-
name="pattern"
|
|
2261
|
-
label="URL匹配规则"
|
|
2262
|
-
rules={[
|
|
2263
|
-
{ required: true, message: '请输入URL匹配规则' },
|
|
2264
|
-
({ getFieldValue }) => ({
|
|
2265
|
-
validator(_, value) {
|
|
2266
|
-
const proxyType = getFieldValue('proxyType');
|
|
2267
|
-
if (!value) return Promise.resolve();
|
|
2268
|
-
|
|
2269
|
-
if (proxyType === 'url_redirect') {
|
|
2270
|
-
try {
|
|
2271
|
-
new URL(value);
|
|
2272
|
-
return Promise.resolve();
|
|
2273
|
-
} catch (e) {
|
|
2274
|
-
return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL(包括http://或https://)'));
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
if (proxyType === 'redirect') {
|
|
2279
|
-
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
2280
|
-
return Promise.resolve();
|
|
2281
|
-
}
|
|
2282
|
-
return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头'));
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
if (proxyType === 'response') {
|
|
2286
|
-
if (value.startsWith('/') && value.endsWith('/')) {
|
|
2287
|
-
try {
|
|
2288
|
-
new RegExp(value.slice(1, -1));
|
|
2289
|
-
return Promise.resolve();
|
|
2290
|
-
} catch (e) {
|
|
2291
|
-
return Promise.reject(new Error('无效的正则表达式格式'));
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
if (value.includes('*')) {
|
|
2296
|
-
if (/^[a-zA-Z0-9\-_/.*]+$/.test(value)) {
|
|
2297
|
-
return Promise.resolve();
|
|
2298
|
-
}
|
|
2299
|
-
return Promise.reject(new Error('通配符URL格式不正确'));
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (/^[a-zA-Z0-9\-_/]+$/.test(value)) {
|
|
2303
|
-
return Promise.resolve();
|
|
2304
|
-
}
|
|
2305
|
-
return Promise.reject(new Error('URL路径格式不正确'));
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
return Promise.resolve();
|
|
2309
|
-
},
|
|
2310
|
-
}),
|
|
2311
|
-
]}
|
|
2312
|
-
tooltip={{
|
|
2313
|
-
title: (
|
|
2314
|
-
<>
|
|
2315
|
-
<div>不同处理方式下URL匹配规则要求:</div>
|
|
2316
|
-
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
2317
|
-
<li><b>模拟响应:</b> 支持路径格式如 /api/users,通配符如 /api/*,正则如 /\/api\/\d+/</li>
|
|
2318
|
-
<li><b>重定向:</b> 必须以http://或https://开头,例如:https://example.com/api</li>
|
|
2319
|
-
<li><b>URL重定向:</b> 必须是完整URL,包括http://或https://,例如:https://example.com/api/users</li>
|
|
2320
|
-
</ul>
|
|
2321
|
-
</>
|
|
2322
|
-
),
|
|
2323
|
-
overlayStyle: { maxWidth: '450px' }
|
|
2324
|
-
}}
|
|
2325
|
-
>
|
|
2326
|
-
<Input
|
|
2327
|
-
placeholder="根据选择的处理方式输入相应格式的URL"
|
|
2328
|
-
/>
|
|
2329
|
-
</Form.Item>
|
|
2330
|
-
</div>
|
|
2331
|
-
<div style={{ flex: 1 }}>
|
|
2332
|
-
<Form.Item
|
|
2333
|
-
name="proxyType"
|
|
2334
|
-
label="处理方式"
|
|
2335
|
-
rules={[{ required: true, message: '请选择处理方式' }]}
|
|
2336
|
-
tooltip={{
|
|
2337
|
-
title: (
|
|
2338
|
-
<>
|
|
2339
|
-
<div>不同处理方式的规则说明:</div>
|
|
2340
|
-
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
2341
|
-
<li><b>模拟响应:</b> 返回您定义的响应内容</li>
|
|
2342
|
-
<li><b>重定向:</b> 将请求重定向到其他URL,URL匹配规则必须以http://或https://开头</li>
|
|
2343
|
-
<li><b>URL重定向:</b> 完全匹配URL时重定向,URL匹配规则必须是完整URL</li>
|
|
2344
|
-
</ul>
|
|
2345
|
-
</>
|
|
2346
|
-
),
|
|
2347
|
-
overlayStyle: { maxWidth: '450px' }
|
|
2348
|
-
}}
|
|
2349
|
-
>
|
|
2350
|
-
<Select onChange={(value) => {
|
|
2351
|
-
form.setFieldsValue({ pattern: '' });
|
|
2352
|
-
const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
|
|
2353
|
-
if (patternInput) {
|
|
2354
|
-
if (value === 'response') {
|
|
2355
|
-
patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
|
|
2356
|
-
} else if (value === 'redirect') {
|
|
2357
|
-
patternInput.placeholder = "例如:https://example.com/api";
|
|
2358
|
-
} else if (value === 'url_redirect') {
|
|
2359
|
-
patternInput.placeholder = "例如:https://example.com/api/users";
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
}}>
|
|
2363
|
-
{proxyTypes.map(item => (
|
|
2364
|
-
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
2365
|
-
))}
|
|
2366
|
-
</Select>
|
|
2367
|
-
</Form.Item>
|
|
2368
|
-
</div>
|
|
2369
|
-
</div>
|
|
2370
|
-
|
|
2371
|
-
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
2372
|
-
<div style={{ flex: 1 }}>
|
|
2373
|
-
<Form.Item
|
|
2374
|
-
name="httpMethod"
|
|
2375
|
-
label="请求方法"
|
|
2376
|
-
rules={[{ required: true, message: '请选择请求方法' }]}
|
|
2377
|
-
>
|
|
2378
|
-
<Select>
|
|
2379
|
-
{httpMethods.map(item => (
|
|
2380
|
-
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
2381
|
-
))}
|
|
2382
|
-
</Select>
|
|
2383
|
-
</Form.Item>
|
|
2384
|
-
</div>
|
|
2385
|
-
<div style={{ flex: 1 }}>
|
|
2386
|
-
{/* 这里可以留空或添加其他字段 */}
|
|
2387
|
-
</div>
|
|
2388
|
-
</div>
|
|
2389
|
-
</div>
|
|
2390
|
-
|
|
2391
|
-
{/* 高级配置 */}
|
|
2392
|
-
<div style={{ marginBottom: '24px' }}>
|
|
2393
|
-
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
2394
|
-
<Badge count="3" style={{ backgroundColor: '#1677ff' }} />
|
|
2395
|
-
<span>高级配置</span>
|
|
2396
|
-
</div>
|
|
2397
|
-
|
|
2398
|
-
{/* 根据proxyType显示不同的表单项 */}
|
|
2399
|
-
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
2400
|
-
{({ getFieldValue }) => {
|
|
2401
|
-
const proxyType = getFieldValue('proxyType');
|
|
2402
|
-
|
|
2403
|
-
if (proxyType === 'response') {
|
|
2404
|
-
return (
|
|
2405
|
-
<>
|
|
2406
|
-
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
2407
|
-
<div style={{ flex: 1 }}>
|
|
2408
|
-
<Form.Item
|
|
2409
|
-
name="statusCode"
|
|
2410
|
-
label="状态码"
|
|
2411
|
-
rules={[{ required: true, message: '请选择状态码' }]}
|
|
2412
|
-
>
|
|
2413
|
-
<Select>
|
|
2414
|
-
{statusCodes.map(item => (
|
|
2415
|
-
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
2416
|
-
))}
|
|
2417
|
-
</Select>
|
|
2418
|
-
</Form.Item>
|
|
2419
|
-
</div>
|
|
2420
|
-
<div style={{ flex: 1 }}>
|
|
2421
|
-
<Form.Item
|
|
2422
|
-
name="contentType"
|
|
2423
|
-
label="内容类型"
|
|
2424
|
-
rules={[{ required: true, message: '请选择内容类型' }]}
|
|
2425
|
-
>
|
|
2426
|
-
<Select>
|
|
2427
|
-
{contentTypes.map(item => (
|
|
2428
|
-
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
2429
|
-
))}
|
|
2430
|
-
</Select>
|
|
2431
|
-
</Form.Item>
|
|
2432
|
-
</div>
|
|
2433
|
-
<div style={{ flex: 1 }}>
|
|
2434
|
-
<Form.Item
|
|
2435
|
-
name="responseDelay"
|
|
2436
|
-
label="延迟响应(毫秒)"
|
|
2437
|
-
tooltip="设置接口响应延迟时间,单位为毫秒"
|
|
2438
|
-
rules={[
|
|
2439
|
-
{
|
|
2440
|
-
pattern: /^\d*$/,
|
|
2441
|
-
message: '请输入有效的数字'
|
|
2442
|
-
},
|
|
2443
|
-
{
|
|
2444
|
-
validator: (_, value) => {
|
|
2445
|
-
if (!value || parseInt(value, 10) <= 60000) {
|
|
2446
|
-
return Promise.resolve();
|
|
2447
|
-
}
|
|
2448
|
-
return Promise.reject(new Error('延迟时间不能超过60秒(60000毫秒)'));
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
]}
|
|
2452
|
-
>
|
|
2453
|
-
<Input placeholder="例如:500" />
|
|
2454
|
-
</Form.Item>
|
|
2455
|
-
</div>
|
|
2456
|
-
</div>
|
|
2457
|
-
|
|
2458
|
-
{/* 请求入参匹配设置 */}
|
|
2459
|
-
<Form.Item
|
|
2460
|
-
label="请求入参匹配"
|
|
2461
|
-
tooltip="设置请求参数的匹配条件,只有当请求参数满足条件时才返回对应的响应。支持嵌套属性路径,如:a.b.c"
|
|
2462
|
-
>
|
|
2463
|
-
<Form.List name="paramMatchers">
|
|
2464
|
-
{(fields, { add, remove }) => (
|
|
2465
|
-
<>
|
|
2466
|
-
{fields.map(({ key, name, ...restField }) => (
|
|
2467
|
-
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
2468
|
-
<Form.Item
|
|
2469
|
-
{...restField}
|
|
2470
|
-
name={[name, 'paramPath']}
|
|
2471
|
-
rules={[
|
|
2472
|
-
{ required: true, message: '请输入参数路径' },
|
|
2473
|
-
{
|
|
2474
|
-
pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
|
|
2475
|
-
message: '请输入有效的参数路径,如:userId 或 data.user.id'
|
|
2476
|
-
}
|
|
2477
|
-
]}
|
|
2478
|
-
style={{ flex: 1, margin: 0 }}
|
|
2479
|
-
>
|
|
2480
|
-
<Input placeholder="参数路径(如:userId 或 data.user.id)" />
|
|
2481
|
-
</Form.Item>
|
|
2482
|
-
<Form.Item
|
|
2483
|
-
{...restField}
|
|
2484
|
-
name={[name, 'paramValue']}
|
|
2485
|
-
rules={[{ required: true, message: '请输入期望值' }]}
|
|
2486
|
-
style={{ flex: 1, margin: 0 }}
|
|
2487
|
-
>
|
|
2488
|
-
<Input placeholder="期望值(如:123 或 admin)" />
|
|
2489
|
-
</Form.Item>
|
|
2490
|
-
<Form.Item
|
|
2491
|
-
{...restField}
|
|
2492
|
-
name={[name, 'matchType']}
|
|
2493
|
-
style={{ width: 100, margin: 0 }}
|
|
2494
|
-
initialValue="exact"
|
|
2495
|
-
>
|
|
2496
|
-
<Select placeholder="匹配类型">
|
|
2497
|
-
<Option value="exact">精确匹配</Option>
|
|
2498
|
-
<Option value="contains">包含</Option>
|
|
2499
|
-
<Option value="regex">正则</Option>
|
|
2500
|
-
</Select>
|
|
2501
|
-
</Form.Item>
|
|
2502
|
-
<Button
|
|
2503
|
-
type="text"
|
|
2504
|
-
danger
|
|
2505
|
-
icon={<DeleteOutlined />}
|
|
2506
|
-
onClick={() => remove(name)}
|
|
2507
|
-
/>
|
|
2508
|
-
</div>
|
|
2509
|
-
))}
|
|
2510
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
|
|
2511
|
-
<Button
|
|
2512
|
-
type="dashed"
|
|
2513
|
-
onClick={() => add()}
|
|
2514
|
-
icon={<PlusOutlined />}
|
|
2515
|
-
style={{ flex: 1 }}
|
|
2516
|
-
>
|
|
2517
|
-
添加参数匹配规则
|
|
2518
|
-
</Button>
|
|
2519
|
-
<Popover
|
|
2520
|
-
content={
|
|
2521
|
-
<div style={{ maxWidth: '300px' }}>
|
|
2522
|
-
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
|
|
2523
|
-
<div style={{ lineHeight: '1.6' }}>
|
|
2524
|
-
<div>• <strong>精确匹配</strong>:参数值完全相等</div>
|
|
2525
|
-
<div>• <strong>包含</strong>:参数值包含指定内容</div>
|
|
2526
|
-
<div>• <strong>正则</strong>:参数值符合正则表达式</div>
|
|
2527
|
-
<div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
|
|
2528
|
-
</div>
|
|
2529
|
-
</div>
|
|
2530
|
-
}
|
|
2531
|
-
title="参数匹配规则"
|
|
2532
|
-
trigger="click"
|
|
2533
|
-
placement="top"
|
|
2534
|
-
>
|
|
2535
|
-
<Button
|
|
2536
|
-
type="text"
|
|
2537
|
-
icon={<QuestionCircleOutlined />}
|
|
2538
|
-
style={{
|
|
2539
|
-
color: '#999',
|
|
2540
|
-
padding: '4px'
|
|
2541
|
-
}}
|
|
2542
|
-
size="small"
|
|
2543
|
-
/>
|
|
2544
|
-
</Popover>
|
|
2545
|
-
</div>
|
|
2546
|
-
</>
|
|
2547
|
-
)}
|
|
2548
|
-
</Form.List>
|
|
2549
|
-
</Form.Item>
|
|
2550
|
-
|
|
2551
|
-
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
2552
|
-
<Form.Item
|
|
2553
|
-
name="responses"
|
|
2554
|
-
initialValue={[]}
|
|
2555
|
-
hidden
|
|
2556
|
-
>
|
|
2557
|
-
<Input />
|
|
2558
|
-
</Form.Item>
|
|
2559
|
-
<Form.Item name="activeResponseId" hidden>
|
|
2560
|
-
<Input />
|
|
2561
|
-
</Form.Item>
|
|
2562
|
-
|
|
2563
|
-
{/* 使用新的合并组件 */}
|
|
2564
|
-
<Form.Item noStyle shouldUpdate>
|
|
2565
|
-
{({ getFieldValue }) => {
|
|
2566
|
-
let responses = getFieldValue('responses') || [];
|
|
2567
|
-
const activeResponseId = getFieldValue('activeResponseId');
|
|
2568
|
-
|
|
2569
|
-
if (!Array.isArray(responses)) {
|
|
2570
|
-
try {
|
|
2571
|
-
if (typeof responses === 'string') {
|
|
2572
|
-
responses = JSON.parse(responses);
|
|
2573
|
-
}
|
|
2574
|
-
} catch (e) {
|
|
2575
|
-
responses = [];
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
return (
|
|
2580
|
-
<ResponseContentEditor
|
|
2581
|
-
form={form}
|
|
2582
|
-
responses={responses}
|
|
2583
|
-
activeResponseId={activeResponseId}
|
|
2584
|
-
onPreview={handlePreview}
|
|
2585
|
-
/>
|
|
2586
|
-
);
|
|
2587
|
-
}}
|
|
2588
|
-
</Form.Item>
|
|
2589
|
-
</>
|
|
2590
|
-
);
|
|
2591
|
-
} else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
2592
|
-
return (
|
|
2593
|
-
<>
|
|
2594
|
-
<Form.Item
|
|
2595
|
-
name="targetUrl"
|
|
2596
|
-
label="重定向目标URL"
|
|
2597
|
-
rules={[
|
|
2598
|
-
{ required: true, message: '请输入重定向目标URL' },
|
|
2599
|
-
{
|
|
2600
|
-
validator(_, value) {
|
|
2601
|
-
if (!value) return Promise.resolve();
|
|
2602
|
-
try {
|
|
2603
|
-
new URL(value);
|
|
2604
|
-
return Promise.resolve();
|
|
2605
|
-
} catch (e) {
|
|
2606
|
-
return Promise.reject(new Error('请输入有效的URL格式(包括http://或https://)'));
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
]}
|
|
2611
|
-
tooltip="重定向的目标URL,必须是完整的URL格式"
|
|
2612
|
-
>
|
|
2613
|
-
<Input
|
|
2614
|
-
placeholder="例如:https://api.example.com/users"
|
|
2615
|
-
/>
|
|
2616
|
-
</Form.Item>
|
|
2617
|
-
|
|
2618
|
-
{/* 自定义请求头设置 */}
|
|
2619
|
-
<Form.Item label="自定义请求头">
|
|
2620
|
-
<Form.List name="headerItems">
|
|
2621
|
-
{(fields, { add, remove }) => (
|
|
2622
|
-
<>
|
|
2623
|
-
{fields.map(({ key, name, ...restField }) => (
|
|
2624
|
-
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
2625
|
-
<Form.Item
|
|
2626
|
-
{...restField}
|
|
2627
|
-
name={[name, 'headerName']}
|
|
2628
|
-
rules={[{ required: true, message: '请输入请求头名称' }]}
|
|
2629
|
-
style={{ flex: 1, margin: 0 }}
|
|
2630
|
-
>
|
|
2631
|
-
<Input placeholder="请求头名称(如:Authorization)" />
|
|
2632
|
-
</Form.Item>
|
|
2633
|
-
<Form.Item
|
|
2634
|
-
{...restField}
|
|
2635
|
-
name={[name, 'headerValue']}
|
|
2636
|
-
rules={[{ required: true, message: '请输入请求头值' }]}
|
|
2637
|
-
style={{ flex: 1, margin: 0 }}
|
|
2638
|
-
>
|
|
2639
|
-
<Input placeholder="请求头值(如:Bearer token123)" />
|
|
2640
|
-
</Form.Item>
|
|
2641
|
-
<Button
|
|
2642
|
-
type="text"
|
|
2643
|
-
danger
|
|
2644
|
-
icon={<DeleteOutlined />}
|
|
2645
|
-
onClick={() => remove(name)}
|
|
2646
|
-
/>
|
|
2647
|
-
</div>
|
|
2648
|
-
))}
|
|
2649
|
-
<Button
|
|
2650
|
-
type="dashed"
|
|
2651
|
-
onClick={() => add()}
|
|
2652
|
-
icon={<PlusOutlined />}
|
|
2653
|
-
style={{ width: '100%', marginTop: '8px' }}
|
|
2654
|
-
>
|
|
2655
|
-
添加请求头
|
|
2656
|
-
</Button>
|
|
2657
|
-
</>
|
|
2658
|
-
)}
|
|
2659
|
-
</Form.List>
|
|
2660
|
-
</Form.Item>
|
|
2661
|
-
</>
|
|
2662
|
-
);
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
return null;
|
|
2666
|
-
}}
|
|
2667
|
-
</Form.Item>
|
|
2668
|
-
</div>
|
|
2669
|
-
</div>
|
|
2670
|
-
</Form>
|
|
2671
|
-
</div>
|
|
2672
|
-
</Modal>
|
|
2673
|
-
)}
|
|
1904
|
+
</Modal>
|
|
2674
1905
|
|
|
2675
1906
|
{/* 使用拆分后的预览模态框组件 */}
|
|
2676
1907
|
<PreviewModal
|
|
@@ -2679,26 +1910,15 @@ const InterfaceManagement = () => {
|
|
|
2679
1910
|
previewContent={previewContent}
|
|
2680
1911
|
/>
|
|
2681
1912
|
|
|
2682
|
-
{/*
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
/>
|
|
2692
|
-
) : (
|
|
2693
|
-
<DataManagementModal
|
|
2694
|
-
visible={dataModalVisible}
|
|
2695
|
-
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
2696
|
-
interfaceItem={dataModalRecord}
|
|
2697
|
-
onSaved={() => {
|
|
2698
|
-
fetchInterfaces();
|
|
2699
|
-
}}
|
|
2700
|
-
/>
|
|
2701
|
-
)}
|
|
1913
|
+
{/* 数据管理弹窗 */}
|
|
1914
|
+
<DataManagementModal
|
|
1915
|
+
visible={dataModalVisible}
|
|
1916
|
+
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
1917
|
+
interfaceItem={dataModalRecord}
|
|
1918
|
+
onSaved={() => {
|
|
1919
|
+
fetchInterfaces();
|
|
1920
|
+
}}
|
|
1921
|
+
/>
|
|
2702
1922
|
</div>
|
|
2703
1923
|
</AppLayout>
|
|
2704
1924
|
);
|