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

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.
@@ -156,5 +156,3 @@
156
156
  /**![setting](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkyNC44IDYyNS43bC02NS41LTU2YzMuMS0xOSA0LjctMzguNCA0LjctNTcuOHMtMS42LTM4LjgtNC43LTU3LjhsNjUuNS01NmEzMi4wMyAzMi4wMyAwIDAwOS4zLTM1LjJsLS45LTIuNmE0NDMuNzQgNDQzLjc0IDAgMDAtNzkuNy0xMzcuOWwtMS44LTIuMWEzMi4xMiAzMi4xMiAwIDAwLTM1LjEtOS41bC04MS4zIDI4LjljLTMwLTI0LjYtNjMuNS00NC05OS43LTU3LjZsLTE1LjctODVhMzIuMDUgMzIuMDUgMCAwMC0yNS44LTI1LjdsLTIuNy0uNWMtNTIuMS05LjQtMTA2LjktOS40LTE1OSAwbC0yLjcuNWEzMi4wNSAzMi4wNSAwIDAwLTI1LjggMjUuN2wtMTUuOCA4NS40YTM1MS44NiAzNTEuODYgMCAwMC05OSA1Ny40bC04MS45LTI5LjFhMzIgMzIgMCAwMC0zNS4xIDkuNWwtMS44IDIuMWE0NDYuMDIgNDQ2LjAyIDAgMDAtNzkuNyAxMzcuOWwtLjkgMi42Yy00LjUgMTIuNS0uOCAyNi41IDkuMyAzNS4ybDY2LjMgNTYuNmMtMy4xIDE4LjgtNC42IDM4LTQuNiA1Ny4xIDAgMTkuMiAxLjUgMzguNCA0LjYgNTcuMUw5OSA2MjUuNWEzMi4wMyAzMi4wMyAwIDAwLTkuMyAzNS4ybC45IDIuNmMxOC4xIDUwLjQgNDQuOSA5Ni45IDc5LjcgMTM3LjlsMS44IDIuMWEzMi4xMiAzMi4xMiAwIDAwMzUuMSA5LjVsODEuOS0yOS4xYzI5LjggMjQuNSA2My4xIDQzLjkgOTkgNTcuNGwxNS44IDg1LjRhMzIuMDUgMzIuMDUgMCAwMDI1LjggMjUuN2wyLjcuNWE0NDkuNCA0NDkuNCAwIDAwMTU5IDBsMi43LS41YTMyLjA1IDMyLjA1IDAgMDAyNS44LTI1LjdsMTUuNy04NWEzNTAgMzUwIDAgMDA5OS43LTU3LjZsODEuMyAyOC45YTMyIDMyIDAgMDAzNS4xLTkuNWwxLjgtMi4xYzM0LjgtNDEuMSA2MS42LTg3LjUgNzkuNy0xMzcuOWwuOS0yLjZjNC41LTEyLjMuOC0yNi4zLTkuMy0zNXpNNzg4LjMgNDY1LjljMi41IDE1LjEgMy44IDMwLjYgMy44IDQ2LjFzLTEuMyAzMS0zLjggNDYuMWwtNi42IDQwLjEgNzQuNyA2My45YTM3MC4wMyAzNzAuMDMgMCAwMS00Mi42IDczLjZMNzIxIDcwMi44bC0zMS40IDI1LjhjLTIzLjkgMTkuNi01MC41IDM1LTc5LjMgNDUuOGwtMzguMSAxNC4zLTE3LjkgOTdhMzc3LjUgMzc3LjUgMCAwMS04NSAwbC0xNy45LTk3LjItMzcuOC0xNC41Yy0yOC41LTEwLjgtNTUtMjYuMi03OC43LTQ1LjdsLTMxLjQtMjUuOS05My40IDMzLjJjLTE3LTIyLjktMzEuMi00Ny42LTQyLjYtNzMuNmw3NS41LTY0LjUtNi41LTQwYy0yLjQtMTQuOS0zLjctMzAuMy0zLjctNDUuNSAwLTE1LjMgMS4yLTMwLjYgMy43LTQ1LjVsNi41LTQwLTc1LjUtNjQuNWMxMS4zLTI2LjEgMjUuNi01MC43IDQyLjYtNzMuNmw5My40IDMzLjIgMzEuNC0yNS45YzIzLjctMTkuNSA1MC4yLTM0LjkgNzguNy00NS43bDM3LjktMTQuMyAxNy45LTk3LjJjMjguMS0zLjIgNTYuOC0zLjIgODUgMGwxNy45IDk3IDM4LjEgMTQuM2MyOC43IDEwLjggNTUuNCAyNi4yIDc5LjMgNDUuOGwzMS40IDI1LjggOTIuOC0zMi45YzE3IDIyLjkgMzEuMiA0Ny42IDQyLjYgNzMuNkw3ODEuOCA0MjZsNi41IDM5Ljl6TTUxMiAzMjZjLTk3LjIgMC0xNzYgNzguOC0xNzYgMTc2czc4LjggMTc2IDE3NiAxNzYgMTc2LTc4LjggMTc2LTE3Ni03OC44LTE3Ni0xNzYtMTc2em03OS4yIDI1NS4yQTExMS42IDExMS42IDAgMDE1MTIgNjE0Yy0yOS45IDAtNTgtMTEuNy03OS4yLTMyLjhBMTExLjYgMTExLjYgMCAwMTQwMCA1MDJjMC0yOS45IDExLjctNTggMzIuOC03OS4yQzQ1NCA0MDEuNiA0ODIuMSAzOTAgNTEyIDM5MGMyOS45IDAgNTggMTEuNiA3OS4yIDMyLjhBMTExLjYgMTExLjYgMCAwMTYyNCA1MDJjMCAyOS45LTExLjcgNTgtMzIuOCA3OS4yeiIgLz48L3N2Zz4=) */
