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.
@@ -4,6 +4,12 @@
4
4
  http://jedwatson.github.io/classnames
5
5
  */
6
6
 
7
+ /*!
8
+ Copyright (c) 2018 Jed Watson.
9
+ Licensed under the MIT License (MIT), see
10
+ http://jedwatson.github.io/classnames
11
+ */
12
+
7
13
  /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
8
14
 
9
15
  /**
@@ -55,6 +61,13 @@
55
61
  * LICENSE file in the root directory of this source tree.
56
62
  */
57
63
 
64
+ /**!
65
+ * Sortable 1.15.6
66
+ * @author RubaXa <trash@rubaxa.org>
67
+ * @author owenm <owen23355@gmail.com>
68
+ * @license MIT
69
+ */
70
+
58
71
  /**![api](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkxNy43IDE0OC44bC00Mi40LTQyLjRjLTEuNi0xLjYtMy42LTIuMy01LjctMi4zcy00LjEuOC01LjcgMi4zbC03Ni4xIDc2LjFhMTk5LjI3IDE5OS4yNyAwIDAwLTExMi4xLTM0LjNjLTUxLjIgMC0xMDIuNCAxOS41LTE0MS41IDU4LjZMNDMyLjMgMzA4LjdhOC4wMyA4LjAzIDAgMDAwIDExLjNMNzA0IDU5MS43YzEuNiAxLjYgMy42IDIuMyA1LjcgMi4zIDIgMCA0LjEtLjggNS43LTIuM2wxMDEuOS0xMDEuOWM2OC45LTY5IDc3LTE3NS43IDI0LjMtMjUzLjVsNzYuMS03Ni4xYzMuMS0zLjIgMy4xLTguMyAwLTExLjR6TTc2OS4xIDQ0MS43bC01OS40IDU5LjQtMTg2LjgtMTg2LjggNTkuNC01OS40YzI0LjktMjQuOSA1OC4xLTM4LjcgOTMuNC0zOC43IDM1LjMgMCA2OC40IDEzLjcgOTMuNCAzOC43IDI0LjkgMjQuOSAzOC43IDU4LjEgMzguNyA5My40IDAgMzUuMy0xMy44IDY4LjQtMzguNyA5My40em0tMTkwLjIgMTA1YTguMDMgOC4wMyAwIDAwLTExLjMgMEw1MDEgNjEzLjMgNDEwLjcgNTIzbDY2LjctNjYuN2MzLjEtMy4xIDMuMS04LjIgMC0xMS4zTDQ0MSA0MDguNmE4LjAzIDguMDMgMCAwMC0xMS4zIDBMMzYzIDQ3NS4zbC00My00M2E3Ljg1IDcuODUgMCAwMC01LjctMi4zYy0yIDAtNC4xLjgtNS43IDIuM0wyMDYuOCA1MzQuMmMtNjguOSA2OS03NyAxNzUuNy0yNC4zIDI1My41bC03Ni4xIDc2LjFhOC4wMyA4LjAzIDAgMDAwIDExLjNsNDIuNCA0Mi40YzEuNiAxLjYgMy42IDIuMyA1LjcgMi4zczQuMS0uOCA1LjctMi4zbDc2LjEtNzYuMWMzMy43IDIyLjkgNzIuOSAzNC4zIDExMi4xIDM0LjMgNTEuMiAwIDEwMi40LTE5LjUgMTQxLjUtNTguNmwxMDEuOS0xMDEuOWMzLjEtMy4xIDMuMS04LjIgMC0xMS4zbC00My00MyA2Ni43LTY2LjdjMy4xLTMuMSAzLjEtOC4yIDAtMTEuM2wtMzYuNi0zNi4yek00NDEuNyA3NjkuMWExMzEuMzIgMTMxLjMyIDAgMDEtOTMuNCAzOC43Yy0zNS4zIDAtNjguNC0xMy43LTkzLjQtMzguN2ExMzEuMzIgMTMxLjMyIDAgMDEtMzguNy05My40YzAtMzUuMyAxMy43LTY4LjQgMzguNy05My40bDU5LjQtNTkuNCAxODYuOCAxODYuOC01OS40IDU5LjR6IiAvPjwvc3ZnPg==) */
