whistle.mock-plugins 1.2.0 → 1.2.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION.md +4 -0
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +15 -0
- package/package.json +3 -1
- package/ui/src/components/interface-management/DataManagementModal.js +110 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +57 -3
- package/ui/src/pages/InterfaceManagement.js +22 -0
- package/ui/src/pages/InterfaceManagementV2.js +22 -0
- package/ui/src/styles/interface-management.css +31 -0
|
@@ -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
|
/** */
|
|
59
72
|
|
|
60
73
|
/** */
|
|
@@ -137,6 +150,8 @@
|
|
|
137
150
|
|
|
138
151
|
/** */
|
|
139
152
|
|
|
153
|
+
/** */
|
|
154
|
+
|
|
140
155
|
/** */
|
|
141
156
|
|
|
142
157
|
/** */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whistle.mock-plugins",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1-beta.2",
|
|
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={1040}
|
|
69
|
+
destroyOnClose
|
|
70
|
+
okText="保存"
|
|
71
|
+
cancelText="取消"
|
|
72
|
+
bodyStyle={{ maxHeight: '80vh', 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 {
|
|
@@ -11,7 +12,8 @@ import {
|
|
|
11
12
|
CheckCircleOutlined,
|
|
12
13
|
EditOutlined,
|
|
13
14
|
FullscreenOutlined,
|
|
14
|
-
FullscreenExitOutlined
|
|
15
|
+
FullscreenExitOutlined,
|
|
16
|
+
MenuOutlined
|
|
15
17
|
} from '@ant-design/icons';
|
|
16
18
|
import { generateResponseId } from './utils';
|
|
17
19
|
|
|
@@ -22,16 +24,32 @@ const ResponseContentEditor = ({
|
|
|
22
24
|
form,
|
|
23
25
|
responses,
|
|
24
26
|
activeResponseId,
|
|
25
|
-
onPreview
|
|
27
|
+
onPreview,
|
|
28
|
+
enableSorting = false
|
|
26
29
|
}) => {
|
|
27
30
|
const { getFieldValue, setFieldsValue } = form;
|
|
28
31
|
const [isEditing, setIsEditing] = useState(false);
|
|
29
32
|
const [editingName, setEditingName] = useState('');
|
|
30
33
|
const [isTextareaFullscreen, setIsTextareaFullscreen] = useState(false);
|
|
34
|
+
const [showSorting, setShowSorting] = useState(false);
|
|
31
35
|
|
|
32
36
|
// 查找激活的响应
|
|
33
37
|
const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
|
|
34
38
|
const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
|
|
39
|
+
// 拖拽排序:基于 ReactSortable 管理一个本地列表镜像
|
|
40
|
+
const [sortableList, setSortableList] = useState(() => {
|
|
41
|
+
return Array.isArray(responses) ? responses.map(r => ({ ...r })) : [];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setSortableList(Array.isArray(responses) ? responses.map(r => ({ ...r })) : []);
|
|
46
|
+
}, [responses]);
|
|
47
|
+
|
|
48
|
+
const handleAfterSort = (newList) => {
|
|
49
|
+
// 将排序后的列表写回表单 responses 字段;activeResponseId 不变
|
|
50
|
+
setFieldsValue({ responses: newList });
|
|
51
|
+
};
|
|
52
|
+
|
|
35
53
|
|
|
36
54
|
// 全屏切换函数
|
|
37
55
|
const toggleTextareaFullscreen = () => {
|
|
@@ -263,6 +281,17 @@ const ResponseContentEditor = ({
|
|
|
263
281
|
}
|
|
264
282
|
extra={
|
|
265
283
|
<Space size="small">
|
|
284
|
+
{enableSorting && (
|
|
285
|
+
<Button
|
|
286
|
+
type="default"
|
|
287
|
+
icon={<MenuOutlined />}
|
|
288
|
+
onClick={() => setShowSorting(!showSorting)}
|
|
289
|
+
size="small"
|
|
290
|
+
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
|
291
|
+
>
|
|
292
|
+
排序
|
|
293
|
+
</Button>
|
|
294
|
+
)}
|
|
266
295
|
<Button
|
|
267
296
|
type="default"
|
|
268
297
|
icon={<PlusCircleOutlined />}
|
|
@@ -313,6 +342,31 @@ const ResponseContentEditor = ({
|
|
|
313
342
|
style={{ marginBottom: 16 }}
|
|
314
343
|
bodyStyle={{ padding: '16px' }}
|
|
315
344
|
>
|
|
345
|
+
{enableSorting && showSorting && Array.isArray(sortableList) && sortableList.length > 1 && (
|
|
346
|
+
<div style={{ marginBottom: 12 }}>
|
|
347
|
+
<ReactSortable
|
|
348
|
+
list={sortableList}
|
|
349
|
+
setList={(newState) => {
|
|
350
|
+
setSortableList(newState);
|
|
351
|
+
handleAfterSort(newState);
|
|
352
|
+
}}
|
|
353
|
+
animation={150}
|
|
354
|
+
ghostClass="sortable-ghost"
|
|
355
|
+
style={{ display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 }}
|
|
356
|
+
>
|
|
357
|
+
{sortableList.map((item) => (
|
|
358
|
+
<div
|
|
359
|
+
key={item.id}
|
|
360
|
+
className={`sortable-chip${item.id === activeResponseId ? ' sortable-chip-active' : ''}`}
|
|
361
|
+
onClick={() => handleResponseSelect(item.id)}
|
|
362
|
+
title={item.name || '未命名响应'}
|
|
363
|
+
>
|
|
364
|
+
{item.name || '未命名响应'}
|
|
365
|
+
</div>
|
|
366
|
+
))}
|
|
367
|
+
</ReactSortable>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
316
370
|
{/* 响应选择和名称编辑合并区域 */}
|
|
317
371
|
<div style={{ marginBottom: '16px' }}>
|
|
318
372
|
<Row gutter={12} align="middle">
|
|
@@ -445,7 +499,7 @@ const ResponseContentEditor = ({
|
|
|
445
499
|
|
|
446
500
|
<div style={{ position: 'relative' }}>
|
|
447
501
|
<TextArea
|
|
448
|
-
rows={
|
|
502
|
+
rows={18}
|
|
449
503
|
placeholder="请输入响应内容..."
|
|
450
504
|
style={{
|
|
451
505
|
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
|
@@ -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;
|
|
@@ -69,6 +70,8 @@ const InterfaceManagement = () => {
|
|
|
69
70
|
const [modalVisible, setModalVisible] = useState(false);
|
|
70
71
|
const [previewVisible, setPreviewVisible] = useState(false);
|
|
71
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);
|
|
@@ -1001,6 +1004,15 @@ const InterfaceManagement = () => {
|
|
|
1001
1004
|
icon={<EditOutlined />}
|
|
1002
1005
|
onClick={() => handleEditInterface(record)}
|
|
1003
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
|
+
)}
|
|
1004
1016
|
<Tooltip title="复制新增">
|
|
1005
1017
|
<Button
|
|
1006
1018
|
type="text"
|
|
@@ -1833,6 +1845,16 @@ const InterfaceManagement = () => {
|
|
|
1833
1845
|
onCancel={() => setPreviewVisible(false)}
|
|
1834
1846
|
previewContent={previewContent}
|
|
1835
1847
|
/>
|
|
1848
|
+
|
|
1849
|
+
{/* 数据管理弹窗 */}
|
|
1850
|
+
<DataManagementModal
|
|
1851
|
+
visible={dataModalVisible}
|
|
1852
|
+
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
1853
|
+
interfaceItem={dataModalRecord}
|
|
1854
|
+
onSaved={() => {
|
|
1855
|
+
fetchInterfaces();
|
|
1856
|
+
}}
|
|
1857
|
+
/>
|
|
1836
1858
|
</div>
|
|
1837
1859
|
</AppLayout>
|
|
1838
1860
|
);
|
|
@@ -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;
|
|
@@ -75,6 +76,8 @@ const InterfaceManagement = () => {
|
|
|
75
76
|
const [modalVisible, setModalVisible] = useState(false);
|
|
76
77
|
const [previewVisible, setPreviewVisible] = useState(false);
|
|
77
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);
|
|
@@ -1007,6 +1010,15 @@ const InterfaceManagement = () => {
|
|
|
1007
1010
|
icon={<EditOutlined />}
|
|
1008
1011
|
onClick={() => handleEditInterface(record)}
|
|
1009
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
|
+
)}
|
|
1010
1022
|
<Tooltip title="复制新增">
|
|
1011
1023
|
<Button
|
|
1012
1024
|
type="text"
|
|
@@ -1926,6 +1938,16 @@ const InterfaceManagement = () => {
|
|
|
1926
1938
|
onCancel={() => setPreviewVisible(false)}
|
|
1927
1939
|
previewContent={previewContent}
|
|
1928
1940
|
/>
|
|
1941
|
+
|
|
1942
|
+
{/* 数据管理弹窗 */}
|
|
1943
|
+
<DataManagementModal
|
|
1944
|
+
visible={dataModalVisible}
|
|
1945
|
+
onCancel={() => { setDataModalVisible(false); setDataModalRecord(null); }}
|
|
1946
|
+
interfaceItem={dataModalRecord}
|
|
1947
|
+
onSaved={() => {
|
|
1948
|
+
fetchInterfaces();
|
|
1949
|
+
}}
|
|
1950
|
+
/>
|
|
1929
1951
|
</div>
|
|
1930
1952
|
</AppLayout>
|
|
1931
1953
|
);
|
|
@@ -1144,6 +1144,37 @@
|
|
|
1144
1144
|
background: #e6f4ff;
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
1147
|
+
/* 紧凑排序 Chips */
|
|
1148
|
+
.sortable-chip {
|
|
1149
|
+
display: inline-flex;
|
|
1150
|
+
align-items: center;
|
|
1151
|
+
padding: 2px 10px;
|
|
1152
|
+
height: 24px;
|
|
1153
|
+
border: 1px solid #e6e6e6;
|
|
1154
|
+
border-radius: 12px;
|
|
1155
|
+
background: #fff;
|
|
1156
|
+
font-size: 12px;
|
|
1157
|
+
color: #595959;
|
|
1158
|
+
cursor: move;
|
|
1159
|
+
user-select: none;
|
|
1160
|
+
white-space: nowrap;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.sortable-chip:hover {
|
|
1164
|
+
border-color: #bfbfbf;
|
|
1165
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.sortable-chip-active {
|
|
1169
|
+
background: #e6f7ff;
|
|
1170
|
+
border-color: #91d5ff;
|
|
1171
|
+
color: #1677ff;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.sortable-ghost {
|
|
1175
|
+
opacity: 0.6;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1147
1178
|
/* 接口弹窗样式 */
|
|
1148
1179
|
.interface-modal .ant-modal-header {
|
|
1149
1180
|
border-bottom: 1px solid #f0f0f0;
|