whistle.mock-plugins 1.2.0-beta.9 → 1.2.1-beta.1

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.
@@ -10,12 +10,12 @@ import AppLayout from '../components/AppLayout';
10
10
  import {
11
11
  Table, Button, Modal, Form, Input, Select, message, Switch,
12
12
  Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col,
13
- Popover, Checkbox, Tag, Input as AntInput, Radio, Drawer, Dropdown, Menu, Collapse, Divider
13
+ Popover, Checkbox, Tag, Input as AntInput, Radio, Drawer, Dropdown, Menu, Collapse
14
14
  } from 'antd';
15
15
  import {
16
16
  PlusOutlined, EditOutlined, DeleteOutlined,
17
17
  FileTextOutlined, PlusCircleOutlined, SettingOutlined,
18
- SearchOutlined, FilterOutlined, ReloadOutlined, DownOutlined, QuestionCircleOutlined
18
+ SearchOutlined, FilterOutlined, ReloadOutlined, DownOutlined, QuestionCircleOutlined, CopyOutlined
19
19
  } from '@ant-design/icons';
20
20
  import '../styles/interface-management.css';
21
21
  import axios from 'axios';
@@ -32,6 +32,7 @@ import {
32
32
  formatResponseContent,
33
33
  generateResponseId
34
34
  } from '../components/interface-management';
35
+ import DataManagementModal from '../components/interface-management/DataManagementModal';
35
36
 
36
37
  const { Option } = Select;
37
38
  const { Search } = AntInput;
@@ -70,11 +71,13 @@ const InterfaceManagement = () => {
70
71
  const [groups, setGroups] = useState([]);
71
72
  const [selectedGroup, setSelectedGroup] = useState(null);
72
73
  const [groupActionLoading, setGroupActionLoading] = useState(false);
73
- const [newGroupName, setNewGroupName] = useState('');
74
74
 
75
75
  // 模态框状态
76
76
  const [modalVisible, setModalVisible] = useState(false);
77
77
  const [previewVisible, setPreviewVisible] = useState(false);
78
+ const [isDuplicateMode, setIsDuplicateMode] = useState(false);
79
+ const [dataModalVisible, setDataModalVisible] = useState(false);
80
+ const [dataModalRecord, setDataModalRecord] = useState(null);
78
81
 
79
82
  // 列配置状态
80
83
  const [columnConfigVisible, setColumnConfigVisible] = useState(false);
@@ -223,17 +226,6 @@ const InterfaceManagement = () => {
223
226
  setSelectedGroup(null);
224
227
  }
225
228
  };
226
-
227
- // 添加新分组
228
- const addNewGroup = () => {
229
- if (newGroupName.trim() && !groups.includes(newGroupName.trim())) {
230
- const updatedGroups = [...groups, newGroupName.trim()].sort();
231
- setGroups(updatedGroups);
232
- setNewGroupName('');
233
- return newGroupName.trim();
234
- }
235
- return null;
236
- };
237
229
 
238
230
  // 根据分组和搜索关键词筛选接口