157
157
 
158
158
  /**![sort-ascending](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTgzOS42IDQzMy44TDc0OSAxNTAuNWE5LjI0IDkuMjQgMCAwMC04LjktNi41aC03Ny40Yy00LjEgMC03LjYgMi42LTguOSA2LjVsLTkxLjMgMjgzLjNjLS4zLjktLjUgMS45LS41IDIuOSAwIDUuMSA0LjIgOS4zIDkuMyA5LjNoNTYuNGM0LjIgMCA3LjgtMi44IDktNi44bDE3LjUtNjEuNmg4OWwxNy4zIDYxLjVjMS4xIDQgNC44IDYuOCA5IDYuOGg2MS4yYzEgMCAxLjktLjEgMi44LS40IDIuNC0uOCA0LjMtMi40IDUuNS00LjYgMS4xLTIuMiAxLjMtNC43LjYtNy4xek02NjMuMyAzMjUuNWwzMi44LTExNi45aDYuM2wzMi4xIDExNi45aC03MS4yem0xNDMuNSA0OTIuOUg2NzcuMnYtLjRsMTMyLjYtMTg4LjljMS4xLTEuNiAxLjctMy40IDEuNy01LjR2LTM2LjRjMC01LjEtNC4yLTkuMy05LjMtOS4zaC0yMDRjLTUuMSAwLTkuMyA0LjItOS4zIDkuM3Y0M2MwIDUuMSA0LjIgOS4zIDkuMyA5LjNoMTIyLjZ2LjRMNTg3LjcgODI4LjlhOS4zNSA5LjM1IDAgMDAtMS43IDUuNHYzNi40YzAgNS4xIDQuMiA5LjMgOS4zIDkuM2gyMTEuNGM1LjEgMCA5LjMtNC4yIDkuMy05LjN2LTQzYTkuMiA5LjIgMCAwMC05LjItOS4zek00MTYgNzAyaC03NlYxNzJjMC00LjQtMy42LTgtOC04aC01NmMtNC40IDAtOCAzLjYtOCA4djUzMGgtNzZjLTYuNyAwLTEwLjUgNy44LTYuMyAxM2wxMTIgMTQxLjlhOCA4IDAgMDAxMi42IDBsMTEyLTE0MS45YzQuMS01LjIuNC0xMy02LjMtMTN6IiAvPjwvc3ZnPg==) */
