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.
@@ -152,8 +152,6 @@
152
152
 
153
153
  /**![menu](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkwNCAxNjBIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHptMCA2MjRIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHptMC0zMTJIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHoiIC8+PC9zdmc+) */
154
154
 
155
- /**![minus-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTY5NiA0ODBIMzI4Yy00LjQgMC04IDMuNi04IDh2NDhjMCA0LjQgMy42IDggOCA4aDM2OGM0LjQgMCA4LTMuNiA4LTh2LTQ4YzAtNC40LTMuNi04LTgtOHoiIC8+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PC9zdmc+) */
156
-
157
155
  /**![minus-square](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMyOCA1NDRoMzY4YzQuNCAwIDgtMy42IDgtOHYtNDhjMC00LjQtMy42LTgtOC04SDMyOGMtNC40IDAtOCAzLjYtOCA4djQ4YzAgNC40IDMuNiA4IDggOHoiIC8+PHBhdGggZD0iTTg4MCAxMTJIMTQ0Yy0xNy43IDAtMzIgMTQuMy0zMiAzMnY3MzZjMCAxNy43IDE0LjMgMzIgMzIgMzJoNzM2YzE3LjcgMCAzMi0xNC4zIDMyLTMyVjE0NGMwLTE3LjctMTQuMy0zMi0zMi0zMnptLTQwIDcyOEgxODRWMTg0aDY1NnY2NTZ6IiAvPjwvc3ZnPg==) */
158
156
 
159
157
  /**![plus-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTY5NiA0ODBINTQ0VjMyOGMwLTQuNC0zLjYtOC04LThoLTQ4Yy00LjQgMC04IDMuNi04IDh2MTUySDMyOGMtNC40IDAtOCAzLjYtOCA4djQ4YzAgNC40IDMuNiA4IDggOGgxNTJ2MTUyYzAgNC40IDMuNiA4IDggOGg0OGM0LjQgMCA4LTMuNiA4LThWNTQ0aDE1MmM0LjQgMCA4LTMuNiA4LTh2LTQ4YzAtNC40LTMuNi04LTgtOHoiIC8+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PC9zdmc+) */
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
  /**
@@ -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 = '{}';
@@ -268,36 +268,19 @@ const ruleManager = {
268
268
  return null;
269
269
  }
270
270
 
271
- // 在URL匹配的接口中进行参数匹配
272
- // 新数据(响应内含 paramMatchers):接口级 paramMatchers 不参与接口筛选,直接返回第一个URL匹配的接口
273
- // 旧数据(接口级 paramMatchers):保留原有逻辑,通过接口级 paramMatchers 筛选接口
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 = legacyInterfaces.find(intf =>
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.activeResponseId) {
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 matchedResponse = null;
754
+ let hitResponse = null;
772
755
 
773
756
  // 检查是否有多响应配置
774
757
  if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) {
775
- // 优先:遍历响应,找第一个 paramMatchers 匹配的响应(响应级匹配)
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
- matchedResponse = resp;
780
- this.log(`[规则处理器] 响应级参数匹配命中: ${resp.name || '未命名'}`);
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
- // 兜底:使用 activeResponseId 对应的响应,或第一个响应
787
- if (!matchedResponse) {
773
+ // 2. 无匹配则回退到 activeResponseId 指定的默认响应
774
+ if (!selectedResponse) {
788
775
  const activeResponseId = config.activeResponseId;
789
- matchedResponse = activeResponseId
790
- ? config.responses.find(r => r.id === activeResponseId)
791
- : null;
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 (matchedResponse) {
802
- template = matchedResponse.content;
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 (matchedResponse) {
831
- res.setHeader('X-Whistle-Mock-Template', matchedResponse.name || 'unnamed');
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: matchedResponse?.name || '默认响应'
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.12",
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
+