59
72
 
60
73
  /**![appstore](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQ2NCAxNDRIMTYwYy04LjggMC0xNiA3LjItMTYgMTZ2MzA0YzAgOC44IDcuMiAxNiAxNiAxNmgzMDRjOC44IDAgMTYtNy4yIDE2LTE2VjE2MGMwLTguOC03LjItMTYtMTYtMTZ6bS01MiAyNjhIMjEyVjIxMmgyMDB2MjAwem00NTItMjY4SDU2MGMtOC44IDAtMTYgNy4yLTE2IDE2djMwNGMwIDguOCA3LjIgMTYgMTYgMTZoMzA0YzguOCAwIDE2LTcuMiAxNi0xNlYxNjBjMC04LjgtNy4yLTE2LTE2LTE2em0tNTIgMjY4SDYxMlYyMTJoMjAwdjIwMHpNNDY0IDU0NEgxNjBjLTguOCAwLTE2IDcuMi0xNiAxNnYzMDRjMCA4LjggNy4yIDE2IDE2IDE2aDMwNGM4LjggMCAxNi03LjIgMTYtMTZWNTYwYzAtOC44LTcuMi0xNi0xNi0xNnptLTUyIDI2OEgyMTJWNjEyaDIwMHYyMDB6bTQ1Mi0yNjhINTYwYy04LjggMC0xNiA3LjItMTYgMTZ2MzA0YzAgOC44IDcuMiAxNiAxNiAxNmgzMDRjOC44IDAgMTYtNy4yIDE2LTE2VjU2MGMwLTguOC03LjItMTYtMTYtMTZ6bS01MiAyNjhINjEyVjYxMmgyMDB2MjAweiIgLz48L3N2Zz4=) */