159
-
160
- /**![unordered-list](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkxMiAxOTJIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHptMCAyODRIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHptMCAyODRIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHpNMTA0IDIyOGE1NiA1NiAwIDEwMTEyIDAgNTYgNTYgMCAxMC0xMTIgMHptMCAyODRhNTYgNTYgMCAxMDExMiAwIDU2IDU2IDAgMTAtMTEyIDB6bTAgMjg0YTU2IDU2IDAgMTAxMTIgMCA1NiA1NiAwIDEwLTExMiAweiIgLz48L3N2Zz4=) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.2.0-beta.9",
3
+ "version": "1.2.0",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -4,12 +4,12 @@ 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, Tag, Input as AntInput, Radio, Drawer, Divider
7
+ Popover, Checkbox, Tag, Input as AntInput, Radio, Drawer
8
8
  } from 'antd';
9
9
  import {
10
10
  PlusOutlined, EditOutlined, DeleteOutlined,
11
11
  FileTextOutlined, PlusCircleOutlined, SettingOutlined,
12
- SearchOutlined, FilterOutlined, ReloadOutlined
12
+ SearchOutlined, FilterOutlined, ReloadOutlined, CopyOutlined
13
13
  } from '@ant-design/icons';
14
14
  import '../styles/interface-management.css';
15
15
  import axios from 'axios';
@@ -64,11 +64,11 @@ const InterfaceManagement = () => {
64
64
  const [groups, setGroups] = useState([]);
65
65
  const [selectedGroup, setSelectedGroup] = useState(null);
66
66
  const [groupActionLoading, setGroupActionLoading] = useState(false);
67
- const [newGroupName, setNewGroupName] = useState('');
68
67
 
69
68
  // 模态框状态
70
69
  const [modalVisible, setModalVisible] = useState(false);
71
70
  const [previewVisible, setPreviewVisible] = useState(false);
71
+ const [isDuplicateMode, setIsDuplicateMode] = useState(false);
72
72
 
73
73
  // 列配置状态
74
74
  const [columnConfigVisible, setColumnConfigVisible] = useState(false);
@@ -217,17 +217,6 @@ const InterfaceManagement = () => {
217
217
  setSelectedGroup(null);
218
218
  }
219
219
  };
220
-
221
- // 添加新分组
222
- const addNewGroup = () => {
223
- if (newGroupName.trim() && !groups.includes(newGroupName.trim())) {
224
- const updatedGroups = [...groups, newGroupName.trim()].sort();
225
- setGroups(updatedGroups);
226
- setNewGroupName('');
227
- return newGroupName.trim();
228
- }
229
- return null;
230
- };
231
220
 
232
221
  // 根据分组和搜索关键词筛选接口
233
222
  const getFilteredInterfaces = () => {
@@ -259,6 +248,7 @@ const InterfaceManagement = () => {
259
248
  return;
260
249
  }
261
250
  form.resetFields();
251
+ setIsDuplicateMode(false);
262
252
 
263
253
  // 创建默认响应
264
254
  const defaultResponseId = generateResponseId();
@@ -298,6 +288,7 @@ const InterfaceManagement = () => {
298
288
 
299
289
  // 重置表单
300
290
  form.resetFields();
291
+ setIsDuplicateMode(false);
301
292
 
302
293
  // 处理响应数据
303
294
  let responses = [];
@@ -357,6 +348,91 @@ const InterfaceManagement = () => {
357
348
  setModalVisible(true);
358
349
  };
359
350
 
351
+ // 复制新增接口
352
+ const handleDuplicate = (record) => {
353
+ if (!selectedFeatureId) {
354
+ message.warning('请先选择一个功能模块');
355
+ return;
356
+ }
357
+ if (!record) {
358
+ message.warning('接口数据不完整,无法复制');
359
+ return;
360
+ }
361
+
362
+ form.resetFields();
363
+
364
+ // 深拷贝源数据
365
+ const source = JSON.parse(JSON.stringify(record));
366
+
367
+ // 处理响应数据
368
+ let responses = [];
369
+ let activeResponseId = '';
370
+
371
+ if (Array.isArray(source.responses) && source.responses.length > 0) {
372
+ // 为每条响应生成新的ID,并重映射activeResponseId
373
+ const idMap = new Map();
374
+ responses = source.responses.map((resp) => {
375
+ const newId = generateResponseId();
376
+ idMap.set(resp.id, newId);
377
+ return {
378
+ id: newId,
379
+ name: resp.name || '未命名响应',
380
+ description: resp.description || '',
381
+ content: resp.content || '{}'
382
+ };
383
+ });
384
+ const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
385
+ activeResponseId = mapped || '';
386
+ } else if (source.responseContent) {
387
+ // 兼容旧数据格式
388
+ const defaultResponseId = generateResponseId();
389
+ responses = [{
390
+ id: defaultResponseId,
391
+ name: '默认响应',
392
+ description: '',
393
+ content: source.responseContent
394
+ }];
395
+ activeResponseId = defaultResponseId;
396
+ }
397
+
398
+ // 处理自定义请求头 -> 表单需要的数组结构
399
+ let headerItems = [];
400
+ if (source.headers && typeof source.headers === 'object') {
401
+ headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
402
+ headerName,
403
+ headerValue
404
+ }));
405
+ }
406
+
407
+ // 分组值保证为字符串
408
+ let groupValue = source.group;
409
+ if (Array.isArray(groupValue)) {
410
+ groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
411
+ }
412
+
413
+ // 设定表单初值
414
+ form.setFieldsValue({
415
+ name: `${source.name || '未命名接口'} - 副本`,
416
+ group: groupValue || undefined,
417
+ pattern: source.urlPattern,
418
+ proxyType: source.proxyType || 'response',
419
+ statusCode: (source.httpStatus || source.statusCode || 200).toString(),
420
+ contentType: source.contentType || 'application/json; charset=utf-8',
421
+ responses,
422
+ activeResponseId,
423
+ httpMethod: source.httpMethod || source.method || 'ALL',
424
+ targetUrl: source.targetUrl || '',
425
+ headerItems,
426
+ paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
427
+ responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
428
+ });
429
+
430
+ setCurrentResponseId(activeResponseId || null);
431
+ setEditingInterface(null); // 新增模式
432
+ setIsDuplicateMode(true);
433
+ setModalVisible(true);
434
+ };
435
+
360
436
  const handleDeleteInterface = async (id) => {
361
437
  try {
362
438
  const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
@@ -397,8 +473,9 @@ const InterfaceManagement = () => {
397
473
 
398
474
  const handleSubmit = async () => {
399
475
  try {
400
- // 表单验证
401
- const values = await form.validateFields();
476
+ // 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
477
+ await form.validateFields();
478
+ const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
402
479
  console.log('表单提交数据:', values);
403
480
 
404
481
  // 处理自定义请求头
@@ -417,19 +494,19 @@ const InterfaceManagement = () => {
417
494
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
418
495
  }
419
496
 
420
- // 构建接口数据
497
+ // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
421
498
  const interfaceData = {
422
- name: values.name,
423
- group: groupValue || '', // 确保group不为undefined
424
- urlPattern: values.pattern,
425
- proxyType: values.proxyType,
499
+ name: values.name || (editingInterface ? editingInterface.name : ''),
500
+ group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
501
+ urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
502
+ proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
426
503
  featureId: selectedFeatureId,
427
- responses: values.responses,
428
- activeResponseId: values.activeResponseId,
429
- httpStatus: parseInt(values.statusCode, 10), // 转换为数字
430
- contentType: values.contentType,
431
- responseDelay: parseInt(values.responseDelay, 10) || 0,
432
- httpMethod: values.httpMethod,
504
+ responses: values.responses || (editingInterface ? editingInterface.responses : []),
505
+ activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
506
+ httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
507
+ contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
508
+ responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
509
+ httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
433
510
  active: true
434
511
  };
435
512
 
@@ -441,13 +518,11 @@ const InterfaceManagement = () => {
441
518
  interfaceData.headers = headers;
442
519
  }
443
520
 
444
- // 添加参数匹配规则
445
- if (values.paramMatchers && Array.isArray(values.paramMatchers) && values.paramMatchers.length > 0) {
446
- // 过滤掉空的匹配规则
447
- interfaceData.paramMatchers = values.paramMatchers.filter(matcher =>
448
- matcher && matcher.paramPath && matcher.paramValue
449
- );
450
- }
521
+ // 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
522
+ const filteredParamMatchers = Array.isArray(values.paramMatchers)
523
+ ? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
524
+ : [];
525
+ interfaceData.paramMatchers = filteredParamMatchers;
451
526
 
452
527
  console.log('提交接口数据:', interfaceData);
453
528
 
@@ -488,6 +563,7 @@ const InterfaceManagement = () => {
488
563
 
489
564
  const handleCancel = () => {
490
565
  setModalVisible(false);
566
+ setIsDuplicateMode(false);
491
567
  };
492
568
 
493
569
  const handleSelectFeature = (featureId) => {
@@ -917,7 +993,7 @@ const InterfaceManagement = () => {
917
993
  {
918
994
  title: '操作',
919
995
  key: 'action',
920
- width: 180,
996
+ width: 220,
921
997
  render: (_, record) => (
922
998
  <div className="action-buttons">
923
999
  <Button
@@ -925,6 +1001,13 @@ const InterfaceManagement = () => {
925
1001
  icon={<EditOutlined />}
926
1002
  onClick={() => handleEditInterface(record)}
927
1003
  />
1004
+ <Tooltip title="复制新增">
1005
+ <Button
1006
+ type="text"
1007
+ icon={<CopyOutlined />}
1008
+ onClick={() => handleDuplicate(record)}
1009
+ />
1010
+ </Tooltip>
928
1011
  <Popconfirm
929
1012
  title="确定要删除此接口吗?"
930
1013
  onConfirm={() => handleDeleteInterface(record.id)}
@@ -1264,7 +1347,7 @@ const InterfaceManagement = () => {
1264
1347
 
1265
1348
  {/* 接口编辑/创建模态框 */}