239
231
  const getFilteredInterfaces = () => {
@@ -265,6 +257,7 @@ const InterfaceManagement = () => {
265
257
  return;
266
258
  }
267
259
  form.resetFields();
260
+ setIsDuplicateMode(false);
268
261
 
269
262
  // 创建默认响应
270
263
  const defaultResponseId = generateResponseId();
@@ -304,6 +297,7 @@ const InterfaceManagement = () => {
304
297
 
305
298
  // 重置表单
306
299
  form.resetFields();
300
+ setIsDuplicateMode(false);
307
301
 
308
302
  // 处理响应数据
309
303
  let responses = [];
@@ -363,6 +357,91 @@ const InterfaceManagement = () => {
363
357
  setModalVisible(true);
364
358
  };
365
359
 
360
+ // 复制新增接口
361
+ const handleDuplicate = (record) => {
362
+ if (!selectedFeatureId) {
363
+ message.warning('请先选择一个功能模块');
364
+ return;
365
+ }
366
+ if (!record) {
367
+ message.warning('接口数据不完整,无法复制');
368
+ return;
369
+ }
370
+
371
+ form.resetFields();
372
+
373
+ // 深拷贝源数据
374
+ const source = JSON.parse(JSON.stringify(record));
375
+
376
+ // 处理响应数据
377
+ let responses = [];
378
+ let activeResponseId = '';
379
+
380
+ if (Array.isArray(source.responses) && source.responses.length > 0) {
381
+ // 为每条响应生成新的ID,并重映射activeResponseId
382
+ const idMap = new Map();
383
+ responses = source.responses.map((resp) => {
384
+ const newId = generateResponseId();
385
+ idMap.set(resp.id, newId);
386
+ return {
387
+ id: newId,
388
+ name: resp.name || '未命名响应',
389
+ description: resp.description || '',
390
+ content: resp.content || '{}'
391
+ };
392
+ });
393
+ const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
394
+ activeResponseId = mapped || '';
395
+ } else if (source.responseContent) {
396
+ // 兼容旧数据格式
397
+ const defaultResponseId = generateResponseId();
398
+ responses = [{
399
+ id: defaultResponseId,
400
+ name: '默认响应',
401
+ description: '',
402
+ content: source.responseContent
403
+ }];
404
+ activeResponseId = defaultResponseId;
405
+ }
406
+
407
+ // 处理自定义请求头 -> 表单需要的数组结构
408
+ let headerItems = [];
409
+ if (source.headers && typeof source.headers === 'object') {
410
+ headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
411
+ headerName,
412
+ headerValue
413
+ }));
414
+ }
415
+
416
+ // 分组值保证为字符串
417
+ let groupValue = source.group;
418
+ if (Array.isArray(groupValue)) {
419
+ groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
420
+ }
421
+
422
+ // 设定表单初值
423
+ form.setFieldsValue({
424
+ name: `${source.name || '未命名接口'} - 副本`,
425
+ group: groupValue || undefined,
426
+ pattern: source.urlPattern,
427
+ proxyType: source.proxyType || 'response',
428
+ statusCode: (source.httpStatus || source.statusCode || 200).toString(),
429
+ contentType: source.contentType || 'application/json; charset=utf-8',
430
+ responses,
431
+ activeResponseId,
432
+ httpMethod: source.httpMethod || source.method || 'ALL',
433
+ targetUrl: source.targetUrl || '',
434
+ headerItems,
435
+ paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
436
+ responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
437
+ });
438
+
439
+ setCurrentResponseId(activeResponseId || null);
440
+ setEditingInterface(null); // 新增模式
441
+ setIsDuplicateMode(true);
442
+ setModalVisible(true);
443
+ };
444
+
366
445
  const handleDeleteInterface = async (id) => {
367
446
  try {
368
447
  const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
@@ -403,8 +482,9 @@ const InterfaceManagement = () => {
403
482
 
404
483
  const handleSubmit = async () => {
405
484
  try {
406
- // 表单验证
407
- const values = await form.validateFields();
485
+ // 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
486
+ await form.validateFields();
487
+ const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
408
488
  console.log('表单提交数据:', values);
409
489
 
410
490
  // 处理自定义请求头
@@ -423,19 +503,19 @@ const InterfaceManagement = () => {
423
503
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
424
504
  }
425
505
 
426
- // 构建接口数据
506
+ // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
427
507
  const interfaceData = {
428
- name: values.name,
429
- group: groupValue || '', // 确保group不为undefined
430
- urlPattern: values.pattern,
431
- proxyType: values.proxyType,
508
+ name: values.name || (editingInterface ? editingInterface.name : ''),
509
+ group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
510
+ urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
511
+ proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
432
512
  featureId: selectedFeatureId,
433
- responses: values.responses,
434
- activeResponseId: values.activeResponseId,
435
- httpStatus: parseInt(values.statusCode, 10), // 转换为数字
436
- contentType: values.contentType,
437
- responseDelay: parseInt(values.responseDelay, 10) || 0,
438
- httpMethod: values.httpMethod,
513
+ responses: values.responses || (editingInterface ? editingInterface.responses : []),
514
+ activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
515
+ httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
516
+ contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
517
+ responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
518
+ httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
439
519
  active: true
440
520
  };
441
521
 
@@ -447,13 +527,11 @@ const InterfaceManagement = () => {
447
527
  interfaceData.headers = headers;
448
528
  }
449
529
 
450
- // 添加参数匹配规则
451
- if (values.paramMatchers && Array.isArray(values.paramMatchers) && values.paramMatchers.length > 0) {
452
- // 过滤掉空的匹配规则
453
- interfaceData.paramMatchers = values.paramMatchers.filter(matcher =>
454
- matcher && matcher.paramPath && matcher.paramValue
455
- );
456
- }
530
+ // 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
531
+ const filteredParamMatchers = Array.isArray(values.paramMatchers)
532
+ ? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
533
+ : [];
534
+ interfaceData.paramMatchers = filteredParamMatchers;
457
535
 
458
536
  console.log('提交接口数据:', interfaceData);
459
537
 
@@ -494,6 +572,7 @@ const InterfaceManagement = () => {
494
572
 
495
573
  const handleCancel = () => {
496
574
  setModalVisible(false);
575
+ setIsDuplicateMode(false);
497
576
  };
498
577
 
499
578
  const handleSelectFeature = (featureId) => {
@@ -923,7 +1002,7 @@ const InterfaceManagement = () => {
923
1002
  {
924
1003
  title: '操作',
925
1004
  key: 'action',
926
- width: 180,
1005
+ width: 220,
927
1006
  render: (_, record) => (
928
1007
  <div className="action-buttons">
929
1008
  <Button
@@ -931,6 +1010,22 @@ const InterfaceManagement = () => {
931
1010
  icon={<EditOutlined />}
932
1011
  onClick={() => handleEditInterface(record)}
933
1012
  />
1013
+ {record.proxyType === 'response' && (
1014
+ <Tooltip title="数据管理">
1015
+ <Button
1016
+ type="text"
1017
+ icon={<FileTextOutlined />}
1018
+ onClick={() => { setDataModalRecord(record); setDataModalVisible(true); }}
1019
+ />
1020
+ </Tooltip>
1021
+ )}
1022
+ <Tooltip title="复制新增">
1023
+ <Button
1024
+ type="text"
1025
+ icon={<CopyOutlined />}
1026
+ onClick={() => handleDuplicate(record)}
1027
+ />
1028
+ </Tooltip>
934
1029
  <Popconfirm
935
1030
  title="确定要删除此接口吗?"
936
1031
  onConfirm={() => handleDeleteInterface(record.id)}
@@ -1283,7 +1378,7 @@ const InterfaceManagement = () => {
1283
1378
  alignItems: 'center',
1284
1379
  gap: '8px'
1285
1380
  }}>
1286
- <span>{editingInterface ? '编辑接口' : '添加接口'}</span>
1381
+ <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1287
1382
  <Badge
1288
1383
  count="配置向导"
1289
1384
  style={{
@@ -1332,8 +1427,8 @@ const InterfaceManagement = () => {
1332
1427
  <Collapse
1333
1428
  defaultActiveKey={
1334
1429
  editingInterface && editingInterface.proxyType === 'response'
1335
- ? ['3']
1336
- : ['1', '2', '3']
1430
+ ? ['3'] // mock代理场景:只展开第3步(高级配置)
1431
+ : ['1', '2', '3'] // 其他场景:展开所有步骤
1337
1432
  }
1338
1433
  style={{ marginBottom: '16px' }}
1339
1434
  ghost
@@ -1365,55 +1460,27 @@ const InterfaceManagement = () => {
1365
1460
  tooltip="为接口设置分组,便于管理和批量操作"
1366
1461
  >
1367
1462
  <Select
1368
- placeholder="选择或输入新分组名称"
1463
+ placeholder="选择或创建分组"
1369
1464
  showSearch
1370
1465
  allowClear
1371
1466
  style={{ width: '100%' }}
1372
- dropdownMatchSelectWidth={false}
1373
- filterOption={(input, option) =>
1374
- option?.children?.toLowerCase()?.indexOf(input.toLowerCase()) >= 0
1375
- }
1467
+ mode="tags"
1468
+ maxTagCount="responsive"
1469
+ maxTagTextLength={10}
1470
+ onChange={(value) => {
1471
+ // 如果值是数组(tags模式),只取第一个元素
1472
+ if (Array.isArray(value) && value.length > 0) {
1473
+ // 如果第一个元素是空字符串,则设置为undefined
1474
+ const groupValue = value[0] === '' ? undefined : value[0];
1475
+ form.setFieldsValue({ group: groupValue });
1476
+ }
1477
+ }}
1376
1478
  onInputKeyDown={(e) => {
1377
1479
  // 防止回车键提交表单
1378
1480
  if (e.key === 'Enter') {
1379
1481
  e.stopPropagation();
1380
1482
  }
1381
1483
  }}
1382
- dropdownRender={(menu) => (
1383
- <div>
1384
- {menu}
1385
- <Divider style={{ margin: '4px 0' }} />
1386
- <div style={{ display: 'flex', flexWrap: 'nowrap', padding: 8 }}>
1387
- <Input
1388
- style={{ flex: 'auto' }}
1389
- value={newGroupName}
1390
- onChange={(e) => setNewGroupName(e.target.value)}
1391
- placeholder="输入新分组名称"
1392
- onPressEnter={(e) => {
1393
- e.stopPropagation();
1394
- const groupName = addNewGroup();
1395
- if (groupName) {
1396
- form.setFieldsValue({ group: groupName });
1397
- }
1398
- }}
1399
- />
1400
- <Button
1401
- type="text"
1402
- icon={<PlusOutlined />}
1403
- style={{ flex: 'none', padding: '4px 8px', cursor: 'pointer' }}
1404
- onClick={(e) => {
1405
- e.stopPropagation();
1406
- const groupName = addNewGroup();
1407
- if (groupName) {
1408
- form.setFieldsValue({ group: groupName });
1409
- }
1410
- }}
1411
- >
1412
- 添加
1413
- </Button>
1414
- </div>
1415
- </div>
1416
- )}
1417
1484
  >
1418
1485
  {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1419
1486
  <Option key={group} value={group}>{group}</Option>
@@ -1871,6 +1938,16 @@ const InterfaceManagement = () => {
1871
1938
  onCancel={() => setPreviewVisible(false)}
1872
1939
  previewContent={previewContent}
1873
1940
  />
1941
+
1942
+ {/* 数据管理弹窗 */}
1943
+ <DataManagementModal
1944
+ visible={dataModalVisible}
1945
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
1946
+ interfaceItem={dataModalRecord}
1947
+ onSaved={() => {
1948
+ fetchInterfaces();
1949
+ }}
1950
+ />
1874
1951
  </div>
1875
1952
  </AppLayout>
1876
1953
  );
@@ -6,8 +6,8 @@
6
6
  import React, { useState, useEffect } from 'react';
7
7
  import { useHistory } from 'react-router-dom';
8
8
  import AppLayout from '../components/AppLayout';
9
- import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Tag, List } from 'antd';
10
- import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
9
+ import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination } from 'antd';
10
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined } from '@ant-design/icons';
11
11
  import '../styles/mock-data.css';
12
12
  import axios from 'axios';
13
13
 
@@ -34,12 +34,6 @@ const MockData = () => {
34
34
  const [loading, setLoading] = useState(true);
35
35
  const [form] = Form.useForm();
36
36
 
37
- // 视图模式配置(卡片/列表)
38
- const [viewMode, setViewMode] = useState(() => {
39
- const savedViewMode = localStorage.getItem('feature-view-mode');
40
- return savedViewMode === 'list' ? 'list' : 'card'; // 默认为卡片视图
41
- });
42
-
43
37
  // 排序和分页配置(支持缓存)
44
38
  const [listConfig, setListConfig] = useState(() => {
45
39
  const cached = localStorage.getItem('feature-list-config');
@@ -57,13 +51,6 @@ const MockData = () => {
57
51
  setListConfig(newConfig);
58
52
  localStorage.setItem('feature-list-config', JSON.stringify(newConfig));
59
53
  };
60
-
61
- // 切换视图模式(卡片/列表)
62
- const toggleViewMode = () => {
63
- const newMode = viewMode === 'card' ? 'list' : 'card';
64
- setViewMode(newMode);
65
- localStorage.setItem('feature-view-mode', newMode);
66
- };
67
54
 
68
55
  // 加载功能列表
69
56
  useEffect(() => {
@@ -426,98 +413,6 @@ const MockData = () => {
426
413
  return sortedFeatures.slice(startIndex, endIndex);
427
414
  };
428
415
 
429
- // 渲染功能模块列表项
430
- const renderFeatureListItem = (feature) => {
431
- const formattedDate = feature.createdAt
432
- ? new Date(feature.createdAt).toLocaleDateString()
433
- : '未知日期';
434
-
435
- return (
436
- <div
437
- key={feature.id}
438
- className={`feature-list-item ${!feature.active ? 'inactive-feature' : ''}`}
439
- onClick={() => viewInterfaces(feature)}
440
- >
441
- <div className="feature-list-content">
442
- <div className="feature-list-main">
443
- <div className="feature-list-name">
444
- <h3>{feature.name}</h3>
445
- {!feature.active && <Tag color="red">已禁用</Tag>}
446
- {feature.active && <Tag color="green">已启用</Tag>}
447
- </div>
448
- <div className="feature-list-description">
449
- {feature.description || '无描述'}
450
- </div>
451
- </div>
452
-
453
- <div className="feature-list-meta">
454
- <div className="feature-list-stats">
455
- <span><InfoCircleOutlined /> {feature.interfaceCount || 0} 个接口</span>
456
- <span><CalendarOutlined /> {formattedDate}</span>
457
- </div>
458
- </div>
459
- </div>
460
-
461
- <div className="feature-list-actions">
462
- <Switch
463
- checked={feature.active}
464
- onChange={(checked, e) => {
465
- e.stopPropagation();
466
- handleToggleActive(feature.id, feature.active);
467
- }}
468
- onClick={(e) => e.stopPropagation()}
469
- size="small"
470
- />
471
- <Tooltip title="管理接口">
472
- <Button
473
- icon={<ApiOutlined />}
474
- size="small"
475
- type="text"
476
- onClick={(e) => {
477
- e.stopPropagation();
478
- viewInterfaces(feature);
479
- }}
480
- />
481
- </Tooltip>
482
- <Tooltip title="编辑功能">
483
- <Button
484
- icon={<EditOutlined />}
485
- size="small"
486
- type="text"
487
- onClick={(e) => {
488
- e.stopPropagation();
489
- openModal(feature);
490
- }}
491
- />
492
- </Tooltip>
493
- <Tooltip title="删除功能">
494
- <Button
495
- icon={<DeleteOutlined />}
496
- size="small"
497
- type="text"
498
- danger
499
- onClick={(e) => {
500
- e.stopPropagation();
501
- deleteFeature(feature.id);
502
- }}
503
- />
504
- </Tooltip>
505
- <Tooltip title="导出配置">
506
- <Button
507
- icon={<ExportOutlined />}
508
- size="small"
509
- type="text"
510
- onClick={(e) => {
511
- e.stopPropagation();
512
- exportFeatureConfig(feature);
513
- }}
514
- />
515
- </Tooltip>
516
- </div>
517
- </div>
518
- );
519
- };
520
-
521
416
  // 渲染功能模块卡片
522
417
  const renderFeatureCard = (feature) => {
523
418
  const formattedDate = feature.createdAt
@@ -722,7 +617,7 @@ const MockData = () => {
722
617
  </Col>
723
618
 
724
619
  {/* 分页控制区域 */}
725
- <Col xs={24} sm={12} md={5} lg={6}>
620
+ <Col xs={24} sm={24} md={8} lg={9}>
726
621
  <div className="pagination-controls">
727
622
  <div className="control-group">
728
623
  <span className="control-label">每页显示</span>
@@ -743,22 +638,6 @@ const MockData = () => {
743
638
  </div>
744
639
  </div>
745
640
  </Col>
746
-
747
- {/* 视图切换按钮区域 */}
748
- <Col xs={24} sm={12} md={3} lg={3}>
749
- <div className="view-toggle-controls">
750
- <Tooltip title={viewMode === 'card' ? '切换到列表视图' : '切换到卡片视图'}>
751
- <Button
752
- type="default"
753
- icon={viewMode === 'card' ? <UnorderedListOutlined /> : <AppstoreOutlined />}
754
- onClick={toggleViewMode}
755
- className="view-toggle-button"
756
- >
757
- {viewMode === 'card' ? '列表视图' : '卡片视图'}
758
- </Button>
759
- </Tooltip>
760
- </div>
761
- </Col>
762
641
  </Row>
763
642
  </Card>
764
643
  )}
@@ -771,16 +650,9 @@ const MockData = () => {
771
650
  </div>
772
651
  ) : mockFeatures.length > 0 ? (
773
652
  <>
774
- {/* 根据视图模式渲染不同的视图 */}
775
- {viewMode === 'card' ? (
776
- <Row gutter={[20, 20]} className="feature-grid">
777
- {getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
778
- </Row>
779
- ) : (
780
- <div className="feature-list-view">
781
- {getPaginatedFeatures().map(feature => renderFeatureListItem(feature))}
782
- </div>
783
- )}
653
+ <Row gutter={[20, 20]} className="feature-grid">
654
+ {getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
655
+ </Row>
784
656
 
785
657
  {/* 分页组件 */}
786
658
  {mockFeatures.length > listConfig.pageSize && (