whistle.mock-plugins 1.3.1 → 1.4.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/VERSION.md +29 -0
  2. package/app/cgi-bin/interfaces.js +2 -0
  3. package/app/cgi-bin/settings.js +1 -0
  4. package/app/cgi-bin/stats.js +22 -12
  5. package/app/cgi-bin/webdav.js +56 -0
  6. package/app/public/index.html +1 -1
  7. package/app/public/js/11.index.js +2 -0
  8. package/app/public/js/11.index.js.LICENSE.txt +5 -0
  9. package/app/public/js/704.index.js +2 -0
  10. package/app/public/js/704.index.js.LICENSE.txt +1 -0
  11. package/app/public/js/798.index.js +2 -0
  12. package/app/public/js/798.index.js.LICENSE.txt +53 -0
  13. package/app/public/js/807.index.js +2 -0
  14. package/app/public/js/807.index.js.LICENSE.txt +1 -0
  15. package/app/public/js/968.index.js +2 -0
  16. package/app/public/js/968.index.js.LICENSE.txt +12 -0
  17. package/app/public/js/index.js +1 -1
  18. package/app/public/js/index.js.LICENSE.txt +0 -173
  19. package/lib/routes/settings.js +1 -0
  20. package/lib/webdav-sync.js +806 -0
  21. package/package.json +9 -2
  22. package/API.md +0 -247
  23. package/COLUMN_RESIZE_IMPLEMENTATION.md +0 -162
  24. package/FEATURE_ENHANCEMENT.md +0 -157
  25. package/TEST_GUIDE.md +0 -156
  26. package/data/features.json +0 -1
  27. package/data/interfaces.json +0 -1
  28. package/index.html +0 -52
  29. package/initialize.js +0 -32
  30. package/public/bundle.js +0 -2
  31. package/public/bundle.js.LICENSE.txt +0 -38
  32. package/public/index.html +0 -1
  33. package/public/plugin.mock-plugin.html +0 -14
  34. package/rules.txt +0 -22
  35. package/ui/src/App.css +0 -173
  36. package/ui/src/components/AppLayout.js +0 -407
  37. package/ui/src/components/FeatureCard.js +0 -58
  38. package/ui/src/components/KeyboardShortcutsHelp.js +0 -58
  39. package/ui/src/components/Layout.js +0 -94
  40. package/ui/src/components/MainNav.js +0 -42
  41. package/ui/src/components/PageContainer.js +0 -16
  42. package/ui/src/components/VersionModal.js +0 -99
  43. package/ui/src/components/interface-management/DataManagementDrawer.js +0 -115
  44. package/ui/src/components/interface-management/DataManagementModal.js +0 -110
  45. package/ui/src/components/interface-management/InterfaceTestModal.js +0 -313
  46. package/ui/src/components/interface-management/PreviewModal.js +0 -48
  47. package/ui/src/components/interface-management/README.md +0 -184
  48. package/ui/src/components/interface-management/RealResponseRecorder.js +0 -165
  49. package/ui/src/components/interface-management/ResponseContentEditor.js +0 -645
  50. package/ui/src/components/interface-management/constants.js +0 -36
  51. package/ui/src/components/interface-management/index.js +0 -7
  52. package/ui/src/components/interface-management/utils.js +0 -103
  53. package/ui/src/hooks/useKeyboardShortcuts.js +0 -99
  54. package/ui/src/index.html +0 -14
  55. package/ui/src/index.js +0 -50
  56. package/ui/src/pages/InterfaceManagementV2.js +0 -2351
  57. package/ui/src/pages/MockDataV2.js +0 -1089
  58. package/ui/src/pages/Settings.js +0 -299
  59. package/ui/src/pages/Sniffer.js +0 -521
  60. package/ui/src/pages/Stats.js +0 -288
  61. package/ui/src/services/del_api.js +0 -192
  62. package/ui/src/styles/app-layout.css +0 -373
  63. package/ui/src/styles/cyber-theme.css +0 -1234
  64. package/ui/src/styles/earth-theme.css +0 -618
  65. package/ui/src/styles/global.css +0 -224
  66. package/ui/src/styles/interface-management.css +0 -1321
  67. package/ui/src/styles/mock-data.css +0 -1185
  68. package/ui/src/styles/settings.css +0 -252
  69. package/ui/src/styles/version-modal.css +0 -116
  70. package/webpack.config.js +0 -54