@@ -156,5 +169,3 @@
156
169
  /**![setting](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkyNC44IDYyNS43bC02NS41LTU2YzMuMS0xOSA0LjctMzguNCA0LjctNTcuOHMtMS42LTM4LjgtNC43LTU3LjhsNjUuNS01NmEzMi4wMyAzMi4wMyAwIDAwOS4zLTM1LjJsLS45LTIuNmE0NDMuNzQgNDQzLjc0IDAgMDAtNzkuNy0xMzcuOWwtMS44LTIuMWEzMi4xMiAzMi4xMiAwIDAwLTM1LjEtOS41bC04MS4zIDI4LjljLTMwLTI0LjYtNjMuNS00NC05OS43LTU3LjZsLTE1LjctODVhMzIuMDUgMzIuMDUgMCAwMC0yNS44LTI1LjdsLTIuNy0uNWMtNTIuMS05LjQtMTA2LjktOS40LTE1OSAwbC0yLjcuNWEzMi4wNSAzMi4wNSAwIDAwLTI1LjggMjUuN2wtMTUuOCA4NS40YTM1MS44NiAzNTEuODYgMCAwMC05OSA1Ny40bC04MS45LTI5LjFhMzIgMzIgMCAwMC0zNS4xIDkuNWwtMS44IDIuMWE0NDYuMDIgNDQ2LjAyIDAgMDAtNzkuNyAxMzcuOWwtLjkgMi42Yy00LjUgMTIuNS0uOCAyNi41IDkuMyAzNS4ybDY2LjMgNTYuNmMtMy4xIDE4LjgtNC42IDM4LTQuNiA1Ny4xIDAgMTkuMiAxLjUgMzguNCA0LjYgNTcuMUw5OSA2MjUuNWEzMi4wMyAzMi4wMyAwIDAwLTkuMyAzNS4ybC45IDIuNmMxOC4xIDUwLjQgNDQuOSA5Ni45IDc5LjcgMTM3LjlsMS44IDIuMWEzMi4xMiAzMi4xMiAwIDAwMzUuMSA5LjVsODEuOS0yOS4xYzI5LjggMjQuNSA2My4xIDQzLjkgOTkgNTcuNGwxNS44IDg1LjRhMzIuMDUgMzIuMDUgMCAwMDI1LjggMjUuN2wyLjcuNWE0NDkuNCA0NDkuNCAwIDAwMTU5IDBsMi43LS41YTMyLjA1IDMyLjA1IDAgMDAyNS44LTI1LjdsMTUuNy04NWEzNTAgMzUwIDAgMDA5OS43LTU3LjZsODEuMyAyOC45YTMyIDMyIDAgMDAzNS4xLTkuNWwxLjgtMi4xYzM0LjgtNDEuMSA2MS42LTg3LjUgNzkuNy0xMzcuOWwuOS0yLjZjNC41LTEyLjMuOC0yNi4zLTkuMy0zNXpNNzg4LjMgNDY1LjljMi41IDE1LjEgMy44IDMwLjYgMy44IDQ2LjFzLTEuMyAzMS0zLjggNDYuMWwtNi42IDQwLjEgNzQuNyA2My45YTM3MC4wMyAzNzAuMDMgMCAwMS00Mi42IDczLjZMNzIxIDcwMi44bC0zMS40IDI1LjhjLTIzLjkgMTkuNi01MC41IDM1LTc5LjMgNDUuOGwtMzguMSAxNC4zLTE3LjkgOTdhMzc3LjUgMzc3LjUgMCAwMS04NSAwbC0xNy45LTk3LjItMzcuOC0xNC41Yy0yOC41LTEwLjgtNTUtMjYuMi03OC43LTQ1LjdsLTMxLjQtMjUuOS05My40IDMzLjJjLTE3LTIyLjktMzEuMi00Ny42LTQyLjYtNzMuNmw3NS41LTY0LjUtNi41LTQwYy0yLjQtMTQuOS0zLjctMzAuMy0zLjctNDUuNSAwLTE1LjMgMS4yLTMwLjYgMy43LTQ1LjVsNi41LTQwLTc1LjUtNjQuNWMxMS4zLTI2LjEgMjUuNi01MC43IDQyLjYtNzMuNmw5My40IDMzLjIgMzEuNC0yNS45YzIzLjctMTkuNSA1MC4yLTM0LjkgNzguNy00NS43bDM3LjktMTQuMyAxNy45LTk3LjJjMjguMS0zLjIgNTYuOC0zLjIgODUgMGwxNy45IDk3IDM4LjEgMTQuM2MyOC43IDEwLjggNTUuNCAyNi4yIDc5LjMgNDUuOGwzMS40IDI1LjggOTIuOC0zMi45YzE3IDIyLjkgMzEuMiA0Ny42IDQyLjYgNzMuNkw3ODEuOCA0MjZsNi41IDM5Ljl6TTUxMiAzMjZjLTk3LjIgMC0xNzYgNzguOC0xNzYgMTc2czc4LjggMTc2IDE3NiAxNzYgMTc2LTc4LjggMTc2LTE3Ni03OC44LTE3Ni0xNzYtMTc2em03OS4yIDI1NS4yQTExMS42IDExMS42IDAgMDE1MTIgNjE0Yy0yOS45IDAtNTgtMTEuNy03OS4yLTMyLjhBMTExLjYgMTExLjYgMCAwMTQwMCA1MDJjMC0yOS45IDExLjctNTggMzIuOC03OS4yQzQ1NCA0MDEuNiA0ODIuMSAzOTAgNTEyIDM5MGMyOS45IDAgNTggMTEuNiA3OS4yIDMyLjhBMTExLjYgMTExLjYgMCAwMTYyNCA1MDJjMCAyOS45LTExLjcgNTgtMzIuOCA3OS4yeiIgLz48L3N2Zz4=) */
157
170
 
