whistle.mock-plugins 1.2.2-beta.12 → 1.2.2-beta.18
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/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
- package/TEST_GUIDE.md +156 -0
- package/VERSION.md +34 -0
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +0 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +4 -2
- package/lib/ruleManager.js +30 -47
- package/package.json +2 -1
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +118 -132
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/pages/InterfaceManagementV2.js +491 -134
- package/ui/src/pages/Settings.js +75 -6
- package/ui/src/styles/interface-management.css +37 -1
- package/ui/src/styles/settings.css +29 -0
|
@@ -152,8 +152,6 @@
|
|
|
152
152
|
|
|
153
153
|
/** */
|
|
154
154
|
|
|
155
|
-
/** */
|
|
156
|
-
|
|
157
155
|
/** */
|
|
158
156
|
|
|
159
157
|
/** */
|
package/lib/models.js
CHANGED
|
@@ -22,6 +22,15 @@
|
|
|
22
22
|
* @property {string} name - 响应名称
|
|
23
23
|
* @property {string} description - 响应描述
|
|
24
24
|
* @property {string} content - 响应内容,使用 Mock.js 语法
|
|
25
|
+
* @property {Array<ParamMatcher>} paramMatchers - 响应级入参匹配规则,运行时按顺序匹配,第一个满足所有规则的响应被返回;无规则的响应作为默认兜底
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 参数匹配规则数据模型
|
|
30
|
+
* @typedef {Object} ParamMatcher
|
|
31
|
+
* @property {string} paramPath - 参数路径,支持嵌套路径(如 data.user.id)
|
|
32
|
+
* @property {string} paramValue - 期望值
|
|
33
|
+
* @property {string} matchType - 匹配类型,可选值: 'exact'(精确匹配), 'contains'(包含), 'regex'(正则)
|
|
25
34
|
*/
|
|
26
35
|
|
|
27
36
|
/**
|
package/lib/routes/interfaces.js
CHANGED
|
@@ -87,7 +87,8 @@ const processInterfaceData = (data) => {
|
|
|
87
87
|
id: Math.random().toString(36).substring(2, 15),
|
|
88
88
|
name: '默认响应',
|
|
89
89
|
description: '',
|
|
90
|
-
content: result.responseContent
|
|
90
|
+
content: result.responseContent,
|
|
91
|
+
paramMatchers: []
|
|
91
92
|
}];
|
|
92
93
|
result.activeResponseId = result.responses[0].id;
|
|
93
94
|
console.log('创建的默认响应:', result.responses[0]);
|
|
@@ -99,7 +100,8 @@ const processInterfaceData = (data) => {
|
|
|
99
100
|
id: Math.random().toString(36).substring(2, 15),
|
|
100
101
|
name: '默认响应',
|
|
101
102
|
description: '',
|
|
102
|
-
content: '{}'
|
|
103
|
+
content: '{}',
|
|
104
|
+
paramMatchers: []
|
|
103
105
|
}];
|
|
104
106
|
result.activeResponseId = result.responses[0].id;
|
|
105
107
|
result.responseContent = '{}';
|
package/lib/ruleManager.js
CHANGED
|
@@ -268,36 +268,19 @@ const ruleManager = {
|
|
|
268
268
|
return null;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// 在URL
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const hasResponseLevelMatchers = (intf) => {
|
|
275
|
-
return intf.responses && Array.isArray(intf.responses) &&
|
|
276
|
-
intf.responses.some(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0);
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// 分离新旧数据接口
|
|
280
|
-
const newStyleInterfaces = urlMatchedInterfaces.filter(intf => hasResponseLevelMatchers(intf));
|
|
281
|
-
const legacyInterfaces = urlMatchedInterfaces.filter(intf => !hasResponseLevelMatchers(intf));
|
|
282
|
-
|
|
283
|
-
// 新数据接口:只要URL匹配就直接返回第一个(参数匹配在响应选择阶段处理)
|
|
284
|
-
if (newStyleInterfaces.length > 0) {
|
|
285
|
-
this.log(`[规则处理器] 找到新式接口(响应级匹配): ${newStyleInterfaces[0].name}`);
|
|
286
|
-
return newStyleInterfaces[0];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 旧数据接口:保留原有接口级 paramMatchers 筛选逻辑
|
|
290
|
-
for (const intf of legacyInterfaces) {
|
|
271
|
+
// 在URL匹配的接口中,查找参数也匹配的接口
|
|
272
|
+
for (const intf of urlMatchedInterfaces) {
|
|
273
|
+
// 检查参数匹配
|
|
291
274
|
if (isParamMatch(req, intf.paramMatchers, this.log)) {
|
|
292
|
-
this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL +
|
|
275
|
+
this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
|
|
293
276
|
return intf;
|
|
294
277
|
} else {
|
|
295
278
|
this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
|
|
296
279
|
}
|
|
297
280
|
}
|
|
298
281
|
|
|
299
|
-
//
|
|
300
|
-
const fallbackInterface =
|
|
282
|
+
// 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
|
|
283
|
+
const fallbackInterface = urlMatchedInterfaces.find(intf =>
|
|
301
284
|
!intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
|
|
302
285
|
);
|
|
303
286
|
|
|
@@ -454,9 +437,9 @@ const ruleManager = {
|
|
|
454
437
|
}
|
|
455
438
|
}
|
|
456
439
|
|
|
457
|
-
//
|
|
458
|
-
if (config.responses && config.
|
|
459
|
-
const activeResponse = config.responses.find(r => r.id === config.activeResponseId);
|
|
440
|
+
// 对于有多个响应的接口,添加当前响应标识(优先展示实际命中的响应,在 handleDataTemplate 前此处只能用 activeResponseId 作为预估)
|
|
441
|
+
if (config.responses && config.responses.length > 0) {
|
|
442
|
+
const activeResponse = config.responses.find(r => r.id === config.activeResponseId) || config.responses[0];
|
|
460
443
|
if (activeResponse && activeResponse.name) {
|
|
461
444
|
res.setHeader('X-Whistle-Mock-Response', activeResponse.name);
|
|
462
445
|
}
|
|
@@ -768,38 +751,36 @@ const ruleManager = {
|
|
|
768
751
|
|
|
769
752
|
// 支持多响应系统
|
|
770
753
|
let template = '';
|
|
771
|
-
let
|
|
754
|
+
let hitResponse = null;
|
|
772
755
|
|
|
773
756
|
// 检查是否有多响应配置
|
|
774
757
|
if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) {
|
|
775
|
-
|
|
758
|
+
let selectedResponse = null;
|
|
759
|
+
|
|
760
|
+
// 1. 按顺序查找第一个入参匹配的响应
|
|
776
761
|
for (const resp of config.responses) {
|
|
777
762
|
if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
|
|
778
763
|
if (isParamMatch(req, resp.paramMatchers, this.log)) {
|
|
779
|
-
|
|
780
|
-
this.log(`[规则处理器]
|
|
764
|
+
selectedResponse = resp;
|
|
765
|
+
this.log(`[规则处理器] 响应级入参匹配成功,使用响应: ${resp.name || '未命名'}`);
|
|
781
766
|
break;
|
|
767
|
+
} else {
|
|
768
|
+
this.log(`[规则处理器] 响应 "${resp.name || '未命名'}" 入参不匹配,继续检查下一个`);
|
|
782
769
|
}
|
|
783
770
|
}
|
|
784
771
|
}
|
|
785
772
|
|
|
786
|
-
//
|
|
787
|
-
if (!
|
|
773
|
+
// 2. 无匹配则回退到 activeResponseId 指定的默认响应
|
|
774
|
+
if (!selectedResponse) {
|
|
788
775
|
const activeResponseId = config.activeResponseId;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (matchedResponse) {
|
|
794
|
-
this.log(`[规则处理器] 使用兜底响应(activeResponseId): ${matchedResponse.name || '未命名'}`);
|
|
795
|
-
} else {
|
|
796
|
-
matchedResponse = config.responses[0];
|
|
797
|
-
this.log(`[规则处理器] 使用第一个响应: ${matchedResponse?.name || '未命名'}`);
|
|
798
|
-
}
|
|
776
|
+
selectedResponse = (activeResponseId && config.responses.find(r => r.id === activeResponseId))
|
|
777
|
+
|| config.responses[0];
|
|
778
|
+
this.log(`[规则处理器] 无响应级匹配,使用默认响应: ${selectedResponse ? (selectedResponse.name || '未命名') : '无'}`);
|
|
799
779
|
}
|
|
800
780
|
|
|
801
|
-
if (
|
|
802
|
-
|
|
781
|
+
if (selectedResponse) {
|
|
782
|
+
hitResponse = selectedResponse;
|
|
783
|
+
template = selectedResponse.content;
|
|
803
784
|
}
|
|
804
785
|
}
|
|
805
786
|
|
|
@@ -827,8 +808,10 @@ const ruleManager = {
|
|
|
827
808
|
const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false';
|
|
828
809
|
if (enableMockHeaders) {
|
|
829
810
|
res.setHeader('X-Whistle-Mock-Data-Type', 'template');
|
|
830
|
-
if (
|
|
831
|
-
res.setHeader('X-Whistle-Mock-Template',
|
|
811
|
+
if (hitResponse) {
|
|
812
|
+
res.setHeader('X-Whistle-Mock-Template', hitResponse.name || 'unnamed');
|
|
813
|
+
// 覆盖 processRequest 中预设的响应头,确保反映实际命中的响应
|
|
814
|
+
res.setHeader('X-Whistle-Mock-Response', hitResponse.name || 'unnamed');
|
|
832
815
|
}
|
|
833
816
|
}
|
|
834
817
|
|
|
@@ -841,7 +824,7 @@ const ruleManager = {
|
|
|
841
824
|
mockInfo: {
|
|
842
825
|
delay: parseInt(config.responseDelay || config.delay || 0, 10),
|
|
843
826
|
timestamp: new Date().toISOString(),
|
|
844
|
-
responseName:
|
|
827
|
+
responseName: hitResponse ? (hitResponse.name || '未命名') : '默认响应'
|
|
845
828
|
}
|
|
846
829
|
};
|
|
847
830
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whistle.mock-plugins",
|
|
3
|
-
"version": "1.2.2-beta.
|
|
3
|
+
"version": "1.2.2-beta.18",
|
|
4
4
|
"description": "Whistle 插件,用于快速创建 API 模拟数据",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"jest": "^29.7.0",
|
|
40
40
|
"react": "^18.2.0",
|
|
41
41
|
"react-dom": "^18.2.0",
|
|
42
|
+
"react-resizable": "^3.0.5",
|
|
42
43
|
"react-sortablejs": "^6.1.4",
|
|
43
44
|
"sortablejs": "^1.15.3",
|
|
44
45
|
"react-markdown": "^8.0.7",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Drawer, Form, Input, message, Button, Space } from 'antd';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { ResponseContentEditor, PreviewModal, formatResponseContent, refreshCacheAfterUpdate } from '.';
|
|
5
|
+
|
|
6
|
+
const DataManagementDrawer = ({ 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
|
+
<Drawer
|
|
64
|
+
title="数据管理"
|
|
65
|
+
placement="right"
|
|
66
|
+
width={1040}
|
|
67
|
+
open={visible}
|
|
68
|
+
onClose={onCancel}
|
|
69
|
+
destroyOnClose
|
|
70
|
+
bodyStyle={{ paddingBottom: 80 }}
|
|
71
|
+
footer={
|
|
72
|
+
<div style={{ textAlign: 'right' }}>
|
|
73
|
+
<Space>
|
|
74
|
+
<Button onClick={onCancel}>取消</Button>
|
|
75
|
+
<Button type="primary" onClick={handleOk}>保存</Button>
|
|
76
|
+
</Space>
|
|
77
|
+
</div>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<Form form={form} layout="vertical" initialValues={{ responses: [], activeResponseId: '' }}>
|
|
81
|
+
<Form.Item name="responses" hidden>
|
|
82
|
+
<Input />
|
|
83
|
+
</Form.Item>
|
|
84
|
+
<Form.Item name="activeResponseId" hidden>
|
|
85
|
+
<Input />
|
|
86
|
+
</Form.Item>
|
|
87
|
+
<Form.Item noStyle shouldUpdate>
|
|
88
|
+
{({ getFieldValue }) => {
|
|
89
|
+
const responses = getFieldValue('responses') || [];
|
|
90
|
+
const activeResponseId = getFieldValue('activeResponseId') || (responses[0] && responses[0].id) || '';
|
|
91
|
+
return (
|
|
92
|
+
<ResponseContentEditor
|
|
93
|
+
form={form}
|
|
94
|
+
responses={responses}
|
|
95
|
+
activeResponseId={activeResponseId}
|
|
96
|
+
onPreview={handlePreview}
|
|
97
|
+
enableSorting={true}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}}
|
|
101
|
+
</Form.Item>
|
|
102
|
+
</Form>
|
|
103
|
+
</Drawer>
|
|
104
|
+
|
|
105
|
+
<PreviewModal
|
|
106
|
+
visible={previewVisible}
|
|
107
|
+
onCancel={() => setPreviewVisible(false)}
|
|
108
|
+
previewContent={previewContent}
|
|
109
|
+
/>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default DataManagementDrawer;
|
|
115
|
+
|