1266
1349
  <Modal
1267
- title={editingInterface ? '编辑接口' : '添加接口'}
1350
+ title={editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}
1268
1351
  open={modalVisible}
1269
1352
  onOk={handleSubmit}
1270
1353
  onCancel={handleCancel}
@@ -1310,55 +1393,27 @@ const InterfaceManagement = () => {
1310
1393
  tooltip="为接口设置分组,便于管理和批量操作"
1311
1394
  >
1312
1395
  <Select
1313
- placeholder="选择或输入新分组名称"
1396
+ placeholder="选择或创建分组"
1314
1397
  showSearch
1315
1398
  allowClear
1316
1399
  style={{ width: '100%' }}
1317
- dropdownMatchSelectWidth={false}
1318
- filterOption={(input, option) =>
1319
- option?.children?.toLowerCase()?.indexOf(input.toLowerCase()) >= 0
1320
- }
1400
+ mode="tags"
1401
+ maxTagCount="responsive"
1402
+ maxTagTextLength={10}
1403
+ onChange={(value) => {
1404
+ // 如果值是数组(tags模式),只取第一个元素
1405
+ if (Array.isArray(value) && value.length > 0) {
1406
+ // 如果第一个元素是空字符串,则设置为undefined
1407
+ const groupValue = value[0] === '' ? undefined : value[0];
1408
+ form.setFieldsValue({ group: groupValue });
1409
+ }
1410
+ }}
1321
1411
  onInputKeyDown={(e) => {
1322
1412
  // 防止回车键提交表单
1323
1413
  if (e.key === 'Enter') {
1324
1414
  e.stopPropagation();
1325
1415
  }
1326
1416
  }}
1327
- dropdownRender={(menu) => (
1328
- <div>
1329
- {menu}
1330
- <Divider style={{ margin: '4px 0' }} />
1331
- <div style={{ display: 'flex', flexWrap: 'nowrap', padding: 8 }}>
1332
- <Input
1333
- style={{ flex: 'auto' }}
1334
- value={newGroupName}
1335
- onChange={(e) => setNewGroupName(e.target.value)}
1336
- placeholder="输入新分组名称"
1337
- onPressEnter={(e) => {
1338
- e.stopPropagation();
1339
- const groupName = addNewGroup();
1340
- if (groupName) {
1341
- form.setFieldsValue({ group: groupName });
1342
- }
1343
- }}
1344
- />
1345
- <Button
1346
- type="text"
1347
- icon={<PlusOutlined />}
1348
- style={{ flex: 'none', padding: '4px 8px', cursor: 'pointer' }}
1349
- onClick={(e) => {
1350
- e.stopPropagation();
1351
- const groupName = addNewGroup();
1352
- if (groupName) {
1353
- form.setFieldsValue({ group: groupName });
1354
- }
1355
- }}
1356
- >
1357
- 添加
1358
- </Button>
1359
- </div>
1360
- </div>
1361
- )}
1362
1417
  >
1363
1418
  {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1364
1419
  <Option key={group} value={group}>{group}</Option>
@@ -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';
@@ -70,11 +70,11 @@ const InterfaceManagement = () => {
70
70
  const [groups, setGroups] = useState([]);
71
71
  const [selectedGroup, setSelectedGroup] = useState(null);
72
72
  const [groupActionLoading, setGroupActionLoading] = useState(false);
73
- const [newGroupName, setNewGroupName] = useState('');
74
73
 
75
74
  // 模态框状态
76
75
  const [modalVisible, setModalVisible] = useState(false);
77
76
  const [previewVisible, setPreviewVisible] = useState(false);
77
+ const [isDuplicateMode, setIsDuplicateMode] = useState(false);
78
78
 
79
79
  // 列配置状态
80
80
  const [columnConfigVisible, setColumnConfigVisible] = useState(false);
@@ -223,17 +223,6 @@ const InterfaceManagement = () => {
223
223
  setSelectedGroup(null);
224
224
  }
225
225
  };
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
226
 
238
227
  // 根据分组和搜索关键词筛选接口
239
228
  const getFilteredInterfaces = () => {
@@ -265,6 +254,7 @@ const InterfaceManagement = () => {
265
254
  return;
266
255
  }
267
256
  form.resetFields();
257
+ setIsDuplicateMode(false);
268
258
 
269
259
  // 创建默认响应
270
260
  const defaultResponseId = generateResponseId();
@@ -304,6 +294,7 @@ const InterfaceManagement = () => {
304
294
 
305
295
  // 重置表单
306
296
  form.resetFields();
297
+ setIsDuplicateMode(false);
307
298
 
308
299
  // 处理响应数据
309
300
  let responses = [];
@@ -363,6 +354,91 @@ const InterfaceManagement = () => {
363
354
  setModalVisible(true);
364
355
  };
365
356
 
357
+ // 复制新增接口
358
+ const handleDuplicate = (record) => {
359
+ if (!selectedFeatureId) {
360
+ message.warning('请先选择一个功能模块');
361
+ return;
362
+ }
363
+ if (!record) {
364
+ message.warning('接口数据不完整,无法复制');
365
+ return;
366
+ }
367
+
368
+ form.resetFields();
369
+
370
+ // 深拷贝源数据
371
+ const source = JSON.parse(JSON.stringify(record));
372
+
373
+ // 处理响应数据
374
+ let responses = [];
375
+ let activeResponseId = '';
376
+
377
+ if (Array.isArray(source.responses) && source.responses.length > 0) {
378
+ // 为每条响应生成新的ID,并重映射activeResponseId
379
+ const idMap = new Map();
380
+ responses = source.responses.map((resp) => {
381
+ const newId = generateResponseId();
382
+ idMap.set(resp.id, newId);
383
+ return {
384
+ id: newId,
385
+ name: resp.name || '未命名响应',
386
+ description: resp.description || '',
387
+ content: resp.content || '{}'
388
+ };
389
+ });
390
+ const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
391
+ activeResponseId = mapped || '';
392
+ } else if (source.responseContent) {
393
+ // 兼容旧数据格式
394
+ const defaultResponseId = generateResponseId();
395
+ responses = [{
396
+ id: defaultResponseId,
397
+ name: '默认响应',
398
+ description: '',
399
+ content: source.responseContent
400
+ }];
401
+ activeResponseId = defaultResponseId;
402
+ }
403
+
404
+ // 处理自定义请求头 -> 表单需要的数组结构
405
+ let headerItems = [];
406
+ if (source.headers && typeof source.headers === 'object') {
407
+ headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
408
+ headerName,
409
+ headerValue
410
+ }));
411
+ }
412
+
413
+ // 分组值保证为字符串
414
+ let groupValue = source.group;
415
+ if (Array.isArray(groupValue)) {
416
+ groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
417
+ }
418
+
419
+ // 设定表单初值
420
+ form.setFieldsValue({
421
+ name: `${source.name || '未命名接口'} - 副本`,
422
+ group: groupValue || undefined,
423
+ pattern: source.urlPattern,
424
+ proxyType: source.proxyType || 'response',
425
+ statusCode: (source.httpStatus || source.statusCode || 200).toString(),
426
+ contentType: source.contentType || 'application/json; charset=utf-8',
427
+ responses,
428
+ activeResponseId,
429
+ httpMethod: source.httpMethod || source.method || 'ALL',
430
+ targetUrl: source.targetUrl || '',
431
+ headerItems,
432
+ paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
433
+ responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
434
+ });
435
+
436
+ setCurrentResponseId(activeResponseId || null);
437
+ setEditingInterface(null); // 新增模式
438
+ setIsDuplicateMode(true);
439
+ setModalVisible(true);
440
+ };
441
+
366
442
  const handleDeleteInterface = async (id) => {
367
443
  try {
368
444
  const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
@@ -403,8 +479,9 @@ const InterfaceManagement = () => {
403
479
 
404
480
  const handleSubmit = async () => {
405
481
  try {
406
- // 表单验证
407
- const values = await form.validateFields();
482
+ // 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
483
+ await form.validateFields();
484
+ const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
408
485
  console.log('表单提交数据:', values);
409
486
 
410
487
  // 处理自定义请求头
@@ -423,19 +500,19 @@ const InterfaceManagement = () => {
423
500
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
424
501
  }
425
502
 
426
- // 构建接口数据
503
+ // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
427
504
  const interfaceData = {
428
- name: values.name,
429
- group: groupValue || '', // 确保group不为undefined
430
- urlPattern: values.pattern,
431
- proxyType: values.proxyType,
505
+ name: values.name || (editingInterface ? editingInterface.name : ''),
506
+ group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
507
+ urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
508
+ proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
432
509
  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,
510
+ responses: values.responses || (editingInterface ? editingInterface.responses : []),
511
+ activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
512
+ httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
513
+ contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
514
+ responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
515
+ httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
439
516
  active: true
440
517
  };
441
518
 
@@ -447,13 +524,11 @@ const InterfaceManagement = () => {
447
524
  interfaceData.headers = headers;
448
525
  }
449
526
 
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
- }
527
+ // 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
528
+ const filteredParamMatchers = Array.isArray(values.paramMatchers)
529
+ ? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
530
+ : [];
531
+ interfaceData.paramMatchers = filteredParamMatchers;
457
532
 
458
533
  console.log('提交接口数据:', interfaceData);
459
534
 
@@ -494,6 +569,7 @@ const InterfaceManagement = () => {
494
569
 
495
570
  const handleCancel = () => {
496
571
  setModalVisible(false);
572
+ setIsDuplicateMode(false);
497
573
  };
498
574
 
499
575
  const handleSelectFeature = (featureId) => {
@@ -923,7 +999,7 @@ const InterfaceManagement = () => {
923
999
  {
924
1000
  title: '操作',
925
1001
  key: 'action',
926
- width: 180,
1002
+ width: 220,
927
1003
  render: (_, record) => (
928
1004
  <div className="action-buttons">
929
1005
  <Button
@@ -931,6 +1007,13 @@ const InterfaceManagement = () => {
931
1007
  icon={<EditOutlined />}
932
1008
  onClick={() => handleEditInterface(record)}
933
1009
  />
1010
+ <Tooltip title="复制新增">
1011
+ <Button
1012
+ type="text"
1013
+ icon={<CopyOutlined />}
1014
+ onClick={() => handleDuplicate(record)}
1015
+ />
1016
+ </Tooltip>
934
1017
  <Popconfirm
935
1018
  title="确定要删除此接口吗?"
936
1019
  onConfirm={() => handleDeleteInterface(record.id)}
@@ -1283,7 +1366,7 @@ const InterfaceManagement = () => {
1283
1366
  alignItems: 'center',
1284
1367
  gap: '8px'
1285
1368
  }}>
1286
- <span>{editingInterface ? '编辑接口' : '添加接口'}</span>
1369
+ <span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
1287
1370
  <Badge
1288
1371
  count="配置向导"
1289
1372
  style={{
@@ -1332,8 +1415,8 @@ const InterfaceManagement = () => {
1332
1415
  <Collapse
1333
1416
  defaultActiveKey={
1334
1417
  editingInterface && editingInterface.proxyType === 'response'
1335
- ? ['3']
1336
- : ['1', '2', '3']
1418
+ ? ['3'] // mock代理场景:只展开第3步(高级配置)
1419
+ : ['1', '2', '3'] // 其他场景:展开所有步骤
1337
1420
  }
1338
1421
  style={{ marginBottom: '16px' }}
1339
1422
  ghost
@@ -1365,55 +1448,27 @@ const InterfaceManagement = () => {
1365
1448
  tooltip="为接口设置分组,便于管理和批量操作"
1366
1449
  >
1367
1450
  <Select
1368
- placeholder="选择或输入新分组名称"
1451
+ placeholder="选择或创建分组"
1369
1452
  showSearch
1370
1453
  allowClear
1371
1454
  style={{ width: '100%' }}
1372
- dropdownMatchSelectWidth={false}
1373
- filterOption={(input, option) =>
1374
- option?.children?.toLowerCase()?.indexOf(input.toLowerCase()) >= 0
1375
- }
1455
+ mode="tags"
1456
+ maxTagCount="responsive"
1457
+ maxTagTextLength={10}
1458
+ onChange={(value) => {
1459
+ // 如果值是数组(tags模式),只取第一个元素
1460
+ if (Array.isArray(value) && value.length > 0) {
1461
+ // 如果第一个元素是空字符串,则设置为undefined
1462
+ const groupValue = value[0] === '' ? undefined : value[0];
1463
+ form.setFieldsValue({ group: groupValue });
1464
+ }
1465
+ }}
1376
1466
  onInputKeyDown={(e) => {
1377
1467
  // 防止回车键提交表单
1378
1468
  if (e.key === 'Enter') {
1379
1469
  e.stopPropagation();
1380
1470
  }
1381
1471
  }}
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
1472
  >
1418
1473
  {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1419
1474
  <Option key={group} value={group}>{group}</Option>