158
171
  /**![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.1-beta.1",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -39,6 +39,8 @@
39
39
  "jest": "^29.7.0",
40
40
  "react": "^18.2.0",
41
41
  "react-dom": "^18.2.0",
42
+ "react-sortablejs": "^6.1.4",
43
+ "sortablejs": "^1.15.3",
42
44
  "react-markdown": "^8.0.7",
43
45
  "style-loader": "^3.3.2",
44
46
  "webpack": "^5.79.0",
@@ -0,0 +1,110 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Modal, Form, Input, message } from 'antd';
3
+ import axios from 'axios';
4
+ import { ResponseContentEditor, PreviewModal, formatResponseContent, refreshCacheAfterUpdate } from '.';
5
+
6
+ const DataManagementModal = ({ visible, onCancel, interfaceItem, onSaved }) => {
7
+ const [form] = Form.useForm();
8
+ const [previewVisible, setPreviewVisible] = useState(false);
9
+ const [previewContent, setPreviewContent] = useState(null);
10
+
11
+ useEffect(() => {
12
+ if (visible && interfaceItem) {
13
+ let responses = Array.isArray(interfaceItem.responses) ? interfaceItem.responses : [];
14
+ let activeResponseId = interfaceItem.activeResponseId || (responses[0] && responses[0].id) || '';
15
+ form.setFieldsValue({ responses, activeResponseId });
16
+ }
17
+ }, [visible, interfaceItem]);
18
+
19
+ const handlePreview = () => {
20
+ const values = form.getFieldsValue(true);
21
+ const responses = Array.isArray(values.responses) ? values.responses : [];
22
+ const activeResponse = responses.find(r => r.id === values.activeResponseId) || responses[0];
23
+ if (!activeResponse) {
24
+ message.error('未找到有效的响应内容');
25
+ return;
26
+ }
27
+ const contentType = 'application/json; charset=utf-8';
28
+ const formatted = formatResponseContent(activeResponse.content || '', contentType);
29
+ setPreviewContent({ title: `预览: ${activeResponse.name || '未命名响应'}`, content: formatted, contentType, description: '' });
30
+ setPreviewVisible(true);
31
+ };
32
+
33
+ const handleOk = async () => {
34
+ try {
35
+ await form.validateFields();
36
+ const values = form.getFieldsValue(true);
37
+ const payload = {
38
+ responses: Array.isArray(values.responses) ? values.responses : [],
39
+ activeResponseId: values.activeResponseId || (values.responses && values.responses[0] && values.responses[0].id) || ''
40
+ };
41
+ if (!interfaceItem || !interfaceItem.id) {
42
+ message.error('缺少接口ID,无法保存');
43
+ return;
44
+ }
45
+ const res = await axios.patch(`/cgi-bin/interfaces/${interfaceItem.id}`, payload);
46
+ if (res.data && res.data.code === 0) {
47
+ message.success('保存成功');
48
+ await refreshCacheAfterUpdate();
49
+ if (onSaved) onSaved();
50
+ if (onCancel) onCancel();
51
+ } else {
52
+ throw new Error(res.data?.message || '保存失败');
53
+ }
54
+ } catch (err) {
55
+ if (err && err.errorFields) return; // 表单校验错误
56
+ console.error('保存失败', err);
57
+ message.error(err.response?.data?.message || err.message || '保存失败');
58
+ }
59
+ };
60
+
61
+ return (
62
+ <>
63
+ <Modal
64
+ title="数据管理"
65
+ open={visible}
66
+ onOk={handleOk}
67
+ onCancel={onCancel}
68
+ width={900}
69
+ destroyOnClose
70
+ okText="保存"
71
+ cancelText="取消"
72
+ bodyStyle={{ maxHeight: '70vh', overflow: 'auto', padding: '24px' }}
73
+ >
74
+ <Form form={form} layout="vertical" initialValues={{ responses: [], activeResponseId: '' }}>
75
+ <Form.Item name="responses" hidden>
76
+ <Input />
77
+ </Form.Item>
78
+ <Form.Item name="activeResponseId" hidden>
79
+ <Input />
80
+ </Form.Item>
81
+ <Form.Item noStyle shouldUpdate>
82
+ {({ getFieldValue }) => {
83
+ const responses = getFieldValue('responses') || [];
84
+ const activeResponseId = getFieldValue('activeResponseId') || (responses[0] && responses[0].id) || '';
85
+ return (
86
+ <ResponseContentEditor
87
+ form={form}
88
+ responses={responses}
89
+ activeResponseId={activeResponseId}
90
+ onPreview={handlePreview}
91
+ enableSorting={true}
92
+ />
93
+ );
94
+ }}
95
+ </Form.Item>
96
+ </Form>
97
+ </Modal>
98
+
99
+ <PreviewModal
100
+ visible={previewVisible}
101
+ onCancel={() => setPreviewVisible(false)}
102
+ previewContent={previewContent}
103
+ />
104
+ </>
105
+ );
106
+ };
107
+
108
+ export default DataManagementModal;
109
+
110
+
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
+ import { ReactSortable } from 'react-sortablejs';
2
3
  import ReactDOM from 'react-dom';
3
4
  import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete } from 'antd';
4
5
  import {
@@ -22,7 +23,8 @@ const ResponseContentEditor = ({
22
23
  form,
23
24
  responses,
24
25
  activeResponseId,
25
- onPreview
26
+ onPreview,
27
+ enableSorting = false
26
28
  }) => {
27
29
  const { getFieldValue, setFieldsValue } = form;
28
30
  const [isEditing, setIsEditing] = useState(false);
@@ -32,6 +34,20 @@ const ResponseContentEditor = ({
32
34
  // 查找激活的响应
33
35
  const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
34
36
  const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
37
+ // 拖拽排序:基于 ReactSortable 管理一个本地列表镜像
38
+ const [sortableList, setSortableList] = useState(() => {
39
+ return Array.isArray(responses) ? responses.map(r => ({ ...r })) : [];
40
+ });
41
+
42
+ useEffect(() => {
43
+ setSortableList(Array.isArray(responses) ? responses.map(r => ({ ...r })) : []);
44
+ }, [responses]);
45
+
46
+ const handleAfterSort = (newList) => {
47
+ // 将排序后的列表写回表单 responses 字段;activeResponseId 不变
48
+ setFieldsValue({ responses: newList });
49
+ };
50
+
35
51
 
36
52
  // 全屏切换函数
37
53
  const toggleTextareaFullscreen = () => {
@@ -313,6 +329,48 @@ const ResponseContentEditor = ({
313
329
  style={{ marginBottom: 16 }}
314
330
  bodyStyle={{ padding: '16px' }}
315
331
  >
332
+ {enableSorting && Array.isArray(sortableList) && sortableList.length > 1 && (
333
+ <div style={{ marginBottom: '16px' }}>
334
+ <div style={{ marginBottom: '8px' }}>
335
+ <label style={{ fontSize: '12px', color: '#666', fontWeight: 'bold' }}>响应排序(拖拽调整顺序)</label>
336
+ </div>
337
+ <ReactSortable
338
+ list={sortableList}
339
+ setList={(newState) => {
340
+ setSortableList(newState);
341
+ handleAfterSort(newState);
342
+ }}
343
+ animation={150}
344
+ handle={undefined}
345
+ ghostClass="sortable-ghost"
346
+ >
347
+ {sortableList.map((item) => (
348
+ <div
349
+ key={item.id}
350
+ style={{
351
+ display: 'flex',
352
+ alignItems: 'center',
353
+ justifyContent: 'space-between',
354
+ padding: '6px 10px',
355
+ border: '1px solid #f0f0f0',
356
+ borderRadius: '4px',
357
+ marginBottom: '6px',
358
+ background: item.id === activeResponseId ? '#e6f7ff' : '#fff',
359
+ cursor: 'move'
360
+ }}
361
+ onClick={() => handleResponseSelect(item.id)}
362
+ title="拖拽以排序,点击选择"
363
+ >
364
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
365
+ <Badge status={item.id === activeResponseId ? 'processing' : 'default'} />
366
+ <span style={{ fontWeight: 500 }}>{item.name || '未命名响应'}</span>
367
+ </div>
368
+ <div style={{ color: '#999', fontSize: 12 }}>拖拽</div>
369
+ </div>
370
+ ))}
371
+ </ReactSortable>
372
+ </div>
373
+ )}
316
374
  {/* 响应选择和名称编辑合并区域 */}
