whistle.mock-plugins 1.2.2-beta.7 → 1.2.2-beta.8
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 +0 -34
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/ruleManager.js +55 -23
- package/package.json +1 -2
- package/ui/src/components/interface-management/ResponseContentEditor.js +178 -30
- package/ui/src/components/interface-management/index.js +0 -1
- package/ui/src/pages/InterfaceManagementV2.js +205 -985
- package/ui/src/pages/Settings.js +6 -75
- package/ui/src/styles/interface-management.css +1 -37
- package/ui/src/styles/settings.css +0 -29
- package/COLUMN_RESIZE_IMPLEMENTATION.md +0 -162
- package/TEST_GUIDE.md +0 -156
- package/ui/src/components/interface-management/DataManagementDrawer.js +0 -115
|
@@ -152,6 +152,8 @@
|
|
|
152
152
|
|
|
153
153
|
/** */
|
|
154
154
|
|
|
155
|
+
/** */
|
|
156
|
+
|
|
155
157
|
/** */
|
|
156
158
|
|
|
157
159
|
/** */
|
package/lib/ruleManager.js
CHANGED
|
@@ -268,19 +268,36 @@ const ruleManager = {
|
|
|
268
268
|
return null;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// 在URL
|
|
272
|
-
|
|
273
|
-
|
|
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) {
|
|
274
291
|
if (isParamMatch(req, intf.paramMatchers, this.log)) {
|
|
275
|
-
this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL +
|
|
292
|
+
this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 接口级参数匹配)`);
|
|
276
293
|
return intf;
|
|
277
294
|
} else {
|
|
278
295
|
this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
|
|
282
|
-
//
|
|
283
|
-
const fallbackInterface =
|
|
299
|
+
// 旧数据兜底:返回第一个没有接口级参数匹配规则的接口
|
|
300
|
+
const fallbackInterface = legacyInterfaces.find(intf =>
|
|
284
301
|
!intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
|
|
285
302
|
);
|
|
286
303
|
|
|
@@ -751,18 +768,38 @@ const ruleManager = {
|
|
|
751
768
|
|
|
752
769
|
// 支持多响应系统
|
|
753
770
|
let template = '';
|
|
771
|
+
let matchedResponse = null;
|
|
754
772
|
|
|
755
773
|
// 检查是否有多响应配置
|
|
756
774
|
if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) {
|
|
757
|
-
//
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
775
|
+
// 优先:遍历响应,找第一个 paramMatchers 匹配的响应(响应级匹配)
|
|
776
|
+
for (const resp of config.responses) {
|
|
777
|
+
if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
|
|
778
|
+
if (isParamMatch(req, resp.paramMatchers, this.log)) {
|
|
779
|
+
matchedResponse = resp;
|
|
780
|
+
this.log(`[规则处理器] 响应级参数匹配命中: ${resp.name || '未命名'}`);
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 兜底:使用 activeResponseId 对应的响应,或第一个响应
|
|
787
|
+
if (!matchedResponse) {
|
|
788
|
+
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
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (matchedResponse) {
|
|
802
|
+
template = matchedResponse.content;
|
|
766
803
|
}
|
|
767
804
|
}
|
|
768
805
|
|
|
@@ -790,11 +827,8 @@ const ruleManager = {
|
|
|
790
827
|
const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false';
|
|
791
828
|
if (enableMockHeaders) {
|
|
792
829
|
res.setHeader('X-Whistle-Mock-Data-Type', 'template');
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
if (activeResponse) {
|
|
796
|
-
res.setHeader('X-Whistle-Mock-Template', activeResponse.name || 'unnamed');
|
|
797
|
-
}
|
|
830
|
+
if (matchedResponse) {
|
|
831
|
+
res.setHeader('X-Whistle-Mock-Template', matchedResponse.name || 'unnamed');
|
|
798
832
|
}
|
|
799
833
|
}
|
|
800
834
|
|
|
@@ -807,9 +841,7 @@ const ruleManager = {
|
|
|
807
841
|
mockInfo: {
|
|
808
842
|
delay: parseInt(config.responseDelay || config.delay || 0, 10),
|
|
809
843
|
timestamp: new Date().toISOString(),
|
|
810
|
-
responseName:
|
|
811
|
-
? (config.responses.find(r => r.id === config.activeResponseId)?.name || '未命名')
|
|
812
|
-
: '默认响应'
|
|
844
|
+
responseName: matchedResponse?.name || '默认响应'
|
|
813
845
|
}
|
|
814
846
|
};
|
|
815
847
|
} 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.8",
|
|
4
4
|
"description": "Whistle 插件,用于快速创建 API 模拟数据",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -39,7 +39,6 @@
|
|
|
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",
|
|
43
42
|
"react-sortablejs": "^6.1.4",
|
|
44
43
|
"sortablejs": "^1.15.3",
|
|
45
44
|
"react-markdown": "^8.0.7",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { ReactSortable } from 'react-sortablejs';
|
|
3
3
|
import ReactDOM from 'react-dom';
|
|
4
|
-
import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete } from 'antd';
|
|
4
|
+
import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete, Tag } from 'antd';
|
|
5
5
|
import {
|
|
6
6
|
CodeOutlined,
|
|
7
7
|
EyeOutlined,
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
EditOutlined,
|
|
14
14
|
FullscreenOutlined,
|
|
15
15
|
FullscreenExitOutlined,
|
|
16
|
-
MenuOutlined
|
|
16
|
+
MenuOutlined,
|
|
17
|
+
FilterOutlined,
|
|
18
|
+
MinusCircleOutlined
|
|
17
19
|
} from '@ant-design/icons';
|
|
18
20
|
import { generateResponseId } from './utils';
|
|
19
21
|
|
|
@@ -89,7 +91,8 @@ const ResponseContentEditor = ({
|
|
|
89
91
|
id: newResponseId,
|
|
90
92
|
name: `响应 ${currentResponses.length + 1}`,
|
|
91
93
|
description: '',
|
|
92
|
-
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
94
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
|
|
95
|
+
paramMatchers: []
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
const updatedResponses = [...currentResponses, newResponse];
|
|
@@ -170,6 +173,42 @@ const ResponseContentEditor = ({
|
|
|
170
173
|
}
|
|
171
174
|
};
|
|
172
175
|
|
|
176
|
+
// 更新当前响应的 paramMatchers
|
|
177
|
+
const updateActiveResponseParamMatchers = (paramMatchers) => {
|
|
178
|
+
if (activeResponseIndex >= 0) {
|
|
179
|
+
const currentResponses = getFieldValue('responses') || [];
|
|
180
|
+
const newResponses = [...currentResponses];
|
|
181
|
+
newResponses[activeResponseIndex] = {
|
|
182
|
+
...newResponses[activeResponseIndex],
|
|
183
|
+
paramMatchers
|
|
184
|
+
};
|
|
185
|
+
setFieldsValue({ responses: newResponses });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// 添加一条匹配规则
|
|
190
|
+
const handleAddMatcher = () => {
|
|
191
|
+
const currentMatchers = activeResponse?.paramMatchers || [];
|
|
192
|
+
updateActiveResponseParamMatchers([
|
|
193
|
+
...currentMatchers,
|
|
194
|
+
{ paramPath: '', paramValue: '', matchType: 'exact' }
|
|
195
|
+
]);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// 删除一条匹配规则
|
|
199
|
+
const handleDeleteMatcher = (index) => {
|
|
200
|
+
const currentMatchers = [...(activeResponse?.paramMatchers || [])];
|
|
201
|
+
currentMatchers.splice(index, 1);
|
|
202
|
+
updateActiveResponseParamMatchers(currentMatchers);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// 更新某条匹配规则的字段
|
|
206
|
+
const handleMatcherChange = (index, field, value) => {
|
|
207
|
+
const currentMatchers = [...(activeResponse?.paramMatchers || [])];
|
|
208
|
+
currentMatchers[index] = { ...currentMatchers[index], [field]: value };
|
|
209
|
+
updateActiveResponseParamMatchers(currentMatchers);
|
|
210
|
+
};
|
|
211
|
+
|
|
173
212
|
// 格式化JSON内容
|
|
174
213
|
const formatJsonContent = () => {
|
|
175
214
|
if (!activeResponse || !activeResponse.content) {
|
|
@@ -416,34 +455,48 @@ const ResponseContentEditor = ({
|
|
|
416
455
|
onChange={handleResponseSelect}
|
|
417
456
|
optionLabelProp="label"
|
|
418
457
|
>
|
|
419
|
-
{responses.map((resp, index) =>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
<
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
458
|
+
{responses.map((resp, index) => {
|
|
459
|
+
const matcherCount = resp.paramMatchers?.length || 0;
|
|
460
|
+
return (
|
|
461
|
+
<Option
|
|
462
|
+
key={resp.id}
|
|
463
|
+
value={resp.id}
|
|
464
|
+
label={
|
|
465
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
466
|
+
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
|
467
|
+
<span>{resp.name || `响应 ${index + 1}`}</span>
|
|
468
|
+
</div>
|
|
469
|
+
}
|
|
470
|
+
>
|
|
471
|
+
<div style={{ padding: '4px 0' }}>
|
|
472
|
+
<div style={{
|
|
473
|
+
display: 'flex',
|
|
474
|
+
alignItems: 'center',
|
|
475
|
+
justifyContent: 'space-between',
|
|
476
|
+
marginBottom: '4px'
|
|
477
|
+
}}>
|
|
478
|
+
<span style={{ fontWeight: 'bold' }}>
|
|
479
|
+
{resp.name || `响应 ${index + 1}`}
|
|
480
|
+
</span>
|
|
481
|
+
<Space size={4}>
|
|
482
|
+
{matcherCount > 0 ? (
|
|
483
|
+
<Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>
|
|
484
|
+
{matcherCount}条规则
|
|
485
|
+
</Tag>
|
|
486
|
+
) : (
|
|
487
|
+
<Tag color="default" style={{ fontSize: '11px', margin: 0 }}>
|
|
488
|
+
兜底
|
|
489
|
+
</Tag>
|
|
490
|
+
)}
|
|
491
|
+
{resp.id === activeResponseId && (
|
|
492
|
+
<Badge status="processing" text="当前选中" />
|
|
493
|
+
)}
|
|
494
|
+
</Space>
|
|
495
|
+
</div>
|
|
443
496
|
</div>
|
|
444
|
-
</
|
|
445
|
-
|
|
446
|
-
)
|
|
497
|
+
</Option>
|
|
498
|
+
);
|
|
499
|
+
})}
|
|
447
500
|
</Select>
|
|
448
501
|
<Tooltip title="编辑响应名称">
|
|
449
502
|
<Button
|
|
@@ -484,6 +537,101 @@ const ResponseContentEditor = ({
|
|
|
484
537
|
</Row>
|
|
485
538
|
</div>
|
|
486
539
|
|
|
540
|
+
{/* 入参匹配规则编辑区域 */}
|
|
541
|
+
{activeResponse && (
|
|
542
|
+
<div style={{
|
|
543
|
+
marginBottom: '16px',
|
|
544
|
+
border: '1px solid #e8e8e8',
|
|
545
|
+
borderRadius: '6px',
|
|
546
|
+
padding: '12px',
|
|
547
|
+
background: '#fafafa'
|
|
548
|
+
}}>
|
|
549
|
+
<div style={{
|
|
550
|
+
display: 'flex',
|
|
551
|
+
alignItems: 'center',
|
|
552
|
+
justifyContent: 'space-between',
|
|
553
|
+
marginBottom: '10px'
|
|
554
|
+
}}>
|
|
555
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
556
|
+
<FilterOutlined style={{ color: '#1890ff' }} />
|
|
557
|
+
<label style={{ fontSize: '12px', color: '#333', fontWeight: 'bold' }}>
|
|
558
|
+
入参匹配规则
|
|
559
|
+
</label>
|
|
560
|
+
{activeResponse.paramMatchers && activeResponse.paramMatchers.length > 0 ? (
|
|
561
|
+
<Tag color="blue" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
|
|
562
|
+
{activeResponse.paramMatchers.length}条
|
|
563
|
+
</Tag>
|
|
564
|
+
) : (
|
|
565
|
+
<Tag color="default" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
|
|
566
|
+
兜底响应
|
|
567
|
+
</Tag>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
<Button
|
|
571
|
+
type="dashed"
|
|
572
|
+
size="small"
|
|
573
|
+
icon={<PlusCircleOutlined />}
|
|
574
|
+
onClick={handleAddMatcher}
|
|
575
|
+
style={{ fontSize: '12px' }}
|
|
576
|
+
>
|
|
577
|
+
添加规则
|
|
578
|
+
</Button>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{(!activeResponse.paramMatchers || activeResponse.paramMatchers.length === 0) && (
|
|
582
|
+
<div style={{
|
|
583
|
+
color: '#999',
|
|
584
|
+
fontSize: '12px',
|
|
585
|
+
textAlign: 'center',
|
|
586
|
+
padding: '8px 0'
|
|
587
|
+
}}>
|
|
588
|
+
无匹配规则,当其他响应均不匹配时作为兜底返回
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
591
|
+
|
|
592
|
+
{activeResponse.paramMatchers && activeResponse.paramMatchers.map((matcher, index) => (
|
|
593
|
+
<Row key={index} gutter={8} style={{ marginBottom: '8px' }} align="middle">
|
|
594
|
+
<Col span={8}>
|
|
595
|
+
<Input
|
|
596
|
+
size="small"
|
|
597
|
+
placeholder="参数路径,如 data.type"
|
|
598
|
+
value={matcher.paramPath}
|
|
599
|
+
onChange={(e) => handleMatcherChange(index, 'paramPath', e.target.value)}
|
|
600
|
+
/>
|
|
601
|
+
</Col>
|
|
602
|
+
<Col span={5}>
|
|
603
|
+
<Select
|
|
604
|
+
size="small"
|
|
605
|
+
style={{ width: '100%' }}
|
|
606
|
+
value={matcher.matchType || 'exact'}
|
|
607
|
+
onChange={(val) => handleMatcherChange(index, 'matchType', val)}
|
|
608
|
+
>
|
|
609
|
+
<Option value="exact">精确匹配</Option>
|
|
610
|
+
<Option value="contains">包含</Option>
|
|
611
|
+
<Option value="regex">正则</Option>
|
|
612
|
+
</Select>
|
|
613
|
+
</Col>
|
|
614
|
+
<Col span={9}>
|
|
615
|
+
<Input
|
|
616
|
+
size="small"
|
|
617
|
+
placeholder="期望值"
|
|
618
|
+
value={matcher.paramValue}
|
|
619
|
+
onChange={(e) => handleMatcherChange(index, 'paramValue', e.target.value)}
|
|
620
|
+
/>
|
|
621
|
+
</Col>
|
|
622
|
+
<Col span={2} style={{ textAlign: 'center' }}>
|
|
623
|
+
<Tooltip title="删除此规则">
|
|
624
|
+
<MinusCircleOutlined
|
|
625
|
+
style={{ color: '#ff4d4f', cursor: 'pointer', fontSize: '16px' }}
|
|
626
|
+
onClick={() => handleDeleteMatcher(index)}
|
|
627
|
+
/>
|
|
628
|
+
</Tooltip>
|
|
629
|
+
</Col>
|
|
630
|
+
</Row>
|
|
631
|
+
))}
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
|
|
487
635
|
{/* 响应内容编辑区域 */}
|
|
488
636
|
{activeResponse && (
|
|
489
637
|
<>
|
|
@@ -2,6 +2,5 @@
|
|
|
2
2
|
export { default as ResponseContentEditor } from './ResponseContentEditor';
|
|
3
3
|
export { default as InterfaceTestModal } from './InterfaceTestModal';
|
|
4
4
|
export { default as PreviewModal } from './PreviewModal';
|
|
5
|
-
export { default as DataManagementDrawer } from './DataManagementDrawer';
|
|
6
5
|
export * from './constants';
|
|
7
6
|
export * from './utils';
|