whistle.mock-plugins 1.1.1-beta.7 → 1.1.1-dev.10

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.
@@ -4,7 +4,7 @@ import AppLayout from '../components/AppLayout';
4
4
  import {
5
5
  Table, Button, Modal, Form, Input, Select, message, Switch,
6
6
  Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col,
7
- Popover, Checkbox
7
+ Popover, Checkbox, Tag
8
8
  } from 'antd';
9
9
  import {
10
10
  PlusOutlined, EditOutlined, DeleteOutlined,
@@ -32,9 +32,11 @@ const { Option } = Select;
32
32
  const COLUMN_CONFIG = [
33
33
  { key: 'active', title: '状态', required: true },
34
34
  { key: 'name', title: '名称', required: true },
35
+ { key: 'group', title: '分组', required: false },
35
36
  { key: 'urlPattern', title: 'URL匹配规则', required: false },
36
37
  { key: 'proxyType', title: '处理方式', required: false },
37
38
  { key: 'currentResponse', title: '当前响应', required: false },
39
+ { key: 'responseDelay', title: '延迟(毫秒)', required: false },
38
40
  { key: 'httpStatus', title: '状态码', required: false },
39
41
  { key: 'contentType', title: '内容类型', required: false },
40
42
  { key: 'targetUrl', title: '目标URL', required: false },
@@ -56,6 +58,11 @@ const InterfaceManagement = () => {
56
58
  const [interfacesLoading, setInterfacesLoading] = useState(false);
57
59
  const [selectedFeatureId, setSelectedFeatureId] = useState(null);
58
60
 
61
+ // 分组状态
62
+ const [groups, setGroups] = useState([]);
63
+ const [selectedGroup, setSelectedGroup] = useState(null);
64
+ const [groupActionLoading, setGroupActionLoading] = useState(false);
65
+
59
66
  // 模态框状态
60
67
  const [modalVisible, setModalVisible] = useState(false);
61
68
  const [previewVisible, setPreviewVisible] = useState(false);
@@ -165,6 +172,9 @@ const InterfaceManagement = () => {
165
172
 
166
173
  setInterfaces(processedInterfaces);
167
174
  console.log('处理后的接口列表:', processedInterfaces);
175
+
176
+ // 提取并更新分组列表
177
+ updateGroups(processedInterfaces);
168
178
  } else {
169
179
  console.warn('接口数据格式不正确:', response.data);
170
180
  setInterfaces([]);
@@ -178,6 +188,41 @@ const InterfaceManagement = () => {
178
188
  setInterfacesLoading(false);
179
189
  }
180
190
  };
191
+
192
+ // 提取并更新分组列表
193
+ const updateGroups = (interfaces) => {
194
+ // 从接口列表中提取所有分组
195
+ const groupSet = new Set();
196
+ interfaces.forEach(item => {
197
+ if (item.group) {
198
+ groupSet.add(item.group);
199
+ }
200
+ });
201
+
202
+ // 转换为数组并排序
203
+ const groupArray = Array.from(groupSet).sort();
204
+ setGroups(groupArray);
205
+
206
+ // 如果当前选中的分组不在列表中,重置选中的分组
207
+ if (selectedGroup && !groupArray.includes(selectedGroup)) {
208
+ setSelectedGroup(null);
209
+ }
210
+ };
211
+
212
+ // 根据分组筛选接口
213
+ const getFilteredInterfaces = () => {
214
+ // 首先按功能模块筛选
215
+ const featureFiltered = interfaces.filter(item =>
216
+ !selectedFeatureId || item.featureId === selectedFeatureId
217
+ );
218
+
219
+ // 然后按分组筛选
220
+ if (selectedGroup) {
221
+ return featureFiltered.filter(item => item.group === selectedGroup);
222
+ }
223
+
224
+ return featureFiltered;
225
+ };
181
226
 
182
227
  const handleAddInterface = () => {
183
228
  if (!selectedFeatureId) {
@@ -214,77 +259,63 @@ const InterfaceManagement = () => {
214
259
  };
215
260
 
216
261
  const handleEditInterface = (record) => {
217
- console.log('开始编辑接口:', record.name, '原始数据:', record);
218
- setEditingInterface(record);
219
-
220
- // 将自定义请求头转换为数组格式,用于动态表单项
221
- let headersArray = [];
222
- if (record.customHeaders && typeof record.customHeaders === 'object') {
223
- headersArray = Object.entries(record.customHeaders).map(([key, value]) => ({
224
- headerName: key,
225
- headerValue: value
226
- }));
227
- }
228
-
229
- // 处理参数匹配规则,转换为数组格式
230
- let paramMatchersArray = [];
231
- if (record.paramMatchers && Array.isArray(record.paramMatchers)) {
232
- paramMatchersArray = record.paramMatchers.map(matcher => ({
233
- paramPath: matcher.paramPath || '',
234
- paramValue: matcher.paramValue || '',
235
- matchType: matcher.matchType || 'exact'
236
- }));
262
+ if (!record) {
263
+ message.warning('接口数据不完整,无法编辑');
264
+ return;
237
265
  }
238
-
239
- // 处理多响应数据
240
- let initialResponses = [];
241
- let initialResponseId = null;
242
-
243
- // 深拷贝记录中的响应数据,避免引用问题
266
+
267
+ console.log('编辑接口:', record);
268
+
269
+ // 重置表单
270
+ form.resetFields();
271
+
272
+ // 处理响应数据
273
+ let responses = [];
274
+ let activeResponseId = '';
275
+
244
276
  if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
245
- console.log('使用现有响应数据, 数量:', record.responses.length);
246
-
247
- initialResponses = JSON.parse(JSON.stringify(record.responses));
248
- initialResponseId = record.activeResponseId || record.responses[0].id;
249
-
250
- // 确保每个响应都有名称
251
- initialResponses = initialResponses.map((resp, index) => ({
252
- id: resp.id || generateResponseId(),
253
- name: resp.name || `响应 ${index + 1}`,
254
- description: resp.description || '',
255
- content: resp.content || '{}'
256
- }));
277
+ responses = record.responses;
278
+ activeResponseId = record.activeResponseId || responses[0].id;
257
279
  } else if (record.responseContent) {
258
- // 向后兼容:如果只有传统的 responseContent 字段,创建一个默认响应
259
- console.log('创建默认响应,使用responseContent字段');
280
+ // 兼容旧数据格式,创建默认响应
260
281
  const defaultResponseId = generateResponseId();
261
- initialResponses = [
262
- {
263
- id: defaultResponseId,
264
- name: '默认响应',
265
- description: '',
266
- content: record.responseContent
267
- }
268
- ];
269
- initialResponseId = defaultResponseId;
282
+ responses = [{
283
+ id: defaultResponseId,
284
+ name: '默认响应',
285
+ description: '',
286
+ content: record.responseContent
287
+ }];
288
+ activeResponseId = defaultResponseId;
270
289
  }
271
290
 
272
- console.log('编辑接口,设置响应数据:', initialResponses);
273
- setCurrentResponseId(initialResponseId);
291
+ // 处理自定义请求头
292
+ let headerItems = [];
293
+ if (record.headers && typeof record.headers === 'object') {
294
+ headerItems = Object.entries(record.headers).map(([headerName, headerValue]) => ({
295
+ headerName,
296
+ headerValue
297
+ }));
298
+ }
274
299
 
300
+ // 设置表单值
275
301
  form.setFieldsValue({
276
302
  name: record.name,
303
+ group: record.group || '',
277
304
  pattern: record.urlPattern,
278
305
  proxyType: record.proxyType || 'response',
279
- statusCode: record.httpStatus?.toString() || '200',
306
+ statusCode: (record.httpStatus || record.statusCode || 200).toString(),
280
307
  contentType: record.contentType || 'application/json; charset=utf-8',
281
- responses: initialResponses,
282
- activeResponseId: initialResponseId,
283
- httpMethod: record.httpMethod || 'ALL',
308
+ responses,
309
+ activeResponseId,
310
+ httpMethod: record.httpMethod || record.method || 'ALL',
284
311
  targetUrl: record.targetUrl || '',
285
- headerItems: headersArray, // 使用数组存储表单项
286
- paramMatchers: paramMatchersArray
312
+ headerItems,
313
+ paramMatchers: record.paramMatchers || [],
314
+ responseDelay: record.responseDelay ? record.responseDelay.toString() : '0'
287
315
  });
316
+
317
+ setCurrentResponseId(activeResponseId);
318
+ setEditingInterface(record);
288
319
  setModalVisible(true);
289
320
  };
290
321
 
@@ -308,12 +339,15 @@ const InterfaceManagement = () => {
308
339
 
309
340
  const handleToggleActive = async (id, currentActive) => {
310
341
  try {
311
- const response = await axios.patch(`/cgi-bin/interfaces?id=${id}`, {
342
+ const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
312
343
  active: !currentActive
313
344
  });
314
345
  if (response.data && response.data.code === 0) {
315
346
  message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
316
347
  fetchInterfaces();
348
+
349
+ // 刷新规则缓存
350
+ refreshCacheAfterUpdate();
317
351
  } else {
318
352
  throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
319
353
  }
@@ -325,174 +359,81 @@ const InterfaceManagement = () => {
325
359
 
326
360
  const handleSubmit = async () => {
327
361
  try {
362
+ // 表单验证
328
363
  const values = await form.validateFields();
329
- console.log('=== 开始表单提交 ===');
330
- console.log('表单提交,原始值:', JSON.stringify(values, null, 2));
331
-
332
- // 获取当前激活的响应ID和所有响应
333
- const activeResponseId = values.activeResponseId;
334
- let responses = values.responses || [];
335
-
336
- console.log('提取的原始响应数据:', responses);
337
- console.log('响应数据类型:', typeof responses);
338
- console.log('是否为数组:', Array.isArray(responses));
364
+ console.log('表单提交数据:', values);
339
365
 
340
- // 确保responses是数组格式
341
- if (!Array.isArray(responses)) {
342
- console.log('响应数据不是数组,尝试解析...');
343
- try {
344
- if (typeof responses === 'string') {
345
- responses = JSON.parse(responses);
346
- if (!Array.isArray(responses)) {
347
- responses = [];
348
- }
349
- } else {
350
- responses = [];
366
+ // 处理自定义请求头
367
+ let headers = {};
368
+ if (values.headerItems && Array.isArray(values.headerItems)) {
369
+ values.headerItems.forEach(item => {
370
+ if (item && item.headerName) {
371
+ headers[item.headerName] = item.headerValue;
351
372
  }
352
- } catch (e) {
353
- console.error('解析响应数据失败:', e);
354
- responses = [];
355
- }
356
- }
357
-
358
- console.log('处理前的响应数据:', JSON.stringify(responses, null, 2));
359
-
360
- // 确保每个响应都有名称、描述和内容
361
- const cleanedResponses = responses.map((resp, index) => {
362
- console.log(`处理响应 ${index}:`, resp);
363
- const cleaned = {
364
- id: resp.id || generateResponseId(),
365
- name: resp.name || `响应 ${index + 1}`,
366
- description: resp.description || '',
367
- content: resp.content || '{}'
368
- };
369
- console.log(`处理后的响应 ${index}:`, cleaned);
370
- return cleaned;
371
- });
372
-
373
- // 如果没有响应,创建一个默认响应
374
- if (cleanedResponses.length === 0) {
375
- console.log('没有响应数据,创建默认响应');
376
- const defaultId = generateResponseId();
377
- cleanedResponses.push({
378
- id: defaultId,
379
- name: '默认响应',
380
- description: '',
381
- content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
382
373
  });
383
- setCurrentResponseId(defaultId);
384
374
  }
385
375
 
386
- // 确保有一个激活的响应ID
387
- const validActiveResponseId = activeResponseId && cleanedResponses.some(r => r.id === activeResponseId)
388
- ? activeResponseId
389
- : cleanedResponses[0].id;
390
-
391
- // 获取激活的响应
392
- const activeResponse = cleanedResponses.find(r => r.id === validActiveResponseId) || cleanedResponses[0];
393
-
394
- console.log('=== 清理后的响应数据 ===');
395
- console.log('清理后的响应数据:', JSON.stringify(cleanedResponses, null, 2));
396
- console.log('激活的响应ID:', validActiveResponseId);
397
- console.log('激活的响应:', activeResponse);
398
-
399
- // 检查responseBody是否是有效的JSON
400
- if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
401
- cleanedResponses.forEach(response => {
402
- try {
403
- if (response.content) {
404
- JSON.parse(response.content);
405
- }
406
- } catch (e) {
407
- message.error(`响应 "${response.name}" 不是有效的JSON格式`);
408
- throw new Error(`响应 "${response.name}" 不是有效的JSON格式`);
409
- }
410
- });
411
- }
412
-
413
- // 处理自定义请求头,将表单项数组转换为对象格式
414
- let customHeaders = {};
415
-
416
- if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
417
- if (values.headerItems && values.headerItems.length > 0) {
418
- values.headerItems.forEach(item => {
419
- if (item && item.headerName && item.headerName.trim()) {
420
- customHeaders[item.headerName.trim()] = item.headerValue || '';
421
- }
422
- });
423
- }
424
- }
425
-
426
- // 处理参数匹配规则,清理无效的规则
427
- let paramMatchers = [];
428
- if (values.proxyType === 'response' && values.paramMatchers && values.paramMatchers.length > 0) {
429
- paramMatchers = values.paramMatchers
430
- .filter(item => item && item.paramPath && item.paramPath.trim() && item.paramValue !== undefined)
431
- .map(item => ({
432
- paramPath: item.paramPath.trim(),
433
- paramValue: item.paramValue,
434
- matchType: item.matchType || 'exact'
435
- }));
436
- }
437
-
438
- console.log('=== 参数匹配规则处理 ===');
439
- console.log('原始参数匹配规则:', values.paramMatchers);
440
- console.log('清理后的参数匹配规则:', paramMatchers);
441
-
376
+ // 构建接口数据
442
377
  const interfaceData = {
443
378
  name: values.name,
444
- featureId: selectedFeatureId,
379
+ group: values.group,
445
380
  urlPattern: values.pattern,
446
381
  proxyType: values.proxyType,
447
- // 同时保存所有响应和当前活跃的响应
448
- responses: cleanedResponses,
449
- activeResponseId: validActiveResponseId,
450
- // 兼容性保留:将当前活跃响应的内容保存到 responseContent
451
- responseContent: activeResponse ? activeResponse.content : '',
452
- targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
453
- customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
454
- paramMatchers: paramMatchers, // 添加参数匹配规则
382
+ featureId: selectedFeatureId,
383
+ responses: values.responses,
384
+ activeResponseId: values.activeResponseId,
455
385
  httpStatus: parseInt(values.statusCode, 10), // 转换为数字
456
386
  contentType: values.contentType,
457
- responseDelay: 0,
387
+ responseDelay: parseInt(values.responseDelay, 10) || 0,
458
388
  httpMethod: values.httpMethod,
459
389
  active: true
460
390
  };
461
391
 
462
- console.log('=== 最终提交的接口数据 ===');
463
- console.log('提交的接口数据:', JSON.stringify(interfaceData, null, 2));
392
+ // 根据代理类型添加不同字段
393
+ if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
394
+ interfaceData.targetUrl = values.targetUrl;
395
+ interfaceData.headers = headers;
396
+ }
464
397
 
465
- let response;
466
- if (editingInterface) {
467
- // 更新现有接口
468
- console.log('执行接口更新,ID:', editingInterface.id);
469
- response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
470
- } else {
471
- // 创建新接口
472
- console.log('执行接口创建');
473
- response = await axios.post('/cgi-bin/interfaces', interfaceData);
398
+ // 添加参数匹配规则
399
+ if (values.paramMatchers && Array.isArray(values.paramMatchers) && values.paramMatchers.length > 0) {
400
+ // 过滤掉空的匹配规则
401
+ interfaceData.paramMatchers = values.paramMatchers.filter(matcher =>
402
+ matcher && matcher.paramPath && matcher.paramValue
403
+ );
474
404
  }
475
-
476
- console.log('服务器响应:', response.data);
477
-
478
- if (response.data && response.data.code === 0) {
479
- console.log('=== 接口操作成功 ===');
480
- message.success(editingInterface ? '接口更新成功' : '接口创建成功');
481
- setModalVisible(false);
482
- fetchInterfaces();
483
-
484
- // 刷新规则缓存
485
- refreshCacheAfterUpdate();
405
+
406
+ console.log('提交接口数据:', interfaceData);
407
+
408
+ if (editingInterface) {
409
+ // 更新接口
410
+ const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
411
+ if (response.data && response.data.code === 0) {
412
+ message.success('接口更新成功');
413
+ setModalVisible(false);
414
+ fetchInterfaces();
415
+
416
+ // 刷新规则缓存
417
+ refreshCacheAfterUpdate();
418
+ } else {
419
+ throw new Error(response.data?.message || '接口更新失败');
420
+ }
486
421
  } else {
487
- throw new Error(response.data?.message || '操作失败');
422
+ // 创建接口
423
+ const response = await axios.post('/cgi-bin/interfaces', interfaceData);
424
+ if (response.data && response.data.code === 0) {
425
+ message.success('接口创建成功');
426
+ setModalVisible(false);
427
+ fetchInterfaces();
428
+
429
+ // 刷新规则缓存
430
+ refreshCacheAfterUpdate();
431
+ } else {
432
+ throw new Error(response.data?.message || '接口创建失败');
433
+ }
488
434
  }
489
435
  } catch (error) {
490
- if (error.message && error.message.includes('JSON格式')) {
491
- // 已经显示了错误信息,不需要再显示
492
- return;
493
- }
494
- console.error('=== 接口操作失败 ===');
495
- console.error('操作失败:', error);
436
+ console.error('表单提交失败:', error);
496
437
  message.error(error.response?.data?.message || error.message || '操作失败');
497
438
  }
498
439
  };
@@ -568,9 +509,7 @@ const InterfaceManagement = () => {
568
509
  }
569
510
  };
570
511
 
571
- const filteredInterfaces = interfaces.filter(item =>
572
- !selectedFeatureId || item.featureId === selectedFeatureId
573
- );
512
+ const filteredInterfaces = getFilteredInterfaces();
574
513
 
575
514
  // 处理表格变化(排序、分页)
576
515
  const handleTableChange = (pagination, filters, sorter) => {
@@ -620,15 +559,11 @@ const InterfaceManagement = () => {
620
559
  dataIndex: 'active',
621
560
  key: 'active',
622
561
  width: 80,
623
- sorter: (a, b) => {
624
- // true排在前面,false排在后面
625
- return Number(b.active) - Number(a.active);
626
- },
627
- sortDirections: ['descend', 'ascend'],
628
562
  render: (active, record) => (
629
563
  <Switch
630
- checked={active}
564
+ checked={active !== false}
631
565
  onChange={() => handleToggleActive(record.id, active)}
566
+ size="small"
632
567
  />
633
568
  ),
634
569
  },
@@ -636,113 +571,115 @@ const InterfaceManagement = () => {
636
571
  title: '名称',
637
572
  dataIndex: 'name',
638
573
  key: 'name',
639
- ellipsis: true,
640
- sorter: (a, b) => a.name.localeCompare(b.name, 'zh-CN'),
641
- sortDirections: ['ascend', 'descend'],
574
+ sorter: (a, b) => a.name.localeCompare(b.name),
575
+ render: (text) => <span className="interface-name">{text}</span>,
576
+ },
577
+ {
578
+ title: '分组',
579
+ dataIndex: 'group',
580
+ key: 'group',
581
+ width: 120,
582
+ sorter: (a, b) => {
583
+ const groupA = a.group || '';
584
+ const groupB = b.group || '';
585
+ return groupA.localeCompare(groupB);
586
+ },
587
+ render: (group) => {
588
+ if (!group) {
589
+ return <span style={{ color: '#999', fontStyle: 'italic' }}>未分组</span>;
590
+ }
591
+ return (
592
+ <Tag color="blue" style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(group)}>
593
+ {group}
594
+ </Tag>
595
+ );
596
+ },
642
597
  },
643
598
  {
644
599
  title: 'URL匹配规则',
645
600
  dataIndex: 'urlPattern',
646
601
  key: 'urlPattern',
647
602
  ellipsis: true,
648
- sorter: (a, b) => a.urlPattern.localeCompare(b.urlPattern),
649
- sortDirections: ['ascend', 'descend'],
603
+ render: (text) => <span className="url-pattern">{text}</span>,
650
604
  },
651
605
  {
652
606
  title: '处理方式',
653
607
  dataIndex: 'proxyType',
654
608
  key: 'proxyType',
655
609
  width: 120,
656
- sorter: (a, b) => {
657
- const aType = a.proxyType || 'response';
658
- const bType = b.proxyType || 'response';
659
- return aType.localeCompare(bType);
660
- },
661
- sortDirections: ['ascend', 'descend'],
662
610
  render: (text) => {
663
- const found = proxyTypes.find(item => item.value === text);
664
- return found ? found.label : text || '模拟响应';
665
- }
611
+ const proxyType = proxyTypes.find(item => item.value === text);
612
+ return (
613
+ <Badge
614
+ color={proxyType?.color || '#999'}
615
+ text={proxyType?.label || text}
616
+ />
617
+ );
618
+ },
666
619
  },
667
620
  {
668
621
  title: '当前响应',
669
- dataIndex: 'responses',
622
+ dataIndex: 'activeResponseId',
670
623
  key: 'currentResponse',
671
- width: 160,
672
- render: (responses, record) => {
673
- // 仅在模拟响应类型时显示
674
- if (record.proxyType !== 'response') {
675
- return '-';
676
- }
677
-
678
- // 如果没有响应数据
679
- if (!responses || !Array.isArray(responses) || responses.length === 0) {
680
- return <span style={{ color: '#999' }}>无响应数据</span>;
624
+ width: 180,
625
+ render: (activeResponseId, record) => {
626
+ // 如果没有响应数据,返回空
627
+ if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
628
+ return <span className="no-response">无响应数据</span>;
681
629
  }
682
630
 
683
- // 如果只有一个响应,直接显示名称
684
- if (responses.length === 1) {
685
- return (
686
- <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
687
- <span>{responses[0].name || '默认响应'}</span>
688
- <Badge count="1" style={{ backgroundColor: '#52c41a' }} />
689
- </div>
690
- );
691
- }
631
+ // 找到当前激活的响应
632
+ const activeResponse = record.responses.find(resp => resp.id === activeResponseId);
692
633
 
693
- // 多个响应时显示选择器
694
- const activeResponse = responses.find(r => r.id === record.activeResponseId) || responses[0];
634
+ // 如果没有找到激活的响应,使用第一个
635
+ const currentResponse = activeResponse || record.responses[0];
695
636
 
696
637
  return (
697
- <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
698
- <Select
699
- size="small"
700
- value={record.activeResponseId || activeResponse.id}
701
- onChange={(responseId) => handleResponseSwitch(record.id, responseId)}
702
- style={{ width: '110px' }}
703
- optionLabelProp="label"
704
- disabled={record.active === false}
705
- >
706
- {responses.map(resp => (
707
- <Option
708
- key={resp.id}
709
- value={resp.id}
710
- label={resp.name || '未命名'}
711
- >
712
- <Tooltip title={resp.description || resp.name}>
713
- <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
714
- <span>{resp.name || '未命名'}</span>
715
- {resp.id === (record.activeResponseId || activeResponse.id) && (
716
- <Badge status="processing" />
717
- )}
718
- </div>
719
- </Tooltip>
720
- </Option>
721
- ))}
722
- </Select>
723
- <Badge
724
- count={responses.length}
725
- style={{ backgroundColor: '#1890ff' }}
726
- title={`共${responses.length}个响应`}
727
- />
728
- </div>
638
+ <Select
639
+ value={activeResponseId || record.responses[0]?.id}
640
+ style={{ width: '100%' }}
641
+ onChange={(value) => handleResponseSwitch(record.id, value)}
642
+ disabled={record.proxyType !== 'response'}
643
+ >
644
+ {record.responses.map(resp => (
645
+ <Option key={resp.id} value={resp.id}>
646
+ {resp.name}
647
+ </Option>
648
+ ))}
649
+ </Select>
729
650
  );
730
- }
651
+ },
652
+ },
653
+ {
654
+ title: '延迟(毫秒)',
655
+ dataIndex: 'responseDelay',
656
+ key: 'responseDelay',
657
+ width: 100,
658
+ sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
659
+ render: (delay) => {
660
+ const delayValue = parseInt(delay, 10) || 0;
661
+ return (
662
+ <span className={delayValue > 0 ? 'delay-active' : 'delay-inactive'}>
663
+ {delayValue > 0 ? delayValue : '无延迟'}
664
+ </span>
665
+ );
666
+ },
731
667
  },
732
668
  {
733
669
  title: '状态码',
734
670
  dataIndex: 'httpStatus',
735
671
  key: 'httpStatus',
736
672
  width: 100,
737
- sorter: (a, b) => {
738
- const aStatus = parseInt(a.httpStatus) || 0;
739
- const bStatus = parseInt(b.httpStatus) || 0;
740
- return aStatus - bStatus;
673
+ render: (status) => {
674
+ const statusCode = status || 200;
675
+ let statusClass = 'status-success';
676
+ if (statusCode >= 400) {
677
+ statusClass = 'status-error';
678
+ } else if (statusCode >= 300) {
679
+ statusClass = 'status-warning';
680
+ }
681
+ return <span className={statusClass}>{statusCode}</span>;
741
682
  },
742
- sortDirections: ['ascend', 'descend'],
743
- render: (text, record) => {
744
- return record.proxyType === 'response' ? text : '-';
745
- }
746
683
  },
747
684
  {
748
685
  title: '内容类型',
@@ -906,6 +843,28 @@ const InterfaceManagement = () => {
906
843
  const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
907
844
  const columns = allColumns.filter(col => visibleColumns.includes(col.key));
908
845
 
846
+ // 分组筛选和操作区域
847
+ const handleBatchToggleActive = async (active) => {
848
+ try {
849
+ setGroupActionLoading(true);
850
+ const response = await axios.patch(`/cgi-bin/interfaces?active=${active}`, {
851
+ featureId: selectedFeatureId,
852
+ group: selectedGroup
853
+ });
854
+ if (response.data && response.data.code === 0) {
855
+ message.success(`分组 "${selectedGroup}" 中的接口已成功${active ? '启用' : '禁用'}`);
856
+ fetchInterfaces();
857
+ } else {
858
+ throw new Error(response.data?.message || '批量操作失败');
859
+ }
860
+ } catch (error) {
861
+ console.error('批量操作失败:', error);
862
+ message.error(error.response?.data?.message || error.message || '批量操作失败');
863
+ } finally {
864
+ setGroupActionLoading(false);
865
+ }
866
+ };
867
+
909
868
  return (
910
869
  <AppLayout>
911
870
  <div className="interface-management-container">
@@ -959,6 +918,77 @@ const InterfaceManagement = () => {
959
918
  />
960
919
  )}
961
920
 
921
+ {/* 分组筛选和操作区域 */}
922
+ <div style={{
923
+ display: 'flex',
924
+ justifyContent: 'space-between',
925
+ alignItems: 'center',
926
+ marginBottom: 16,
927
+ padding: '12px 16px',
928
+ backgroundColor: '#f5f5f5',
929
+ borderRadius: '4px'
930
+ }}>
931
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
932
+ <span>分组筛选:</span>
933
+ <Select
934
+ value={selectedGroup}
935
+ onChange={value => setSelectedGroup(value)}
936
+ style={{ width: 200 }}
937
+ placeholder="选择分组"
938
+ allowClear
939
+ showSearch
940
+ loading={interfacesLoading}
941
+ >
942
+ {groups.map(group => (
943
+ <Option key={group} value={group}>{group}</Option>
944
+ ))}
945
+ </Select>
946
+ {selectedGroup && (
947
+ <Button
948
+ type="link"
949
+ onClick={() => setSelectedGroup(null)}
950
+ size="small"
951
+ >
952
+ 清除筛选
953
+ </Button>
954
+ )}
955
+ </div>
956
+
957
+ {selectedGroup && (
958
+ <div style={{ display: 'flex', gap: '8px' }}>
959
+ <Popconfirm
960
+ title={`确定要启用分组 "${selectedGroup}" 中的所有接口吗?`}
961
+ onConfirm={() => handleBatchToggleActive(true)}
962
+ okText="确定"
963
+ cancelText="取消"
964
+ >
965
+ <Button
966
+ type="primary"
967
+ size="small"
968
+ loading={groupActionLoading}
969
+ >
970
+ 批量启用
971
+ </Button>
972
+ </Popconfirm>
973
+
974
+ <Popconfirm
975
+ title={`确定要禁用分组 "${selectedGroup}" 中的所有接口吗?`}
976
+ onConfirm={() => handleBatchToggleActive(false)}
977
+ okText="确定"
978
+ cancelText="取消"
979
+ >
980
+ <Button
981
+ danger
982
+ size="small"
983
+ loading={groupActionLoading}
984
+ >
985
+ 批量禁用
986
+ </Button>
987
+ </Popconfirm>
988
+ </div>
989
+ )}
990
+ </div>
991
+
962
992
  {/* 接口列表状态栏 */}
963
993
  {filteredInterfaces.length > 0 && !interfacesLoading && (
964
994
  <div style={{
@@ -974,6 +1004,11 @@ const InterfaceManagement = () => {
974
1004
  }}>
975
1005
  <span>
976
1006
  当前功能模块:<strong>{selectedFeature?.name}</strong>
1007
+ {selectedGroup && (
1008
+ <span style={{ marginLeft: 8 }}>
1009
+ | 分组:<Tag color="blue">{selectedGroup}</Tag>
1010
+ </span>
1011
+ )}
977
1012
  </span>
978
1013
  <span>
979
1014
  共 {filteredInterfaces.length} 个接口,每页显示 {tableConfig.pageSize} 个
@@ -1088,6 +1123,7 @@ const InterfaceManagement = () => {
1088
1123
  layout="vertical"
1089
1124
  initialValues={{
1090
1125
  name: '',
1126
+ group: '',
1091
1127
  pattern: '',
1092
1128
  proxyType: 'response',
1093
1129
  statusCode: '200',
@@ -1096,10 +1132,11 @@ const InterfaceManagement = () => {
1096
1132
  httpMethod: 'ALL',
1097
1133
  targetUrl: '',
1098
1134
  headerItems: [],
1099
- paramMatchers: []
1135
+ paramMatchers: [],
1136
+ responseDelay: '0'
1100
1137
  }}
1101
1138
  >
1102
- {/* 基础信息表单项保持原样 */}
1139
+ {/* 基础信息表单项 */}
1103
1140
  <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1104
1141
  <div style={{ flex: 1 }}>
1105
1142
  <Form.Item
@@ -1110,6 +1147,34 @@ const InterfaceManagement = () => {
1110
1147
  <Input placeholder="请输入接口名称" />
1111
1148
  </Form.Item>
1112
1149
  </div>
1150
+ <div style={{ flex: 1 }}>
1151
+ <Form.Item
1152
+ name="group"
1153
+ label="分组"
1154
+ tooltip="为接口设置分组,便于管理和批量操作"
1155
+ >
1156
+ <Select
1157
+ placeholder="选择或创建分组"
1158
+ showSearch
1159
+ allowClear
1160
+ mode="tags"
1161
+ tokenSeparators={[',']}
1162
+ onInputKeyDown={(e) => {
1163
+ // 防止回车键提交表单
1164
+ if (e.key === 'Enter') {
1165
+ e.stopPropagation();
1166
+ }
1167
+ }}
1168
+ >
1169
+ {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1170
+ <Option key={group} value={group}>{group}</Option>
1171
+ ))}
1172
+ </Select>
1173
+ </Form.Item>
1174
+ </div>
1175
+ </div>
1176
+
1177
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1113
1178
  <div style={{ flex: 1 }}>
1114
1179
  <Form.Item
1115
1180
  name="pattern"
@@ -1189,9 +1254,6 @@ const InterfaceManagement = () => {
1189
1254
  />
1190
1255
  </Form.Item>
1191
1256
  </div>
1192
- </div>
1193
-
1194
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1195
1257
  <div style={{ flex: 1 }}>
1196
1258
  <Form.Item
1197
1259
  name="proxyType"
@@ -1219,7 +1281,7 @@ const InterfaceManagement = () => {
1219
1281
  const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
1220
1282
  if (patternInput) {
1221
1283
  if (value === 'response') {
1222
- patternInput.placeholder = "例如:/api/users,/api/users/*,/api\/users\/\d+/";
1284
+ patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
1223
1285
  } else if (value === 'redirect') {
1224
1286
  patternInput.placeholder = "例如:https://example.com/api";
1225
1287
  } else if (value === 'url_redirect') {
@@ -1233,6 +1295,9 @@ const InterfaceManagement = () => {
1233
1295
  </Select>
1234
1296
  </Form.Item>
1235
1297
  </div>
1298
+ </div>
1299
+
1300
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1236
1301
  <div style={{ flex: 1 }}>
1237
1302
  <Form.Item
1238
1303
  name="httpMethod"
@@ -1246,6 +1311,9 @@ const InterfaceManagement = () => {
1246
1311
  </Select>
1247
1312
  </Form.Item>
1248
1313
  </div>
1314
+ <div style={{ flex: 1 }}>
1315
+ {/* 这里可以留空或添加其他字段 */}
1316
+ </div>
1249
1317
  </div>
1250
1318
 
1251
1319
  {/* 根据proxyType显示不同的表单项 */}
@@ -1283,6 +1351,29 @@ const InterfaceManagement = () => {
1283
1351
  </Select>
1284
1352
  </Form.Item>
1285
1353
  </div>
1354
+ <div style={{ flex: 1 }}>
1355
+ <Form.Item
1356
+ name="responseDelay"
1357
+ label="延迟响应(毫秒)"
1358
+ tooltip="设置接口响应延迟时间,单位为毫秒"
1359
+ rules={[
1360
+ {
1361
+ pattern: /^\d*$/,
1362
+ message: '请输入有效的数字'
1363
+ },
1364
+ {
1365
+ validator: (_, value) => {
1366
+ if (!value || parseInt(value, 10) <= 60000) {
1367
+ return Promise.resolve();
1368
+ }
1369
+ return Promise.reject(new Error('延迟时间不能超过60秒(60000毫秒)'));
1370
+ }
1371
+ }
1372
+ ]}
1373
+ >
1374
+ <Input placeholder="例如:500" />
1375
+ </Form.Item>
1376
+ </div>
1286
1377
  </div>
1287
1378
 
1288
1379
  {/* 请求入参匹配设置 */}