317
375
  <div style={{ marginBottom: '16px' }}>
318
376
  <Row gutter={12} align="middle">
@@ -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';
@@ -26,6 +26,7 @@ import {
26
26
  formatResponseContent,
27
27
  generateResponseId
28
28
  } from '../components/interface-management';
29
+ import DataManagementModal from '../components/interface-management/DataManagementModal';
29
30
 
30
31
  const { Option } = Select;
31
32
  const { Search } = AntInput;
@@ -64,11 +65,13 @@ const InterfaceManagement = () => {
64
65
  const [groups, setGroups] = useState([]);
65
66
  const [selectedGroup, setSelectedGroup] = useState(null);
66
67
  const [groupActionLoading, setGroupActionLoading] = useState(false);
67
- const [newGroupName, setNewGroupName] = useState('');
68
68
 
69
69
  // 模态框状态
70
70
  const [modalVisible, setModalVisible] = useState(false);
71
71
  const [previewVisible, setPreviewVisible] = useState(false);
72
+ const [isDuplicateMode, setIsDuplicateMode] = useState(false);
73
+ const [dataModalVisible, setDataModalVisible] = useState(false);
74
+ const [dataModalRecord, setDataModalRecord] = useState(null);
72
75
 
73
76
  // 列配置状态
74
77
  const [columnConfigVisible, setColumnConfigVisible] = useState(false);
@@ -217,17 +220,6 @@ const InterfaceManagement = () => {
217
220
  setSelectedGroup(null);
218
221
  }
219
222
  };
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
223
 