@@ -1,2351 +0,0 @@
1
- /**
2
- * InterfaceManagementV2.js - 接口管理页面
3
- */
4
-
5
- import React, { useState, useEffect, useRef } from 'react';
6
- import { useParams, useHistory } from 'react-router-dom';
7
- import AppLayout from '../components/AppLayout';
8
- import {
9
- Table, Button, Modal, Form, Input, Select, message, Switch,
10
- Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col,
11
- Popover, Checkbox, Tag, Input as AntInput, Radio, Drawer, Dropdown, Menu, Collapse
12
- } from 'antd';
13
- import {
14
- PlusOutlined, EditOutlined, DeleteOutlined,
15
- FileTextOutlined, PlusCircleOutlined, SettingOutlined,
16
- SearchOutlined, FilterOutlined, ReloadOutlined, DownOutlined, QuestionCircleOutlined, CopyOutlined
17
- } from '@ant-design/icons';
18
- import '../styles/interface-management.css';
19
- import axios from 'axios';
20
-
21
- // 导入拆分后的组件
22
- import {
23
- ResponseContentEditor,
24
- PreviewModal,
25
- contentTypes,
26
- proxyTypes,
27
- statusCodes,
28
- httpMethods,
29
- refreshCacheAfterUpdate,
30
- formatResponseContent,
31
- generateResponseId
32
- } from '../components/interface-management';
33
- import DataManagementModal from '../components/interface-management/DataManagementModal';
34
- import { DataManagementDrawer } from '../components/interface-management';
35
-
36
- const { Option } = Select;
37
- const { Search } = AntInput;
38
-
39
- // 列配置数据结构
40
- const COLUMN_CONFIG = [
41
- { key: 'active', title: '状态', required: true },
42
- { key: 'name', title: '名称', required: true },
43
- { key: 'group', title: '分组', required: false },
44
- { key: 'urlPattern', title: 'URL匹配规则', required: false },
45
- { key: 'proxyType', title: '处理方式', required: false },
46
- { key: 'currentResponse', title: '当前响应', required: false },
47
- { key: 'responseDelay', title: '延迟(毫秒)', required: false },
48
- { key: 'httpStatus', title: '状态码', required: false },
49
- { key: 'contentType', title: '内容类型', required: false },
50
- { key: 'targetUrl', title: '目标URL', required: false },
51
- { key: 'customHeaders', title: '自定义头', required: false },
52
- { key: 'paramMatchers', title: '参数匹配', required: false },
53
- { key: 'httpMethod', title: '请求方法', required: false },
54
- { key: 'action', title: '操作', required: true }
55
- ];
56
-
57
- const InterfaceManagement = () => {
58
- const { featureId } = useParams();
59
- const history = useHistory();
60
-
61
- // 基础状态
62
- const [features, setFeatures] = useState([]);
63
- const [interfaces, setInterfaces] = useState([]);
64
- const [loading, setLoading] = useState(false);
65
- const [featuresLoading, setFeaturesLoading] = useState(false);
66
- const [interfacesLoading, setInterfacesLoading] = useState(false);
67
- const [selectedFeatureId, setSelectedFeatureId] = useState(null);
68
-
69
- // 分组状态
70
- const [groups, setGroups] = useState([]);
71
- const [selectedGroup, setSelectedGroup] = useState(null);
72
- const [groupActionLoading, setGroupActionLoading] = useState(false);
73
-
74
- // 模态框状态
75
- const [modalVisible, setModalVisible] = useState(false);
76
- const [previewVisible, setPreviewVisible] = useState(false);
77
- const [isDuplicateMode, setIsDuplicateMode] = useState(false);
78
- const [dataModalVisible, setDataModalVisible] = useState(false);
79
- const [dataModalRecord, setDataModalRecord] = useState(null);
80
-
81
- // 列配置状态
82
- const [columnConfigVisible, setColumnConfigVisible] = useState(false);
83
-
84
- // 编辑状态
85
- const [editingInterface, setEditingInterface] = useState(null);
86
- const [previewContent, setPreviewContent] = useState(null);
87
- const [currentResponseId, setCurrentResponseId] = useState(null);
88
-
89
- // 搜索状态
90
- const searchRef = useRef(null);
91
- const [searchValue, setSearchValue] = useState('');
92
- const [selectedRowKeys, setSelectedRowKeys] = useState([]);
93
- const [drawerVisible, setDrawerVisible] = useState(false);
94
-
95
- // 数据管理显示模式(modal / drawer)
96
- const [displayMode, setDisplayMode] = useState('modal');
97
-
98
- // 接口表单显示模式(modal / drawer)
99
- const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
100
-
101
- // 接口入参匹配显示开关
102
- const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
103
-
104
- // 响应入参匹配显示开关
105
- const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
106
-
107
- // 表单实例
108
- const [form] = Form.useForm();
109
-
110
- // 表格配置状态(支持缓存)
111
- const [tableConfig, setTableConfig] = useState(() => {
112
- const cached = localStorage.getItem('interface-table-config');
113
- const defaultVisibleColumns = COLUMN_CONFIG.map(col => col.key);
114
- return cached ? JSON.parse(cached) : {
115
- sortOrder: null,
116
- sortField: null,
117
- pageSize: 10,
118
- current: 1,
119
- visibleColumns: defaultVisibleColumns
120
- };
121
- });
122
-
123
- // 保存表格配置到localStorage
124
- const saveTableConfig = (config) => {
125
- const newConfig = { ...tableConfig, ...config };
126
- setTableConfig(newConfig);
127
- localStorage.setItem('interface-table-config', JSON.stringify(newConfig));
128
- };
129
-
130
- useEffect(() => {
131
- fetchFeatures();
132
- }, []);
133
-
134
- useEffect(() => {
135
- if (selectedFeatureId) {
136
- fetchInterfaces();
137
- }
138
- }, [selectedFeatureId]);
139
-
140
- // 键盘快捷键监听
141
- useEffect(() => {
142
- const handleSave = () => {
143
- if (modalVisible) {
144
- handleSubmit();
145
- }
146
- };
147
- const handleNew = () => {
148
- handleAddInterface();
149
- };
150
- const handleFocusSearch = () => {
151
- searchRef.current?.focus();
152
- };
153
- window.addEventListener('shortcut-save', handleSave);
154
- window.addEventListener('shortcut-new-interface', handleNew);
155
- window.addEventListener('shortcut-focus-search', handleFocusSearch);
156
- return () => {
157
- window.removeEventListener('shortcut-save', handleSave);
158
- window.removeEventListener('shortcut-new-interface', handleNew);
159
- window.removeEventListener('shortcut-focus-search', handleFocusSearch);
160
- };
161
- }, [modalVisible]);
162
-
163
- // 监听数据管理显示模式变化
164
- useEffect(() => {
165
- const savedMode = localStorage.getItem('dataManagementDisplayMode');
166
- if (savedMode) {
167
- setDisplayMode(savedMode);
168
- }
169
-
170
- const handleStorageChange = () => {
171
- const newMode = localStorage.getItem('dataManagementDisplayMode');
172
- if (newMode) {
173
- setDisplayMode(newMode);
174
- }
175
- };
176
-
177
- window.addEventListener('storage', handleStorageChange);
178
- return () => {
179
- window.removeEventListener('storage', handleStorageChange);
180
- };
181
- }, []);
182
-
183
- // 监听接口表单显示模式变化
184
- useEffect(() => {
185
- const savedMode = localStorage.getItem('interfaceFormDisplayMode');
186
- if (savedMode) {
187
- setInterfaceFormDisplayMode(savedMode);
188
- }
189
-
190
- const handleStorageChange = () => {
191
- const newMode = localStorage.getItem('interfaceFormDisplayMode');
192
- if (newMode) {
193
- setInterfaceFormDisplayMode(newMode);
194
- }
195
- };
196
-
197
- window.addEventListener('storage', handleStorageChange);
198
- return () => {
199
- window.removeEventListener('storage', handleStorageChange);
200
- };
201
- }, []);
202
-
203
- // 监听接口入参匹配 / 响应入参匹配开关变化
204
- useEffect(() => {
205
- const readSwitches = () => {
206
- const v1 = localStorage.getItem('interfaceParamMatcherEnabled');
207
- setInterfaceParamMatcherEnabled(v1 === null ? true : v1 !== 'false');
208
- const v2 = localStorage.getItem('responseParamMatcherEnabled');
209
- setResponseParamMatcherEnabled(v2 === null ? true : v2 !== 'false');
210
- };
211
- readSwitches();
212
- window.addEventListener('storage', readSwitches);
213
- return () => window.removeEventListener('storage', readSwitches);
214
- }, []);
215
-
216
- // 获取当前选中的功能模块
217
- const selectedFeature = features.find(f => f.id === selectedFeatureId);
218
-
219
- const fetchFeatures = async () => {
220
- try {
221
- setFeaturesLoading(true);
222
- const response = await axios.get('/cgi-bin/features');
223
- if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
224
- const featureList = response.data.data;
225
- setFeatures(featureList);
226
- // 优先级:URL 参数 > localStorage 上次选择 > 第一个功能模块
227
- const lastSelected = localStorage.getItem('selectedFeatureId');
228
- const validLastSelected = lastSelected && featureList.find(f => f.id === lastSelected) ? lastSelected : null;
229
- const initialFeatureId = featureId || validLastSelected || featureList[0]?.id;
230
- if (initialFeatureId) {
231
- setSelectedFeatureId(initialFeatureId);
232
- }
233
- } else {
234
- setFeatures([]);
235
- message.warning('获取功能模块数据格式不正确');
236
- }
237
- } catch (error) {
238
- console.error('获取功能模块失败:', error);
239
- message.error(error.response?.data?.message || '获取功能模块失败');
240
- setFeatures([]);
241
- } finally {
242
- setFeaturesLoading(false);
243
- }
244
- };
245
-
246
- const fetchInterfaces = async () => {
247
- if (!selectedFeatureId) {
248
- return;
249
- }
250
- try {
251
- setInterfacesLoading(true);
252
- const response = await axios.get(`/cgi-bin/interfaces?featureId=${selectedFeatureId}`);
253
-
254
- console.log('获取接口列表响应:', response.data);
255
-
256
- if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
257
- // 确保每个接口的响应数据格式正确
258
- const processedInterfaces = response.data.data.map(item => {
259
- console.log(`处理接口 ${item.name}:`, {
260
- hasResponses: !!item.responses,
261
- responsesLength: item.responses ? item.responses.length : 0,
262
- activeResponseId: item.activeResponseId
263
- });
264
-
265
- // 确保 responses 是数组
266
- if (!Array.isArray(item.responses)) {
267
- item.responses = [];
268
- }
269
-
270
- // 确保每个响应都有必要的字段
271
- item.responses = item.responses.map((resp, index) => ({
272
- id: resp.id || `resp-${index}-${Date.now()}`,
273
- name: resp.name || `响应 ${index + 1}`,
274
- description: resp.description || '',
275
- content: resp.content || '{}',
276
- paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
277
- }));
278
-
279
- return item;
280
- });
281
-
282
- setInterfaces(processedInterfaces);
283
- console.log('处理后的接口列表:', processedInterfaces);
284
- console.log('接口分组信息:', processedInterfaces.map(item => ({ id: item.id, name: item.name, group: item.group })));
285
-
286
- // 提取并更新分组列表
287
- updateGroups(processedInterfaces);
288
- } else {
289
- console.warn('接口数据格式不正确:', response.data);
290
- setInterfaces([]);
291
- message.warning('获取接口配置数据格式不正确');
292
- }
293
- } catch (error) {
294
- console.error('获取接口配置失败:', error);
295
- message.error(error.response?.data?.message || '获取接口配置失败');
296
- setInterfaces([]);
297
- } finally {
298
- setInterfacesLoading(false);
299
- }
300
- };
301
-
302
- // 提取并更新分组列表
303
- const updateGroups = (interfaces) => {
304
- // 从接口列表中提取所有分组
305
- const groupSet = new Set();
306
- interfaces.forEach(item => {
307
- if (item.group) {
308
- groupSet.add(item.group);
309
- }
310
- });
311
-
312
- // 转换为数组并排序
313
- const groupArray = Array.from(groupSet).sort();
314
- setGroups(groupArray);
315
-
316
- // 如果当前选中的分组不在列表中,重置选中的分组
317
- if (selectedGroup && !groupArray.includes(selectedGroup)) {
318
- setSelectedGroup(null);
319
- }
320
- };
321
-
322
- // 根据分组和搜索关键词筛选接口
323
- const getFilteredInterfaces = () => {
324
- // 首先按功能模块筛选
325
- let featureFiltered = interfaces.filter(item =>
326
- !selectedFeatureId || item.featureId === selectedFeatureId
327
- );
328
-
329
- // 然后按分组筛选
330
- if (selectedGroup) {
331
- featureFiltered = featureFiltered.filter(item => item.group === selectedGroup);
332
- }
333
-
334
- // 最后按搜索关键词筛选
335
- if (searchValue) {
336
- const searchLower = searchValue.toLowerCase();
337
- return featureFiltered.filter(item =>
338
- (item.name && item.name.toLowerCase().includes(searchLower)) ||
339
- (item.urlPattern && item.urlPattern.toLowerCase().includes(searchLower))
340
- );
341
- }
342
-
343
- return featureFiltered;
344
- };
345
-
346
- const handleAddInterface = () => {
347
- if (!selectedFeatureId) {
348
- message.warning('请先选择一个功能模块');
349
- return;
350
- }
351
- form.resetFields();
352
- setIsDuplicateMode(false);
353
-
354
- // 创建默认响应
355
- const defaultResponseId = generateResponseId();
356
- const defaultResponses = [{
357
- id: defaultResponseId,
358
- name: '默认响应',
359
- description: '',
360
- content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
361
- }];
362
-
363
- console.log('创建新接口,初始化响应数据:', defaultResponses);
364
-
365
- form.setFieldsValue({
366
- proxyType: 'response',
367
- statusCode: '200',
368
- contentType: 'application/json; charset=utf-8',
369
- responses: defaultResponses,
370
- activeResponseId: defaultResponseId,
371
- httpMethod: 'ALL',
372
- headerItems: [], // 初始化为空数组
373
- paramMatchers: [], // 初始化为空数组
374
- group: undefined // 确保分组字段为undefined,而不是空字符串
375
- });
376
-
377
- setCurrentResponseId(defaultResponseId);
378
- setEditingInterface(null);
379
- setModalVisible(true);
380
- };
381
-
382
- const handleEditInterface = (record) => {
383
- if (!record) {
384
- message.warning('接口数据不完整,无法编辑');
385
- return;
386
- }
387
-
388
- console.log('编辑接口:', record);
389
-
390
- // 重置表单
391
- form.resetFields();
392
- setIsDuplicateMode(false);
393
-
394
- // 处理响应数据
395
- let responses = [];
396
- let activeResponseId = '';
397
-
398
- if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
399
- responses = record.responses;
400
- activeResponseId = record.activeResponseId || responses[0].id;
401
- } else if (record.responseContent) {
402
- // 兼容旧数据格式,创建默认响应
403
- const defaultResponseId = generateResponseId();
404
- responses = [{
405
- id: defaultResponseId,
406
- name: '默认响应',
407
- description: '',
408
- content: record.responseContent
409
- }];
410
- activeResponseId = defaultResponseId;
411
- }
412
-
413
- // 处理自定义请求头
414
- let headerItems = [];
415
- if (record.headers && typeof record.headers === 'object') {
416
- headerItems = Object.entries(record.headers).map(([headerName, headerValue]) => ({
417
- headerName,
418
- headerValue
419
- }));
420
- }
421
-
422
- // 处理分组值,确保它是字符串而不是数组
423
- let groupValue = record.group;
424
- if (Array.isArray(groupValue)) {
425
- groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
426
- }
427
-
428
- // 设置表单值
429
- form.setFieldsValue({
430
- name: record.name,
431
- group: groupValue || undefined, // 使用undefined而不是空字符串
432
- pattern: record.urlPattern,
433
- proxyType: record.proxyType || 'response',
434
- statusCode: (record.httpStatus || record.statusCode || 200).toString(),
435
- contentType: record.contentType || 'application/json; charset=utf-8',
436
- responses,
437
- activeResponseId,
438
- httpMethod: record.httpMethod || record.method || 'ALL',
439
- targetUrl: record.targetUrl || '',
440
- headerItems,
441
- paramMatchers: record.paramMatchers || [],
442
- responseDelay: record.responseDelay ? record.responseDelay.toString() : '0'
443
- });
444
-
445
- console.log('编辑接口时设置的分组值:', groupValue);
446
-
447
- setCurrentResponseId(activeResponseId);
448
- setEditingInterface(record);
449
- setModalVisible(true);
450
- };
451
-
452
- // 复制新增接口
453
- const handleDuplicate = (record) => {
454
- if (!selectedFeatureId) {
455
- message.warning('请先选择一个功能模块');
456
- return;
457
- }
458
- if (!record) {
459
- message.warning('接口数据不完整,无法复制');
460
- return;
461
- }
462
-
463
- form.resetFields();
464
-
465
- // 深拷贝源数据
466
- const source = JSON.parse(JSON.stringify(record));
467
-
468
- // 处理响应数据
469
- let responses = [];
470
- let activeResponseId = '';
471
-
472
- if (Array.isArray(source.responses) && source.responses.length > 0) {
473
- // 为每条响应生成新的ID,并重映射activeResponseId
474
- const idMap = new Map();
475
- responses = source.responses.map((resp) => {
476
- const newId = generateResponseId();
477
- idMap.set(resp.id, newId);
478
- return {
479
- id: newId,
480
- name: resp.name || '未命名响应',
481
- description: resp.description || '',
482
- content: resp.content || '{}',
483
- paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers.map(m => ({ ...m })) : []
484
- };
485
- });
486
- const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
487
- activeResponseId = mapped || '';
488
- } else if (source.responseContent) {
489
- // 兼容旧数据格式
490
- const defaultResponseId = generateResponseId();
491
- responses = [{
492
- id: defaultResponseId,
493
- name: '默认响应',
494
- description: '',
495
- content: source.responseContent
496
- }];
497
- activeResponseId = defaultResponseId;
498
- }
499
-
500
- // 处理自定义请求头 -> 表单需要的数组结构
501
- let headerItems = [];
502
- if (source.headers && typeof source.headers === 'object') {
503
- headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
504
- headerName,
505
- headerValue
506
- }));
507
- }
508
-
509
- // 分组值保证为字符串
510
- let groupValue = source.group;
511
- if (Array.isArray(groupValue)) {
512
- groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
513
- }
514
-
515
- // 设定表单初值
516
- form.setFieldsValue({
517
- name: `${source.name || '未命名接口'} - 副本`,
518
- group: groupValue || undefined,
519
- pattern: source.urlPattern,
520
- proxyType: source.proxyType || 'response',
521
- statusCode: (source.httpStatus || source.statusCode || 200).toString(),
522
- contentType: source.contentType || 'application/json; charset=utf-8',
523
- responses,
524
- activeResponseId,
525
- httpMethod: source.httpMethod || source.method || 'ALL',
526
- targetUrl: source.targetUrl || '',
527
- headerItems,
528
- paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
529
- responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
530
- });
531
-
532
- setCurrentResponseId(activeResponseId || null);
533
- setEditingInterface(null); // 新增模式
534
- setIsDuplicateMode(true);
535
- setModalVisible(true);
536
- };
537
-
538
- const handleDeleteInterface = async (id) => {
539
- try {
540
- const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
541
- if (response.data && response.data.code === 0) {
542
- message.success('接口删除成功');
543
- fetchInterfaces();
544
-
545
- // 刷新规则缓存
546
- refreshCacheAfterUpdate();
547
- } else {
548
- throw new Error(response.data?.message || '接口删除失败');
549
- }
550
- } catch (error) {
551
- console.error('接口删除失败:', error);
552
- message.error(error.response?.data?.message || error.message || '接口删除失败');
553
- }
554
- };
555
-
556
- const handleToggleActive = async (id, currentActive) => {
557
- try {
558
- const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
559
- active: !currentActive
560
- });
561
- if (response.data && response.data.code === 0) {
562
- message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
563
- fetchInterfaces();
564
-
565
- // 刷新规则缓存
566
- refreshCacheAfterUpdate();
567
- } else {
568
- throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
569
- }
570
- } catch (error) {
571
- console.error(`接口${!currentActive ? '启用' : '禁用'}失败:`, error);
572
- message.error(error.response?.data?.message || error.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
573
- }
574
- };
575
-
576
- const handleSubmit = async () => {
577
- try {
578
- // 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
579
- await form.validateFields();
580
- const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
581
- console.log('表单提交数据:', values);
582
-
583
- // 处理自定义请求头
584
- let headers = {};
585
- if (values.headerItems && Array.isArray(values.headerItems)) {
586
- values.headerItems.forEach(item => {
587
- if (item && item.headerName) {
588
- headers[item.headerName] = item.headerValue;
589
- }
590
- });
591
- }
592
-
593
- // 处理分组值,确保它是字符串而不是数组
594
- let groupValue = values.group;
595
- if (Array.isArray(groupValue)) {
596
- groupValue = groupValue.length > 0 ? groupValue[0] : '';
597
- }
598
-
599
- // 处理 responses,清理每个响应中的空 paramMatchers 规则
600
- const cleanedResponses = (values.responses || (editingInterface ? editingInterface.responses : [])).map(resp => ({
601
- ...resp,
602
- paramMatchers: Array.isArray(resp.paramMatchers)
603
- ? resp.paramMatchers.filter(m => m && m.paramPath && m.paramValue)
604
- : []
605
- }));
606
-
607
- // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
608
- const interfaceData = {
609
- name: values.name || (editingInterface ? editingInterface.name : ''),
610
- group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
611
- urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
612
- proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
613
- featureId: selectedFeatureId,
614
- responses: cleanedResponses,
615
- activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
616
- httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
617
- contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
618
- responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
619
- httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
620
- active: true
621
- };
622
-
623
- console.log('提交的分组值:', groupValue);
624
-
625
- // 根据代理类型添加不同字段
626
- if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
627
- interfaceData.targetUrl = values.targetUrl;
628
- interfaceData.headers = headers;
629
- }
630
-
631
- // 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
632
- const filteredParamMatchers = Array.isArray(values.paramMatchers)
633
- ? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
634
- : [];
635
- interfaceData.paramMatchers = filteredParamMatchers;
636
-
637
- console.log('提交接口数据:', interfaceData);
638
-
639
- if (editingInterface) {
640
- // 更新接口
641
- const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
642
- if (response.data && response.data.code === 0) {
643
- message.success('接口更新成功');
644
- setModalVisible(false);
645
- fetchInterfaces();
646
-
647
- // 刷新规则缓存
648
- refreshCacheAfterUpdate();
649
- } else {
650
- throw new Error(response.data?.message || '接口更新失败');
651
- }
652
- } else {
653
- // 创建接口
654
- const response = await axios.post('/cgi-bin/interfaces', interfaceData);
655
- if (response.data && response.data.code === 0) {
656
- message.success('接口创建成功');
657
- console.log('接口创建成功,返回数据:', response.data);
658
- console.log('返回的接口数据中的分组信息:', response.data.data?.group);
659
- setModalVisible(false);
660
- fetchInterfaces();
661
-
662
- // 刷新规则缓存
663
- refreshCacheAfterUpdate();
664
- } else {
665
- throw new Error(response.data?.message || '接口创建失败');
666
- }
667
- }
668
- } catch (error) {
669
- console.error('表单提交失败:', error);
670
- message.error(error.response?.data?.message || error.message || '操作失败');
671
- }
672
- };
673
-
674
- const handleCancel = () => {
675
- setModalVisible(false);
676
- setIsDuplicateMode(false);
677
- };
678
-
679
- const handleSelectFeature = (featureId) => {
680
- setSelectedFeatureId(featureId);
681
- localStorage.setItem('selectedFeatureId', featureId);
682
- // 切换功能模块时重置分页到第一页,但保留其他配置
683
- saveTableConfig({ current: 1 });
684
- };
685
-
686
- // 预览响应内容
687
- const handlePreview = () => {
688
- // 获取当前选中的响应内容
689
- const responses = form.getFieldValue('responses') || [];
690
- const activeResponseId = form.getFieldValue('activeResponseId');
691
- const activeResponse = responses.find(r => r.id === activeResponseId);
692
-
693
- if (!activeResponse) {
694
- message.error('未找到有效的响应内容');
695
- return;
696
- }
697
-
698
- // 根据内容类型格式化响应内容
699
- const contentType = form.getFieldValue('contentType') || '';
700
- const formattedContent = formatResponseContent(activeResponse.content, contentType);
701
-
702
- // 设置预览内容,包含响应名称
703
- setPreviewContent({
704
- title: `预览: ${activeResponse.name || '未命名响应'}`,
705
- content: formattedContent,
706
- description: '',
707
- contentType
708
- });
709
- setPreviewVisible(true);
710
- };
711
-
712
- const handleResponseSelect = (responseId) => {
713
- setCurrentResponseId(responseId);
714
- };
715
-
716
- // 在列表页面直接切换响应数据
717
- const handleResponseSwitch = async (interfaceId, responseId) => {
718
- try {
719
- const response = await axios.patch(`/cgi-bin/interfaces?id=${interfaceId}`, {
720
- activeResponseId: responseId
721
- });
722
-
723
- if (response.data.code === 0) {
724
- // 更新本地状态
725
- setInterfaces(interfaces.map(item =>
726
- item.id === interfaceId
727
- ? { ...item, activeResponseId: responseId }
728
- : item
729
- ));
730
-
731
- const activeResponse = interfaces
732
- .find(item => item.id === interfaceId)
733
- ?.responses
734
- ?.find(resp => resp.id === responseId);
735
-
736
- message.success(`已切换到响应: ${activeResponse?.name || '未命名'}`);
737
-
738
- // 刷新缓存以立即生效
739
- await refreshCacheAfterUpdate();
740
- }
741
- } catch (error) {
742
- console.error('切换响应失败:', error);
743
- message.error('切换响应失败: ' + (error.response?.data?.message || error.message));
744
- }
745
- };
746
-
747
- const filteredInterfaces = getFilteredInterfaces();
748
-
749
- // 处理表格变化(排序、分页)
750
- const handleTableChange = (pagination, filters, sorter) => {
751
- console.log('表格变化:', { pagination, filters, sorter });
752
-
753
- // 保存排序配置
754
- const sortConfig = {
755
- sortField: sorter.field || null,
756
- sortOrder: sorter.order || null,
757
- current: pagination.current,
758
- pageSize: pagination.pageSize
759
- };
760
-
761
- saveTableConfig(sortConfig);
762
- };
763
-
764
- // 处理列配置变更
765
- const handleColumnConfigChange = (checkedValues) => {
766
- // 确保必须显示的列始终被选中
767
- const requiredColumns = COLUMN_CONFIG.filter(col => col.required).map(col => col.key);
768
- const finalColumns = [...new Set([...requiredColumns, ...checkedValues])];
769
-
770
- saveTableConfig({ visibleColumns: finalColumns });
771
- };
772
-
773
- // 全选列配置
774
- const handleSelectAllColumns = () => {
775
- const allColumns = COLUMN_CONFIG.map(col => col.key);
776
- saveTableConfig({ visibleColumns: allColumns });
777
- };
778
-
779
- // 重置列配置
780
- const handleResetColumns = () => {
781
- const defaultColumns = COLUMN_CONFIG.map(col => col.key);
782
- saveTableConfig({ visibleColumns: defaultColumns });
783
- };
784
-
785
- // 切换列配置面板显示
786
- const handleColumnConfigToggle = (visible) => {
787
- setColumnConfigVisible(visible);
788
- };
789
-
790
- // 搜索处理
791
- const handleSearch = (value) => {
792
- setSearchValue(value);
793
- };
794
-
795
- // 清除搜索
796
- const handleClearSearch = () => {
797
- setSearchValue('');
798
- };
799
-
800
- // 表格行选择处理
801
- const onSelectChange = (selectedKeys) => {
802
- setSelectedRowKeys(selectedKeys);
803
- };
804
-
805
-
806
-
807
- // 刷新数据
808
- const refreshData = () => {
809
- fetchInterfaces();
810
- message.success('数据已刷新');
811
- };
812
-
813
- // 批量操作接口状态
814
- const handleBatchOperation = async (active) => {
815
- if (!selectedRowKeys || selectedRowKeys.length === 0) {
816
- message.warning('请先选择要操作的接口');
817
- return;
818
- }
819
-
820
- try {
821
- setLoading(true);
822
- const operations = selectedRowKeys.map(id =>
823
- axios.patch(`/cgi-bin/interfaces/${id}`, { active })
824
- );
825
-
826
- await Promise.all(operations);
827
- message.success(`已${active ? '启用' : '禁用'} ${selectedRowKeys.length} 个接口`);
828
- setSelectedRowKeys([]);
829
- fetchInterfaces();
830
-
831
- // 刷新规则缓存
832
- refreshCacheAfterUpdate();
833
- } catch (error) {
834
- console.error('批量操作失败:', error);
835
- message.error('批量操作失败: ' + (error.response?.data?.message || error.message));
836
- } finally {
837
- setLoading(false);
838
- }
839
- };
840
-
841
- // 定义所有可用的列
842
- const allColumns = [
843
- {
844
- title: '状态',
845
- dataIndex: 'active',
846
- key: 'active',
847
- width: 80,
848
- render: (active, record) => (
849
- <Switch
850
- checked={active !== false}
851
- onChange={(checked) => handleToggleActive(record.id, active !== false)}
852
- size="small"
853
- />
854
- ),
855
- },
856
- {
857
- title: '名称',
858
- dataIndex: 'name',
859
- key: 'name',
860
- sorter: (a, b) => a.name.localeCompare(b.name),
861
- render: (text) => <span className="interface-name">{text}</span>,
862
- },
863
- {
864
- title: '分组',
865
- dataIndex: 'group',
866
- key: 'group',
867
- width: 120,
868
- sorter: (a, b) => {
869
- const groupA = a.group || '';
870
- const groupB = b.group || '';
871
- return groupA.localeCompare(groupB);
872
- },
873
- render: (group) => {
874
- if (!group) {
875
- return <span style={{ color: '#999', fontStyle: 'italic' }}>未分组</span>;
876
- }
877
- return (
878
- <Tag color="blue" style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(group)}>
879
- {group}
880
- </Tag>
881
- );
882
- },
883
- },
884
- {
885
- title: 'URL匹配规则',
886
- dataIndex: 'urlPattern',
887
- key: 'urlPattern',
888
- ellipsis: true,
889
- render: (text) => <span className="url-pattern">{text}</span>,
890
- },
891
- {
892
- title: '处理方式',
893
- dataIndex: 'proxyType',
894
- key: 'proxyType',
895
- width: 120,
896
- render: (text) => {
897
- const proxyType = proxyTypes.find(item => item.value === text);
898
- return (
899
- <Badge
900
- color={proxyType?.color || '#999'}
901
- text={proxyType?.label || text}
902
- />
903
- );
904
- },
905
- },
906
- {
907
- title: '当前响应',
908
- dataIndex: 'activeResponseId',
909
- key: 'currentResponse',
910
- width: 200,
911
- render: (activeResponseId, record) => {
912
- // 如果没有响应数据,返回空
913
- if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
914
- return <span className="no-response">无响应数据</span>;
915
- }
916
-
917
- return (
918
- <Select
919
- value={activeResponseId || record.responses[0]?.id}
920
- style={{ width: '100%' }}
921
- onChange={(value) => handleResponseSwitch(record.id, value)}
922
- disabled={record.proxyType !== 'response'}
923
- >
924
- {record.responses.map(resp => {
925
- return (
926
- <Option key={resp.id} value={resp.id}>
927
- {resp.name}
928
- </Option>
929
- );
930
- })}
931
- </Select>
932
- );
933
- },
934
- },
935
- {
936
- title: '延迟(毫秒)',
937
- dataIndex: 'responseDelay',
938
- key: 'responseDelay',
939
- width: 100,
940
- sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
941
- render: (delay) => {
942
- const delayValue = parseInt(delay, 10) || 0;
943
- return (
944
- <span className={delayValue > 0 ? 'delay-active' : 'delay-inactive'}>
945
- {delayValue > 0 ? delayValue : '无延迟'}
946
- </span>
947
- );
948
- },
949
- },
950
- {
951
- title: '状态码',
952
- dataIndex: 'httpStatus',
953
- key: 'httpStatus',
954
- width: 100,
955
- render: (status) => {
956
- const statusCode = status || 200;
957
- let statusClass = 'status-success';
958
- if (statusCode >= 400) {
959
- statusClass = 'status-error';
960
- } else if (statusCode >= 300) {
961
- statusClass = 'status-warning';
962
- }
963
- return <span className={statusClass}>{statusCode}</span>;
964
- },
965
- },
966
- {
967
- title: '内容类型',
968
- dataIndex: 'contentType',
969
- key: 'contentType',
970
- width: 120,
971
- sorter: (a, b) => {
972
- const aType = a.contentType || '';
973
- const bType = b.contentType || '';
974
- return aType.localeCompare(bType);
975
- },
976
- sortDirections: ['ascend', 'descend'],
977
- render: (text, record) => {
978
- if (record.proxyType !== 'response') {
979
- return '-';
980
- }
981
- const found = contentTypes.find(item => item.value === text);
982
- return found ? found.label : text;
983
- }
984
- },
985
- {
986
- title: '目标URL',
987
- dataIndex: 'targetUrl',
988
- key: 'targetUrl',
989
- ellipsis: true,
990
- sorter: (a, b) => {
991
- const aUrl = a.targetUrl || '';
992
- const bUrl = b.targetUrl || '';
993
- return aUrl.localeCompare(bUrl);
994
- },
995
- sortDirections: ['ascend', 'descend'],
996
- render: (text, record) => {
997
- return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
998
- }
999
- },
1000
- {
1001
- title: '自定义头',
1002
- dataIndex: 'customHeaders',
1003
- key: 'customHeaders',
1004
- width: 100,
1005
- sorter: (a, b) => {
1006
- const aCount = Object.keys(a.customHeaders || {}).length;
1007
- const bCount = Object.keys(b.customHeaders || {}).length;
1008
- return aCount - bCount;
1009
- },
1010
- sortDirections: ['ascend', 'descend'],
1011
- render: (_, record) => {
1012
- if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
1013
- return '-';
1014
- }
1015
-
1016
- const headers = record.customHeaders || {};
1017
- const count = Object.keys(headers).length;
1018
-
1019
- if (count === 0) {
1020
- return '-';
1021
- }
1022
-
1023
- // 检查是否包含随机值
1024
- const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
1025
-
1026
- return (
1027
- <Tooltip title={
1028
- <div>
1029
- {Object.entries(headers).map(([key, value]) => (
1030
- <div key={key}>
1031
- {key}: {value}
1032
- {value && value.startsWith('@') && (
1033
- <span style={{ color: '#52c41a' }}> (随机)</span>
1034
- )}
1035
- </div>
1036
- ))}
1037
- </div>
1038
- }>
1039
- <span style={{ color: '#1677ff' }}>
1040
- {count}个
1041
- {hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
1042
- </span>
1043
- </Tooltip>
1044
- );
1045
- }
1046
- },
1047
- {
1048
- title: '参数匹配',
1049
- dataIndex: 'paramMatchers',
1050
- key: 'paramMatchers',
1051
- width: 120,
1052
- render: (_, record) => {
1053
- if (record.proxyType !== 'response') {
1054
- return '-';
1055
- }
1056
-
1057
- const showInterface = interfaceParamMatcherEnabled;
1058
- const showResponse = responseParamMatcherEnabled;
1059
-
1060
- const interfaceMatchers = record.paramMatchers || [];
1061
- const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
1062
-
1063
- // 统计所有响应级规则
1064
- const responsesWithMatchers = showResponse
1065
- ? (record.responses || []).filter(resp =>
1066
- Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
1067
- )
1068
- : [];
1069
- const responseMatcherTotal = responsesWithMatchers.reduce(
1070
- (sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
1071
- );
1072
-
1073
- if (interfaceCount === 0 && responseMatcherTotal === 0) {
1074
- return '-';
1075
- }
1076
-
1077
- const tooltipContent = (
1078
- <div style={{ maxWidth: 280 }}>
1079
- {interfaceCount > 0 && (
1080
- <div style={{ marginBottom: responsesWithMatchers.length > 0 ? 8 : 0 }}>
1081
- <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#faad14' }}>接口级条件({interfaceCount}条)</div>
1082
- {interfaceMatchers.filter(m => m.paramPath).map((m, i) => (
1083
- <div key={i} style={{ fontSize: '12px' }}>
1084
- {m.paramPath}: {m.paramValue}
1085
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1086
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1087
- </span>
1088
- </div>
1089
- ))}
1090
- </div>
1091
- )}
1092
- {responsesWithMatchers.length > 0 && (
1093
- <div>
1094
- <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1677ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
1095
- {responsesWithMatchers.map((resp, ri) => (
1096
- <div key={ri} style={{ marginBottom: 4 }}>
1097
- <div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
1098
- {resp.paramMatchers.filter(m => m.paramPath).map((m, mi) => (
1099
- <div key={mi} style={{ fontSize: '12px', paddingLeft: 8 }}>
1100
- {m.paramPath}: {m.paramValue}
1101
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
1102
- ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1103
- </span>
1104
- </div>
1105
- ))}
1106
- </div>
1107
- ))}
1108
- </div>
1109
- )}
1110
- </div>
1111
- );
1112
-
1113
- return (
1114
- <Tooltip title={tooltipContent}>
1115
- <Space size={4} direction="vertical" style={{ lineHeight: 1.4 }}>
1116
- {interfaceCount > 0 && (
1117
- <Tag color="orange" style={{ fontSize: '11px', padding: '0 4px' }}>接口:{interfaceCount}条</Tag>
1118
- )}
1119
- {responseMatcherTotal > 0 && (
1120
- <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>响应:{responseMatcherTotal}条</Tag>
1121
- )}
1122
- </Space>
1123
- </Tooltip>
1124
- );
1125
- }
1126
- },
1127
- {
1128
- title: '请求方法',
1129
- dataIndex: 'httpMethod',
1130
- key: 'httpMethod',
1131
- width: 120,
1132
- sorter: (a, b) => {
1133
- const aMethod = a.httpMethod || '';
1134
- const bMethod = b.httpMethod || '';
1135
- return aMethod.localeCompare(bMethod);
1136
- },
1137
- sortDirections: ['ascend', 'descend'],
1138
- render: (text) => {
1139
- const found = httpMethods.find(item => item.value === text);
1140
- return found ? found.label : text;
1141
- }
1142
- },
1143
- {
1144
- title: '操作',
1145
- key: 'action',
1146
- width: 220,
1147
- render: (_, record) => (
1148
- <div className="action-buttons">
1149
- <Button
1150
- type="text"
1151
- icon={<EditOutlined />}
1152
- onClick={() => handleEditInterface(record)}
1153
- />
1154
- {record.proxyType === 'response' && (
1155
- <Tooltip title="数据管理">
1156
- <Button
1157
- type="text"
1158
- icon={<FileTextOutlined />}
1159
- onClick={() => { setDataModalRecord(record); setDataModalVisible(true); }}
1160
- />
1161
- </Tooltip>
1162
- )}
1163
- <Tooltip title="复制新增">
1164
- <Button
1165
- type="text"
1166
- icon={<CopyOutlined />}
1167
- onClick={() => handleDuplicate(record)}
1168
- />
1169
- </Tooltip>
1170
- <Popconfirm
1171
- title="确定要删除此接口吗?"
1172
- onConfirm={() => handleDeleteInterface(record.id)}
1173
- okText="确定"
1174
- cancelText="取消"
1175
- >
1176
- <Button type="text" danger icon={<DeleteOutlined />} />
1177
- </Popconfirm>
1178
- </div>
1179
- ),
1180
- },
1181
- ];
1182
-
1183
- // 根据配置过滤可见列
1184
- const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
1185
- const columns = allColumns.filter(col => visibleColumns.includes(col.key));
1186
-
1187
- // 分组筛选和操作区域
1188
- const handleBatchToggleActive = async (active) => {
1189
- try {
1190
- setGroupActionLoading(true);
1191
- const response = await axios.patch(`/cgi-bin/interfaces?active=${active}`, {
1192
- featureId: selectedFeatureId,
1193
- group: selectedGroup
1194
- });
1195
- if (response.data && response.data.code === 0) {
1196
- message.success(`分组 "${selectedGroup}" 中的接口已成功${active ? '启用' : '禁用'}`);
1197
- fetchInterfaces();
1198
- } else {
1199
- throw new Error(response.data?.message || '批量操作失败');
1200
- }
1201
- } catch (error) {
1202
- console.error('批量操作失败:', error);
1203
- message.error(error.response?.data?.message || error.message || '批量操作失败');
1204
- } finally {
1205
- setGroupActionLoading(false);
1206
- }
1207
- };
1208
-
1209
- return (
1210
- <AppLayout>
1211
- <div className="interface-management-container">
1212
- {/* 警告信息置顶显示 */}
1213
- {!features.length && (
1214
- <Alert
1215
- message="未找到功能模块"
1216
- description="请先在模拟数据页面创建功能模块,然后再添加接口"
1217
- type="warning"
1218
- showIcon
1219
- style={{ marginBottom: 16 }}
1220
- />
1221
- )}
1222
-
1223
- {selectedFeature?.active === false && (
1224
- <Alert
1225
- message="功能模块已禁用"
1226
- description="当前功能模块已被禁用,所有关联接口不会生效。您可以在模拟数据页面启用此功能模块。"
1227
- type="warning"
1228
- showIcon
1229
- style={{ marginBottom: 16 }}
1230
- />
1231
- )}
1232
-
1233
- <Card className="interface-control-panel">
1234
- <div className="control-panel-header">
1235
- <div className="control-left">
1236
- <div className="feature-selector">
1237
- <span>功能模块:</span>
1238
- <Select
1239
- value={selectedFeatureId}
1240
- onChange={handleSelectFeature}
1241
- style={{ width: 200 }}
1242
- placeholder="选择功能模块"
1243
- loading={featuresLoading}
1244
- dropdownMatchSelectWidth={false}
1245
- >
1246
- {(features || []).map(feature => (
1247
- <Option key={feature.id} value={feature.id}>
1248
- {feature.name}
1249
- {feature.active === false &&
1250
- <span style={{ color: '#ff4d4f', marginLeft: 8 }}>(已禁用)</span>
1251
- }
1252
- </Option>
1253
- ))}
1254
- </Select>
1255
- </div>
1256
-
1257
- <Search
1258
- ref={searchRef}
1259
- placeholder="搜索接口名称或URL"
1260
- onSearch={handleSearch}
1261
- value={searchValue}
1262
- onChange={(e) => setSearchValue(e.target.value)}
1263
- style={{ width: 220, marginLeft: 12 }}
1264
- enterButton={<SearchOutlined />}
1265
- allowClear
1266
- />
1267
-
1268
- <Dropdown
1269
- overlay={
1270
- <Menu
1271
- selectedKeys={selectedGroup ? [selectedGroup] : ['ALL_GROUPS']}
1272
- onClick={({ key }) => {
1273
- if (key === 'ALL_GROUPS') {
1274
- setSelectedGroup(null);
1275
- } else {
1276
- setSelectedGroup(key === selectedGroup ? null : key);
1277
- }
1278
- }}
1279
- >
1280
- <Menu.Item key="ALL_GROUPS">全部分组</Menu.Item>
1281
- {groups.map(group => (
1282
- <Menu.Item key={group}>
1283
- {group} ({interfaces.filter(item => item.group === group).length})
1284
- </Menu.Item>
1285
- ))}
1286
- </Menu>
1287
- }
1288
- trigger={['click']}
1289
- disabled={groups.length === 0}
1290
- >
1291
- <Button style={{ marginLeft: 12 }} icon={<FilterOutlined />}>
1292
- {selectedGroup ? `分组: ${selectedGroup}` : '分组筛选'}
1293
- <DownOutlined />
1294
- </Button>
1295
- </Dropdown>
1296
- </div>
1297
-
1298
- <div className="control-right">
1299
- <Space>
1300
- <Tooltip title="刷新数据">
1301
- <Button
1302
- icon={<ReloadOutlined />}
1303
- onClick={refreshData}
1304
- loading={interfacesLoading}
1305
- />
1306
- </Tooltip>
1307
-
1308
- <Button
1309
- type="primary"
1310
- icon={<PlusOutlined />}
1311
- onClick={handleAddInterface}
1312
- disabled={!selectedFeatureId || selectedFeature?.active === false}
1313
- >
1314
- 添加接口
1315
- </Button>
1316
- </Space>
1317
- </div>
1318
- </div>
1319
-
1320
- {/* 统计和批量操作行 */}
1321
- <div className="control-panel-stats">
1322
- <div className="stats-left">
1323
- <Space size="large">
1324
- {selectedGroup && (
1325
- <span className="stat-item">
1326
- <span className="stat-label">当前分组:</span>
1327
- <Tag color="blue">{selectedGroup}</Tag>
1328
- </span>
1329
- )}
1330
-
1331
- <span className="stat-item">
1332
- {searchValue ? (
1333
- <Badge
1334
- count={`搜索"${searchValue}" - ${filteredInterfaces.length}个结果`}
1335
- style={{ backgroundColor: '#108ee9' }}
1336
- />
1337
- ) : (
1338
- <Badge
1339
- count={`共 ${filteredInterfaces.length} 个接口`}
1340
- style={{ backgroundColor: '#52c41a' }}
1341
- />
1342
- )}
1343
- </span>
1344
- </Space>
1345
- </div>
1346
-
1347
- <div className="stats-right">
1348
- {selectedRowKeys.length > 0 && (
1349
- <Space>
1350
- <span className="selected-count">
1351
- 已选择 {selectedRowKeys.length} 项
1352
- </span>
1353
- <Popconfirm
1354
- title="确定要批量启用选中的接口吗?"
1355
- onConfirm={() => handleBatchOperation(true)}
1356
- okText="确定"
1357
- cancelText="取消"
1358
- >
1359
- <Button size="small" type="primary">批量启用</Button>
1360
- </Popconfirm>
1361
- <Popconfirm
1362
- title="确定要批量禁用选中的接口吗?"
1363
- onConfirm={() => handleBatchOperation(false)}
1364
- okText="确定"
1365
- cancelText="取消"
1366
- >
1367
- <Button size="small" danger>批量禁用</Button>
1368
- </Popconfirm>
1369
- </Space>
1370
- )}
1371
-
1372
- {selectedGroup && (
1373
- <Space style={{ marginLeft: selectedRowKeys.length > 0 ? 16 : 0 }}>
1374
- <Popconfirm
1375
- title={`确定要启用分组 "${selectedGroup}" 中的所有接口吗?`}
1376
- onConfirm={() => handleBatchToggleActive(true)}
1377
- okText="确定"
1378
- cancelText="取消"
1379
- >
1380
- <Button
1381
- type="primary"
1382
- size="small"
1383
- loading={groupActionLoading}
1384
- >
1385
- 分组启用
1386
- </Button>
1387
- </Popconfirm>
1388
- <Popconfirm
1389
- title={`确定要禁用分组 "${selectedGroup}" 中的所有接口吗?`}
1390
- onConfirm={() => handleBatchToggleActive(false)}
1391
- okText="确定"
1392
- cancelText="取消"
1393
- >
1394
- <Button
1395
- danger
1396
- size="small"
1397
- loading={groupActionLoading}
1398
- >
1399
- 分组禁用
1400
- </Button>
1401
- </Popconfirm>
1402
- </Space>
1403
- )}
1404
-
1405
- {/* 列设置按钮 */}
1406
- <Popover
1407
- title="自定义显示列"
1408
- trigger="click"
1409
- open={columnConfigVisible}
1410
- onOpenChange={handleColumnConfigToggle}
1411
- content={
1412
- <div style={{ width: 280 }}>
1413
- <div style={{ marginBottom: 12 }}>
1414
- <Space>
1415
- <Button size="small" onClick={handleSelectAllColumns}>
1416
- 全选
1417
- </Button>
1418
- <Button size="small" onClick={handleResetColumns}>
1419
- 重置
1420
- </Button>
1421
- </Space>
1422
- </div>
1423
- <Checkbox.Group
1424
- value={tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key)}
1425
- onChange={handleColumnConfigChange}
1426
- style={{ width: '100%' }}
1427
- >
1428
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
1429
- {COLUMN_CONFIG.map(col => (
1430
- <Checkbox
1431
- key={col.key}
1432
- value={col.key}
1433
- disabled={col.required}
1434
- style={{
1435
- width: '100%',
1436
- color: col.required ? '#999' : undefined
1437
- }}
1438
- >
1439
- {col.title}
1440
- {col.required && (
1441
- <span style={{ color: '#999', fontSize: '12px', marginLeft: 4 }}>
1442
- (必须)
1443
- </span>
1444
- )}
1445
- </Checkbox>
1446
- ))}
1447
- </div>
1448
- </Checkbox.Group>
1449
- </div>
1450
- }
1451
- >
1452
- <Tooltip title="自定义表格显示列">
1453
- <Button
1454
- icon={<SettingOutlined />}
1455
- type="text"
1456
- size="small"
1457
- className="inline-column-settings-btn"
1458
- style={{ marginLeft: (selectedRowKeys.length > 0 || selectedGroup) ? 16 : 0 }}
1459
- >
1460
- 列设置
1461
- </Button>
1462
- </Tooltip>
1463
- </Popover>
1464
- </div>
1465
- </div>
1466
- </Card>
1467
-
1468
- <Card className="list-container-card" bordered={false} bodyStyle={{ padding: 0 }}>
1469
- <div className="interface-list-container">
1470
- <Table
1471
- columns={columns}
1472
- dataSource={filteredInterfaces}
1473
- rowKey="id"
1474
- loading={interfacesLoading}
1475
- onChange={handleTableChange}
1476
- rowSelection={{
1477
- selectedRowKeys,
1478
- onChange: onSelectChange,
1479
- }}
1480
- size="middle"
1481
- pagination={{
1482
- current: tableConfig.current,
1483
- pageSize: tableConfig.pageSize,
1484
- showSizeChanger: true,
1485
- showQuickJumper: true,
1486
- showTotal: (total, range) => `共 ${total} 个接口,显示第 ${range[0]}-${range[1]} 个`,
1487
- pageSizeOptions: ['10', '20', '50', '100'],
1488
- size: 'default'
1489
- }}
1490
- locale={{ emptyText: '暂无接口配置' }}
1491
- sortDirections={['ascend', 'descend']}
1492
- className="interface-table"
1493
- />
1494
- </div>
1495
- </Card>
1496
-
1497
- {/* 接口编辑/创建模态框/抽屉 */}
1498
- {interfaceFormDisplayMode === 'drawer' ? (
1499
- <Drawer
1500
- title={
1501
- <div style={{
1502
- fontSize: '18px',
1503
- fontWeight: 600,
1504
- color: '#262626',
1505
- display: 'flex',
1506
- alignItems: 'center',
1507
- gap: '8px'
1508
- }}>
1509
- <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1510
- <Badge
1511
- count="配置向导"
1512
- style={{
1513
- backgroundColor: '#f0f9ff',
1514
- color: '#1677ff',
1515
- fontSize: '11px',
1516
- fontWeight: 'normal'
1517
- }}
1518
- />
1519
- </div>
1520
- }
1521
- placement="right"
1522
- width={1040}
1523
- open={modalVisible}
1524
- onClose={handleCancel}
1525
- destroyOnClose
1526
- bodyStyle={{
1527
- padding: '0',
1528
- overflowY: 'auto'
1529
- }}
1530
- footer={
1531
- <div style={{ textAlign: 'right' }}>
1532
- <Space>
1533
- <Button onClick={handleCancel}>取消</Button>
1534
- <Button type="primary" onClick={handleSubmit}>
1535
- {editingInterface ? '保存' : '创建'}
1536
- </Button>
1537
- </Space>
1538
- </div>
1539
- }
1540
- className="interface-drawer"
1541
- >
1542
- <div style={{ padding: '24px' }}>
1543
- <Form
1544
- form={form}
1545
- layout="vertical"
1546
- initialValues={{
1547
- name: '',
1548
- group: '',
1549
- pattern: '',
1550
- proxyType: 'response',
1551
- statusCode: '200',
1552
- contentType: 'application/json; charset=utf-8',
1553
- responses: [],
1554
- httpMethod: 'ALL',
1555
- targetUrl: '',
1556
- headerItems: [],
1557
- paramMatchers: [],
1558
- responseDelay: '0'
1559
- }}
1560
- >
1561
- <Collapse
1562
- defaultActiveKey={
1563
- editingInterface && editingInterface.proxyType === 'response'
1564
- ? ['3']
1565
- : ['1', '2', '3']
1566
- }
1567
- style={{ marginBottom: '16px' }}
1568
- ghost
1569
- >
1570
- {/* 第一步:基础信息 */}
1571
- <Collapse.Panel
1572
- header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="1" style={{ backgroundColor: '#1677ff' }} /><span>基础信息</span></div>}
1573
- key="1"
1574
- >
1575
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1576
- <div style={{ flex: 1 }}>
1577
- <Form.Item name="name" label="接口名称" rules={[{ required: true, message: '请输入接口名称' }]}>
1578
- <Input placeholder="请输入接口名称" />
1579
- </Form.Item>
1580
- </div>
1581
- <div style={{ flex: 1 }}>
1582
- <Form.Item name="group" label="分组" tooltip="为接口设置分组,便于管理和批量操作">
1583
- <Select placeholder="选择或创建分组" showSearch allowClear style={{ width: '100%' }} mode="tags" maxTagCount="responsive" maxTagTextLength={10}
1584
- onChange={(value) => {
1585
- if (Array.isArray(value) && value.length > 0) {
1586
- const groupValue = value[0] === '' ? undefined : value[0];
1587
- form.setFieldsValue({ group: groupValue });
1588
- }
1589
- }}
1590
- onInputKeyDown={(e) => { if (e.key === 'Enter') e.stopPropagation(); }}
1591
- >
1592
- {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1593
- <Option key={group} value={group}>{group}</Option>
1594
- ))}
1595
- </Select>
1596
- </Form.Item>
1597
- </div>
1598
- </div>
1599
- </Collapse.Panel>
1600
-
1601
- {/* 第二步:URL配置(复用 Modal 的内容) */}
1602
- <Collapse.Panel
1603
- header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="2" style={{ backgroundColor: '#1677ff' }} /><span>URL配置</span></div>}
1604
- key="2"
1605
- >
1606
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1607
- <div style={{ flex: 1 }}>
1608
- <Form.Item name="pattern" label="URL匹配规则" rules={[{ required: true, message: '请输入URL匹配规则' }, ({ getFieldValue }) => ({ validator(_, value) { const proxyType = getFieldValue('proxyType'); if (!value) return Promise.resolve(); if (proxyType === 'url_redirect') { try { new URL(value); return Promise.resolve(); } catch (e) { return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL')); } } if (proxyType === 'redirect') { if (value.startsWith('http://') || value.startsWith('https://')) return Promise.resolve(); return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头')); } return Promise.resolve(); } })]}>
1609
- <Input placeholder="根据选择的处理方式输入相应格式的URL" />
1610
- </Form.Item>
1611
- </div>
1612
- <div style={{ flex: 1 }}>
1613
- <Form.Item name="proxyType" label="处理方式" rules={[{ required: true, message: '请选择处理方式' }]}>
1614
- <Select onChange={() => form.setFieldsValue({ pattern: '' })}>
1615
- {proxyTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}
1616
- </Select>
1617
- </Form.Item>
1618
- </div>
1619
- </div>
1620
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1621
- <div style={{ flex: 1 }}>
1622
- <Form.Item name="httpMethod" label="请求方法" rules={[{ required: true, message: '请选择请求方法' }]}>
1623
- <Select>{httpMethods.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1624
- </Form.Item>
1625
- </div>
1626
- <div style={{ flex: 1 }} />
1627
- </div>
1628
- </Collapse.Panel>
1629
-
1630
- {/* 第三步:高级配置 */}
1631
- <Collapse.Panel
1632
- header={<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Badge count="3" style={{ backgroundColor: '#1677ff' }} /><span>高级配置</span></div>}
1633
- key="3"
1634
- >
1635
- <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
1636
- {({ getFieldValue, setFieldsValue }) => {
1637
- const proxyType = getFieldValue('proxyType');
1638
- if (proxyType === 'response') {
1639
- return (
1640
- <>
1641
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1642
- <div style={{ flex: 1 }}>
1643
- <Form.Item name="statusCode" label="状态码" rules={[{ required: true, message: '请选择状态码' }]}>
1644
- <Select>{statusCodes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1645
- </Form.Item>
1646
- </div>
1647
- <div style={{ flex: 1 }}>
1648
- <Form.Item name="contentType" label="内容类型" rules={[{ required: true, message: '请选择内容类型' }]}>
1649
- <Select>{contentTypes.map(item => (<Option key={item.value} value={item.value}>{item.label}</Option>))}</Select>
1650
- </Form.Item>
1651
- </div>
1652
- <div style={{ flex: 1 }}>
1653
- <Form.Item name="responseDelay" label="延迟响应(毫秒)" rules={[{ pattern: /^\d*$/, message: '请输入有效的数字' }]}>
1654
- <Input placeholder="例如:500" />
1655
- </Form.Item>
1656
- </div>
1657
- </div>
1658
- {interfaceParamMatcherEnabled && (
1659
- <Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。">
1660
- <Form.List name="paramMatchers">
1661
- {(fields, { add, remove }) => (<>
1662
- {fields.map(({ key, name, ...restField }) => (
1663
- <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1664
- <Form.Item {...restField} name={[name, 'paramPath']} rules={[{ required: true, message: '请输入参数路径' }]} style={{ flex: 1, margin: 0 }}>
1665
- <Input placeholder="参数路径(如:userId 或 data.user.id)" />
1666
- </Form.Item>
1667
- <Form.Item {...restField} name={[name, 'paramValue']} rules={[{ required: true, message: '请输入期望值' }]} style={{ flex: 1, margin: 0 }}>
1668
- <Input placeholder="期望值(如:123 或 admin)" />
1669
- </Form.Item>
1670
- <Form.Item {...restField} name={[name, 'matchType']} style={{ width: 90, margin: 0 }} initialValue="exact">
1671
- <Select><Option value="exact">精确匹配</Option><Option value="contains">包含</Option><Option value="regex">正则</Option></Select>
1672
- </Form.Item>
1673
- <Form.Item noStyle shouldUpdate>
1674
- {({ getFieldValue }) => {
1675
- const responses = getFieldValue('responses') || [];
1676
- return (
1677
- <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 130, margin: 0 }}>
1678
- <Select allowClear placeholder="指定响应(可选)">
1679
- {responses.filter(r => r && r.id).map(r => (
1680
- <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
1681
- ))}
1682
- </Select>
1683
- </Form.Item>
1684
- );
1685
- }}
1686
- </Form.Item>
1687
- <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1688
- </div>
1689
- ))}
1690
- <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%' }}>添加参数匹配规则</Button>
1691
- </>)}
1692
- </Form.List>
1693
- </Form.Item>
1694
- )}
1695
- <Form.Item name="responses" initialValue={[]} hidden><Input /></Form.Item>
1696
- <Form.Item name="activeResponseId" hidden><Input /></Form.Item>
1697
- <Form.Item noStyle shouldUpdate>
1698
- {({ getFieldValue }) => {
1699
- let responses = getFieldValue('responses') || [];
1700
- const activeResponseId = getFieldValue('activeResponseId');
1701
- if (!Array.isArray(responses)) { try { responses = JSON.parse(responses); } catch (e) { responses = []; } }
1702
- return (<ResponseContentEditor form={form} responses={responses} activeResponseId={activeResponseId} onPreview={handlePreview} />);
1703
- }}
1704
- </Form.Item>
1705
- </>
1706
- );
1707
- } else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
1708
- return (
1709
- <>
1710
- <Form.Item name="targetUrl" label="重定向目标URL" rules={[{ required: true, message: '请输入重定向目标URL' }, { validator(_, value) { if (!value) return Promise.resolve(); try { new URL(value); return Promise.resolve(); } catch (e) { return Promise.reject(new Error('请输入有效的URL格式')); } } }]} tooltip="重定向的目标URL,必须是完整的URL格式">
1711
- <Input placeholder="例如:https://api.example.com/users" />
1712
- </Form.Item>
1713
- <Form.Item label="自定义请求头">
1714
- <Form.List name="headerItems">
1715
- {(fields, { add, remove }) => (<>
1716
- {fields.map(({ key, name, ...restField }) => (
1717
- <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1718
- <Form.Item {...restField} name={[name, 'headerName']} rules={[{ required: true, message: '请输入请求头名称' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头名称(如:Authorization)" /></Form.Item>
1719
- <Form.Item {...restField} name={[name, 'headerValue']} rules={[{ required: true, message: '请输入请求头值' }]} style={{ flex: 1, margin: 0 }}><Input placeholder="请求头值(如:Bearer token123)" /></Form.Item>
1720
- <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1721
- </div>
1722
- ))}
1723
- <Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} style={{ width: '100%', marginTop: '8px' }}>添加请求头</Button>
1724
- </>)}
1725
- </Form.List>
1726
- </Form.Item>
1727
- </>
1728
- );
1729
- }
1730
- return null;
1731
- }}
1732
- </Form.Item>
1733
- </Collapse.Panel>
1734
- </Collapse>
1735
- </Form>
1736
- </div>
1737
- </Drawer>
1738
- ) : (
1739
- <Modal
1740
- title={
1741
- <div style={{
1742
- fontSize: '18px',
1743
- fontWeight: 600,
1744
- color: '#262626',
1745
- display: 'flex',
1746
- alignItems: 'center',
1747
- gap: '8px'
1748
- }}>
1749
- <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1750
- <Badge
1751
- count="配置向导"
1752
- style={{
1753
- backgroundColor: '#f0f9ff',
1754
- color: '#1677ff',
1755
- fontSize: '11px',
1756
- fontWeight: 'normal'
1757
- }}
1758
- />
1759
- </div>
1760
- }
1761
- open={modalVisible}
1762
- onOk={handleSubmit}
1763
- onCancel={handleCancel}
1764
- width={1200}
1765
- destroyOnClose
1766
- okText={editingInterface ? '保存' : '创建'}
1767
- cancelText="取消"
1768
- bodyStyle={{
1769
- maxHeight: '75vh',
1770
- overflow: 'auto',
1771
- padding: '0'
1772
- }}
1773
- className="interface-modal"
1774
- >
1775
- <div style={{ padding: '24px' }}>
1776
- <Form
1777
- form={form}
1778
- layout="vertical"
1779
- initialValues={{
1780
- name: '',
1781
- group: '',
1782
- pattern: '',
1783
- proxyType: 'response',
1784
- statusCode: '200',
1785
- contentType: 'application/json; charset=utf-8',
1786
- responses: [],
1787
- httpMethod: 'ALL',
1788
- targetUrl: '',
1789
- headerItems: [],
1790
- paramMatchers: [],
1791
- responseDelay: '0'
1792
- }}
1793
- >
1794
- <Collapse
1795
- defaultActiveKey={
1796
- editingInterface && editingInterface.proxyType === 'response'
1797
- ? ['3'] // mock代理场景:只展开第3步(高级配置)
1798
- : ['1', '2', '3'] // 其他场景:展开所有步骤
1799
- }
1800
- style={{ marginBottom: '16px' }}
1801
- ghost
1802
- >
1803
- {/* 第一步:基础信息 */}
1804
- <Collapse.Panel
1805
- header={
1806
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1807
- <Badge count="1" style={{ backgroundColor: '#1677ff' }} />
1808
- <span>基础信息</span>
1809
- </div>
1810
- }
1811
- key="1"
1812
- >
1813
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1814
- <div style={{ flex: 1 }}>
1815
- <Form.Item
1816
- name="name"
1817
- label="接口名称"
1818
- rules={[{ required: true, message: '请输入接口名称' }]}
1819
- >
1820
- <Input placeholder="请输入接口名称" />
1821
- </Form.Item>
1822
- </div>
1823
- <div style={{ flex: 1 }}>
1824
- <Form.Item
1825
- name="group"
1826
- label="分组"
1827
- tooltip="为接口设置分组,便于管理和批量操作"
1828
- >
1829
- <Select
1830
- placeholder="选择或创建分组"
1831
- showSearch
1832
- allowClear
1833
- style={{ width: '100%' }}
1834
- mode="tags"
1835
- maxTagCount="responsive"
1836
- maxTagTextLength={10}
1837
- onChange={(value) => {
1838
- // 如果值是数组(tags模式),只取第一个元素
1839
- if (Array.isArray(value) && value.length > 0) {
1840
- // 如果第一个元素是空字符串,则设置为undefined
1841
- const groupValue = value[0] === '' ? undefined : value[0];
1842
- form.setFieldsValue({ group: groupValue });
1843
- }
1844
- }}
1845
- onInputKeyDown={(e) => {
1846
- // 防止回车键提交表单
1847
- if (e.key === 'Enter') {
1848
- e.stopPropagation();
1849
- }
1850
- }}
1851
- >
1852
- {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1853
- <Option key={group} value={group}>{group}</Option>
1854
- ))}
1855
- </Select>
1856
- </Form.Item>
1857
- </div>
1858
- </div>
1859
- </Collapse.Panel>
1860
-
1861
- {/* 第二步:URL配置 */}
1862
- <Collapse.Panel
1863
- header={
1864
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1865
- <Badge count="2" style={{ backgroundColor: '#1677ff' }} />
1866
- <span>URL配置</span>
1867
- </div>
1868
- }
1869
- key="2"
1870
- >
1871
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1872
- <div style={{ flex: 1 }}>
1873
- <Form.Item
1874
- name="pattern"
1875
- label="URL匹配规则"
1876
- rules={[
1877
- { required: true, message: '请输入URL匹配规则' },
1878
- ({ getFieldValue }) => ({
1879
- validator(_, value) {
1880
- const proxyType = getFieldValue('proxyType');
1881
- if (!value) return Promise.resolve();
1882
-
1883
- // URL重定向模式下,必须是完整URL
1884
- if (proxyType === 'url_redirect') {
1885
- try {
1886
- new URL(value);
1887
- return Promise.resolve();
1888
- } catch (e) {
1889
- return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL(包括http://或https://)'));
1890
- }
1891
- }
1892
-
1893
- // 重定向模式下,必须以http://或https://开头
1894
- if (proxyType === 'redirect') {
1895
- if (value.startsWith('http://') || value.startsWith('https://')) {
1896
- return Promise.resolve();
1897
- }
1898
- return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头'));
1899
- }
1900
-
1901
- // 响应模式下的验证
1902
- if (proxyType === 'response') {
1903
- // 正则表达式验证
1904
- if (value.startsWith('/') && value.endsWith('/')) {
1905
- try {
1906
- new RegExp(value.slice(1, -1));
1907
- return Promise.resolve();
1908
- } catch (e) {
1909
- return Promise.reject(new Error('无效的正则表达式格式'));
1910
- }
1911
- }
1912
-
1913
- // 通配符验证
1914
- if (value.includes('*')) {
1915
- if (/^[a-zA-Z0-9\-_/.*]+$/.test(value)) {
1916
- return Promise.resolve();
1917
- }
1918
- return Promise.reject(new Error('通配符URL格式不正确'));
1919
- }
1920
-
1921
- // 普通路径验证
1922
- if (/^[a-zA-Z0-9\-_/]+$/.test(value)) {
1923
- return Promise.resolve();
1924
- }
1925
- return Promise.reject(new Error('URL路径格式不正确'));
1926
- }
1927
-
1928
- return Promise.resolve();
1929
- },
1930
- }),
1931
- ]}
1932
- tooltip={{
1933
- title: (
1934
- <>
1935
- <div>不同处理方式下URL匹配规则要求:</div>
1936
- <ul style={{margin: '5px 0 0 15px', padding: 0}}>
1937
- <li><b>模拟响应:</b> 支持路径格式如 /api/users,通配符如 /api/*,正则如 /\/api\/\d+/</li>
1938
- <li><b>重定向:</b> 必须以http://或https://开头,例如:https://example.com/api</li>
1939
- <li><b>URL重定向:</b> 必须是完整URL,包括http://或https://,例如:https://example.com/api/users</li>
1940
- </ul>
1941
- </>
1942
- ),
1943
- overlayStyle: { maxWidth: '450px' }
1944
- }}
1945
- >
1946
- <Input
1947
- placeholder="根据选择的处理方式输入相应格式的URL"
1948
- />
1949
- </Form.Item>
1950
- </div>
1951
- <div style={{ flex: 1 }}>
1952
- <Form.Item
1953
- name="proxyType"
1954
- label="处理方式"
1955
- rules={[{ required: true, message: '请选择处理方式' }]}
1956
- tooltip={{
1957
- title: (
1958
- <>
1959
- <div>不同处理方式的规则说明:</div>
1960
- <ul style={{margin: '5px 0 0 15px', padding: 0}}>
1961
- <li><b>模拟响应:</b> 返回您定义的响应内容</li>
1962
- <li><b>重定向:</b> 将请求重定向到其他URL,URL匹配规则必须以http://或https://开头</li>
1963
- <li><b>URL重定向:</b> 完全匹配URL时重定向,URL匹配规则必须是完整URL</li>
1964
- </ul>
1965
- </>
1966
- ),
1967
- overlayStyle: { maxWidth: '450px' }
1968
- }}
1969
- >
1970
- <Select onChange={(value) => {
1971
- // 当切换代理类型时,清空pattern字段,并提供不同的placeholder
1972
- form.setFieldsValue({ pattern: '' });
1973
-
1974
- // 为不同的代理类型提供不同的pattern占位符
1975
- const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
1976
- if (patternInput) {
1977
- if (value === 'response') {
1978
- patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
1979
- } else if (value === 'redirect') {
1980
- patternInput.placeholder = "例如:https://example.com/api";
1981
- } else if (value === 'url_redirect') {
1982
- patternInput.placeholder = "例如:https://example.com/api/users";
1983
- }
1984
- }
1985
- }}>
1986
- {proxyTypes.map(item => (
1987
- <Option key={item.value} value={item.value}>{item.label}</Option>
1988
- ))}
1989
- </Select>
1990
- </Form.Item>
1991
- </div>
1992
- </div>
1993
-
1994
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1995
- <div style={{ flex: 1 }}>
1996
- <Form.Item
1997
- name="httpMethod"
1998
- label="请求方法"
1999
- rules={[{ required: true, message: '请选择请求方法' }]}
2000
- >
2001
- <Select>
2002
- {httpMethods.map(item => (
2003
- <Option key={item.value} value={item.value}>{item.label}</Option>
2004
- ))}
2005
- </Select>
2006
- </Form.Item>
2007
- </div>
2008
- <div style={{ flex: 1 }}>
2009
- {/* 这里可以留空或添加其他字段 */}
2010
- </div>
2011
- </div>
2012
- </Collapse.Panel>
2013
-
2014
- {/* 第三步:高级配置 */}
2015
- <Collapse.Panel
2016
- header={
2017
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
2018
- <Badge count="3" style={{ backgroundColor: '#1677ff' }} />
2019
- <span>高级配置</span>
2020
- </div>
2021
- }
2022
- key="3"
2023
- >
2024
-
2025
- {/* 根据proxyType显示不同的表单项 */}
2026
- <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
2027
- {({ getFieldValue, setFieldsValue }) => {
2028
- const proxyType = getFieldValue('proxyType');
2029
-
2030
- if (proxyType === 'response') {
2031
- return (
2032
- <>
2033
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
2034
- <div style={{ flex: 1 }}>
2035
- <Form.Item
2036
- name="statusCode"
2037
- label="状态码"
2038
- rules={[{ required: true, message: '请选择状态码' }]}
2039
- >
2040
- <Select>
2041
- {statusCodes.map(item => (
2042
- <Option key={item.value} value={item.value}>{item.label}</Option>
2043
- ))}
2044
- </Select>
2045
- </Form.Item>
2046
- </div>
2047
- <div style={{ flex: 1 }}>
2048
- <Form.Item
2049
- name="contentType"
2050
- label="内容类型"
2051
- rules={[{ required: true, message: '请选择内容类型' }]}
2052
- >
2053
- <Select>
2054
- {contentTypes.map(item => (
2055
- <Option key={item.value} value={item.value}>{item.label}</Option>
2056
- ))}
2057
- </Select>
2058
- </Form.Item>
2059
- </div>
2060
- <div style={{ flex: 1 }}>
2061
- <Form.Item
2062
- name="responseDelay"
2063
- label="延迟响应(毫秒)"
2064
- tooltip="设置接口响应延迟时间,单位为毫秒"
2065
- rules={[
2066
- {
2067
- pattern: /^\d*$/,
2068
- message: '请输入有效的数字'
2069
- },
2070
- {
2071
- validator: (_, value) => {
2072
- if (!value || parseInt(value, 10) <= 60000) {
2073
- return Promise.resolve();
2074
- }
2075
- return Promise.reject(new Error('延迟时间不能超过60秒(60000毫秒)'));
2076
- }
2077
- }
2078
- ]}
2079
- >
2080
- <Input placeholder="例如:500" />
2081
- </Form.Item>
2082
- </div>
2083
- </div>
2084
-
2085
- {/* 请求入参匹配设置(接口级,用于同URL多接口区分) */}
2086
- {interfaceParamMatcherEnabled && (
2087
- <Form.Item
2088
- label="接口匹配条件"
2089
- tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。支持嵌套属性路径,如:a.b.c"
2090
- >
2091
- <Form.List name="paramMatchers">
2092
- {(fields, { add, remove }) => (
2093
- <>
2094
- {fields.map(({ key, name, ...restField }) => (
2095
- <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
2096
- <Form.Item
2097
- {...restField}
2098
- name={[name, 'paramPath']}
2099
- rules={[
2100
- { required: true, message: '请输入参数路径' },
2101
- {
2102
- pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
2103
- message: '请输入有效的参数路径,如:userId 或 data.user.id'
2104
- }
2105
- ]}
2106
- style={{ flex: 1, margin: 0 }}
2107
- >
2108
- <Input placeholder="参数路径(如:userId 或 data.user.id)" />
2109
- </Form.Item>
2110
- <Form.Item
2111
- {...restField}
2112
- name={[name, 'paramValue']}
2113
- rules={[{ required: true, message: '请输入期望值' }]}
2114
- style={{ flex: 1, margin: 0 }}
2115
- >
2116
- <Input placeholder="期望值(如:123 或 admin)" />
2117
- </Form.Item>
2118
- <Form.Item
2119
- {...restField}
2120
- name={[name, 'matchType']}
2121
- style={{ width: 100, margin: 0 }}
2122
- initialValue="exact"
2123
- >
2124
- <Select placeholder="匹配类型">
2125
- <Option value="exact">精确匹配</Option>
2126
- <Option value="contains">包含</Option>
2127
- <Option value="regex">正则</Option>
2128
- </Select>
2129
- </Form.Item>
2130
- <Form.Item noStyle shouldUpdate>
2131
- {({ getFieldValue }) => {
2132
- const responses = getFieldValue('responses') || [];
2133
- return (
2134
- <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 140, margin: 0 }}>
2135
- <Select allowClear placeholder="指定响应(可选)">
2136
- {responses.filter(r => r && r.id).map(r => (
2137
- <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
2138
- ))}
2139
- </Select>
2140
- </Form.Item>
2141
- );
2142
- }}
2143
- </Form.Item>
2144
- <Button
2145
- type="text"
2146
- danger
2147
- icon={<DeleteOutlined />}
2148
- onClick={() => remove(name)}
2149
- />
2150
- </div>
2151
- ))}
2152
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
2153
- <Button
2154
- type="dashed"
2155
- onClick={() => add()}
2156
- icon={<PlusOutlined />}
2157
- style={{ flex: 1 }}
2158
- >
2159
- 添加参数匹配规则
2160
- </Button>
2161
- <Popover
2162
- content={
2163
- <div style={{ maxWidth: '300px' }}>
2164
- <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
2165
- <div style={{ lineHeight: '1.6' }}>
2166
- <div>• <strong>精确匹配</strong>:参数值完全相等</div>
2167
- <div>• <strong>包含</strong>:参数值包含指定内容</div>
2168
- <div>• <strong>正则</strong>:参数值符合正则表达式</div>
2169
- <div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
2170
- </div>
2171
- </div>
2172
- }
2173
- title="参数匹配规则"
2174
- trigger="click"
2175
- placement="top"
2176
- >
2177
- <Button
2178
- type="text"
2179
- icon={<QuestionCircleOutlined />}
2180
- style={{
2181
- color: '#999',
2182
- padding: '4px'
2183
- }}
2184
- size="small"
2185
- />
2186
- </Popover>
2187
- </div>
2188
- </>
2189
- )}
2190
- </Form.List>
2191
- </Form.Item>
2192
- )}
2193
-
2194
- {/* 隐藏的表单字段用于存储响应数据 */}
2195
- <Form.Item
2196
- name="responses"
2197
- initialValue={[]}
2198
- hidden
2199
- >
2200
- <Input />
2201
- </Form.Item>
2202
- <Form.Item name="activeResponseId" hidden>
2203
- <Input />
2204
- </Form.Item>
2205
-
2206
- {/* 使用新的合并组件 */}
2207
- <Form.Item noStyle shouldUpdate>
2208
- {({ getFieldValue }) => {
2209
- let responses = getFieldValue('responses') || [];
2210
- const activeResponseId = getFieldValue('activeResponseId');
2211
-
2212
- // 确保是数组
2213
- if (!Array.isArray(responses)) {
2214
- try {
2215
- if (typeof responses === 'string') {
2216
- responses = JSON.parse(responses);
2217
- }
2218
- } catch (e) {
2219
- responses = [];
2220
- }
2221
- }
2222
-
2223
- return (
2224
- <ResponseContentEditor
2225
- form={form}
2226
- responses={responses}
2227
- activeResponseId={activeResponseId}
2228
- onPreview={handlePreview}
2229
- />
2230
- );
2231
- }}
2232
- </Form.Item>
2233
- </>
2234
- );
2235
- } else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
2236
- return (
2237
- <>
2238
- <Form.Item
2239
- name="targetUrl"
2240
- label="重定向目标URL"
2241
- rules={[
2242
- { required: true, message: '请输入重定向目标URL' },
2243
- {
2244
- validator(_, value) {
2245
- if (!value) return Promise.resolve();
2246
- try {
2247
- new URL(value);
2248
- return Promise.resolve();
2249
- } catch (e) {
2250
- return Promise.reject(new Error('请输入有效的URL格式(包括http://或https://)'));
2251
- }
2252
- }
2253
- }
2254
- ]}
2255
- tooltip="重定向的目标URL,必须是完整的URL格式"
2256
- >
2257
- <Input
2258
- placeholder="例如:https://api.example.com/users"
2259
- />
2260
- </Form.Item>
2261
-
2262
- {/* 自定义请求头设置 */}
2263
- <Form.Item label="自定义请求头">
2264
- <Form.List name="headerItems">
2265
- {(fields, { add, remove }) => (
2266
- <>
2267
- {fields.map(({ key, name, ...restField }) => (
2268
- <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
2269
- <Form.Item
2270
- {...restField}
2271
- name={[name, 'headerName']}
2272
- rules={[{ required: true, message: '请输入请求头名称' }]}
2273
- style={{ flex: 1, margin: 0 }}
2274
- >
2275
- <Input placeholder="请求头名称(如:Authorization)" />
2276
- </Form.Item>
2277
- <Form.Item
2278
- {...restField}
2279
- name={[name, 'headerValue']}
2280
- rules={[{ required: true, message: '请输入请求头值' }]}
2281
- style={{ flex: 1, margin: 0 }}
2282
- >
2283
- <Input placeholder="请求头值(如:Bearer token123)" />
2284
- </Form.Item>
2285
- <Button
2286
- type="text"
2287
- danger
2288
- icon={<DeleteOutlined />}
2289
- onClick={() => remove(name)}
2290
- />
2291
- </div>
2292
- ))}
2293
- <Button
2294
- type="dashed"
2295
- onClick={() => add()}
2296
- icon={<PlusOutlined />}
2297
- style={{ width: '100%', marginTop: '8px' }}
2298
- >
2299
- 添加请求头
2300
- </Button>
2301
- </>
2302
- )}
2303
- </Form.List>
2304
- </Form.Item>
2305
- </>
2306
- );
2307
- }
2308
-
2309
- return null;
2310
- }}
2311
- </Form.Item>
2312
- </Collapse.Panel>
2313
- </Collapse>
2314
- </Form>
2315
- </div>
2316
- </Modal>
2317
- )}
2318
-
2319
- {/* 使用拆分后的预览模态框组件 */}
2320
- <PreviewModal
2321
- visible={previewVisible}
2322
- onCancel={() => setPreviewVisible(false)}
2323
- previewContent={previewContent}
2324
- />
2325
-
2326
- {/* 数据管理弹窗/抽屉 */}
2327
- {displayMode === 'drawer' ? (
2328
- <DataManagementDrawer
2329
- visible={dataModalVisible}
2330
- onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2331
- interfaceItem={dataModalRecord}
2332
- onSaved={() => {
2333
- fetchInterfaces();
2334
- }}
2335
- />
2336
- ) : (
2337
- <DataManagementModal
2338
- visible={dataModalVisible}
2339
- onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
2340
- interfaceItem={dataModalRecord}
2341
- onSaved={() => {
2342
- fetchInterfaces();
2343
- }}
2344
- />
2345
- )}
2346
- </div>
2347
- </AppLayout>
2348
- );
2349
- };
2350
-
2351
- export default InterfaceManagement;