232
224
  // 根据分组和搜索关键词筛选接口
233
225
  const getFilteredInterfaces = () => {
@@ -259,6 +251,7 @@ const InterfaceManagement = () => {
259
251
  return;
260
252
  }
261
253
  form.resetFields();
254
+ setIsDuplicateMode(false);
262
255
 
263
256
  // 创建默认响应
264
257
  const defaultResponseId = generateResponseId();
@@ -298,6 +291,7 @@ const InterfaceManagement = () => {
298
291
 
299
292
  // 重置表单
300
293
  form.resetFields();
294
+ setIsDuplicateMode(false);
301
295
 
302
296
  // 处理响应数据
303
297
  let responses = [];
@@ -357,6 +351,91 @@ const InterfaceManagement = () => {
357
351
  setModalVisible(true);
358
352
  };
359
353
 
354
+ // 复制新增接口
355
+ const handleDuplicate = (record) => {
356
+ if (!selectedFeatureId) {
357
+ message.warning('请先选择一个功能模块');
358
+ return;
359
+ }
360
+ if (!record) {
361
+ message.warning('接口数据不完整,无法复制');
362
+ return;
363
+ }
364
+
365
+ form.resetFields();
366
+
367
+ // 深拷贝源数据
368
+ const source = JSON.parse(JSON.stringify(record));
369
+
370
+ // 处理响应数据
371
+ let responses = [];
372
+ let activeResponseId = '';
373
+
374
+ if (Array.isArray(source.responses) && source.responses.length > 0) {
375
+ // 为每条响应生成新的ID,并重映射activeResponseId
376
+ const idMap = new Map();
377
+ responses = source.responses.map((resp) => {
378
+ const newId = generateResponseId();
379
+ idMap.set(resp.id, newId);
380
+ return {
381
+ id: newId,
382
+ name: resp.name || '未命名响应',
383
+ description: resp.description || '',
384
+ content: resp.content || '{}'
385
+ };
386
+ });
387
+ const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
388
+ activeResponseId = mapped || '';
389
+ } else if (source.responseContent) {
390
+ // 兼容旧数据格式
391
+ const defaultResponseId = generateResponseId();
392
+ responses = [{
393
+ id: defaultResponseId,
394
+ name: '默认响应',
395
+ description: '',
396
+ content: source.responseContent
397
+ }];
398
+ activeResponseId = defaultResponseId;
399
+ }
400
+
401
+ // 处理自定义请求头 -> 表单需要的数组结构
402
+ let headerItems = [];
403
+ if (source.headers && typeof source.headers === 'object') {
404
+ headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
405
+ headerName,
406
+ headerValue
407
+ }));
408
+ }
409
+
410
+ // 分组值保证为字符串
411
+ let groupValue = source.group;
412
+ if (Array.isArray(groupValue)) {
413
+ groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
414
+ }
415
+
416
+ // 设定表单初值
417
+ form.setFieldsValue({
418
+ name: `${source.name || '未命名接口'} - 副本`,
419
+ group: groupValue || undefined,
420
+ pattern: source.urlPattern,
421
+ proxyType: source.proxyType || 'response',
422
+ statusCode: (source.httpStatus || source.statusCode || 200).toString(),
423
+ contentType: source.contentType || 'application/json; charset=utf-8',
424
+ responses,
425
+ activeResponseId,
426
+ httpMethod: source.httpMethod || source.method || 'ALL',
427
+ targetUrl: source.targetUrl || '',
428
+ headerItems,
429
+ paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
430
+ responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
431
+ });
432
+
433
+ setCurrentResponseId(activeResponseId || null);
434
+ setEditingInterface(null); // 新增模式
435
+ setIsDuplicateMode(true);
436
+ setModalVisible(true);
437
+ };
438
+
360
439
  const handleDeleteInterface = async (id) => {
361
440
  try {
362
441
  const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
@@ -397,8 +476,9 @@ const InterfaceManagement = () => {
397
476
 
398
477
  const handleSubmit = async () => {
399
478
  try {
400
- // 表单验证
401
- const values = await form.validateFields();
479
+ // 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
480
+ await form.validateFields();
481
+ const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
402
482
  console.log('表单提交数据:', values);
403
483
 
404
484
  // 处理自定义请求头
@@ -417,19 +497,19 @@ const InterfaceManagement = () => {
417
497
  groupValue = groupValue.length > 0 ? groupValue[0] : '';
418
498
  }
419
499
 
420
- // 构建接口数据
500
+ // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
421
501
  const interfaceData = {
422
- name: values.name,
423
- group: groupValue || '', // 确保group不为undefined
424
- urlPattern: values.pattern,
425
- proxyType: values.proxyType,
502
+ name: values.name || (editingInterface ? editingInterface.name : ''),
503
+ group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
504
+ urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
505
+ proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
426
506
  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,
507
+ responses: values.responses || (editingInterface ? editingInterface.responses : []),
508
+ activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
509
+ httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
510
+ contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
511
+ responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
512
+ httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
433
513
  active: true
434
514
  };
435
515
 
@@ -441,13 +521,11 @@ const InterfaceManagement = () => {
441
521
  interfaceData.headers = headers;
442
522
  }
443
523
 
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
- }
524
+ // 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
525
+ const filteredParamMatchers = Array.isArray(values.paramMatchers)
526
+ ? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
527
+ : [];
528
+ interfaceData.paramMatchers = filteredParamMatchers;
451
529
 
452
530
  console.log('提交接口数据:', interfaceData);
453
531
 
@@ -488,6 +566,7 @@ const InterfaceManagement = () => {
488
566
 
489
567
  const handleCancel = () => {
490
568
  setModalVisible(false);
569
+ setIsDuplicateMode(false);
491
570
  };
492
571
 
493
572
  const handleSelectFeature = (featureId) => {
@@ -917,7 +996,7 @@ const InterfaceManagement = () => {
917
996
  {
918
997
  title: '操作',
919
998
  key: 'action',
920
- width: 180,
999
+ width: 220,
921
1000
  render: (_, record) => (
922
1001
  <div className="action-buttons">
923
1002
  <Button
@@ -925,6 +1004,22 @@ const InterfaceManagement = () => {
925
1004
  icon={<EditOutlined />}
926
1005
  onClick={() => handleEditInterface(record)}
927
1006
  />
1007
+ {record.proxyType === 'response' && (
1008
+ <Tooltip title="数据管理">
1009
+ <Button
1010
+ type="text"
1011
+ icon={<FileTextOutlined />}
1012
+ onClick={() => { setDataModalRecord(record); setDataModalVisible(true); }}
1013
+ />
1014
+ </Tooltip>
1015
+ )}
1016
+ <Tooltip title="复制新增">
1017
+ <Button
1018
+ type="text"
1019
+ icon={<CopyOutlined />}
1020
+ onClick={() => handleDuplicate(record)}
1021
+ />
1022
+ </Tooltip>
928
1023
  <Popconfirm
929
1024
  title="确定要删除此接口吗?"
930
1025
  onConfirm={() => handleDeleteInterface(record.id)}
@@ -1264,7 +1359,7 @@ const InterfaceManagement = () => {
1264
1359
 
1265
1360
  {/* 接口编辑/创建模态框 */}
1266
1361
  <Modal
1267
- title={editingInterface ? '编辑接口' : '添加接口'}
1362
+ title={editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}
1268
1363
  open={modalVisible}
1269
1364
  onOk={handleSubmit}
1270
1365
  onCancel={handleCancel}
@@ -1310,55 +1405,27 @@ const InterfaceManagement = () => {
1310
1405
  tooltip="为接口设置分组,便于管理和批量操作"
1311
1406
  >
1312
1407
  <Select
1313
- placeholder="选择或输入新分组名称"
1408
+ placeholder="选择或创建分组"
1314
1409
  showSearch
1315
1410
  allowClear
1316
1411
  style={{ width: '100%' }}
1317
- dropdownMatchSelectWidth={false}
1318
- filterOption={(input, option) =>
1319
- option?.children?.toLowerCase()?.indexOf(input.toLowerCase()) >= 0
1320
- }
1412
+ mode="tags"
1413
+ maxTagCount="responsive"
1414
+ maxTagTextLength={10}
1415
+ onChange={(value) => {
1416
+ // 如果值是数组(tags模式),只取第一个元素
1417
+ if (Array.isArray(value) && value.length > 0) {
1418
+ // 如果第一个元素是空字符串,则设置为undefined
1419
+ const groupValue = value[0] === '' ? undefined : value[0];
1420
+ form.setFieldsValue({ group: groupValue });
1421
+ }
1422
+ }}
1321
1423
  onInputKeyDown={(e) => {
1322
1424
  // 防止回车键提交表单
1323
1425
  if (e.key === 'Enter') {
1324
1426
  e.stopPropagation();
1325
1427
  }
1326
1428
  }}
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
1429
  >
1363
1430
  {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1364
1431
  <Option key={group} value={group}>{group}</Option>
@@ -1778,6 +1845,16 @@ const InterfaceManagement = () => {
1778
1845
  onCancel={() => setPreviewVisible(false)}
1779
1846
  previewContent={previewContent}
1780
1847
  />
1848
+
1849
+ {/* 数据管理弹窗 */}
1850
+ <DataManagementModal
1851
+ visible={dataModalVisible}
1852
+ onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
1853
+ interfaceItem={dataModalRecord}
1854
+ onSaved={() => {
1855
+ fetchInterfaces();
1856
+ }}
1857
+ />
1781
1858
  </div>
1782
1859
  </AppLayout>
1783
1860
  );