whistle.mock-plugins 1.2.2-beta.2 → 1.2.2-beta.22
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 +50 -1
- package/app/cgi-bin/settings.js +37 -0
- package/app/public/js/index.js +1 -1
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +105 -41
- package/lib/server.js +4 -0
- package/package.json +1 -1
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +146 -36
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/pages/InterfaceManagementV2.js +448 -145
- package/ui/src/pages/MockDataV2.js +1 -1
- package/ui/src/pages/Settings.js +143 -6
- package/ui/src/styles/interface-management.css +3 -12
- package/ui/src/styles/settings.css +29 -0
|
@@ -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,
|
|
4
|
+
import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, Tag, Divider } from 'antd';
|
|
5
5
|
import {
|
|
6
6
|
CodeOutlined,
|
|
7
7
|
EyeOutlined,
|
|
@@ -13,13 +13,22 @@ import {
|
|
|
13
13
|
EditOutlined,
|
|
14
14
|
FullscreenOutlined,
|
|
15
15
|
FullscreenExitOutlined,
|
|
16
|
-
MenuOutlined
|
|
16
|
+
MenuOutlined,
|
|
17
|
+
FilterOutlined,
|
|
18
|
+
PlusOutlined,
|
|
19
|
+
QuestionCircleOutlined
|
|
17
20
|
} from '@ant-design/icons';
|
|
18
21
|
import { generateResponseId } from './utils';
|
|
19
22
|
|
|
20
23
|
const { TextArea } = Input;
|
|
21
24
|
const { Option } = Select;
|
|
22
25
|
|
|
26
|
+
const MATCH_TYPE_LABELS = {
|
|
27
|
+
exact: '精确',
|
|
28
|
+
contains: '包含',
|
|
29
|
+
regex: '正则',
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
const ResponseContentEditor = ({
|
|
24
33
|
form,
|
|
25
34
|
responses,
|
|
@@ -33,6 +42,20 @@ const ResponseContentEditor = ({
|
|
|
33
42
|
const [isTextareaFullscreen, setIsTextareaFullscreen] = useState(false);
|
|
34
43
|
const [showSorting, setShowSorting] = useState(false);
|
|
35
44
|
|
|
45
|
+
const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(() => {
|
|
46
|
+
const v = localStorage.getItem('responseParamMatcherEnabled');
|
|
47
|
+
return v === null ? true : v !== 'false';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const handleStorage = () => {
|
|
52
|
+
const v = localStorage.getItem('responseParamMatcherEnabled');
|
|
53
|
+
setResponseParamMatcherEnabled(v === null ? true : v !== 'false');
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('storage', handleStorage);
|
|
56
|
+
return () => window.removeEventListener('storage', handleStorage);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
36
59
|
// 查找激活的响应
|
|
37
60
|
const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
|
|
38
61
|
const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
|
|
@@ -46,7 +69,6 @@ const ResponseContentEditor = ({
|
|
|
46
69
|
}, [responses]);
|
|
47
70
|
|
|
48
71
|
const handleAfterSort = (newList) => {
|
|
49
|
-
// 将排序后的列表写回表单 responses 字段;activeResponseId 不变
|
|
50
72
|
setFieldsValue({ responses: newList });
|
|
51
73
|
};
|
|
52
74
|
|
|
@@ -70,7 +92,6 @@ const ResponseContentEditor = ({
|
|
|
70
92
|
const newResponseId = generateResponseId();
|
|
71
93
|
let currentResponses = getFieldValue('responses') || [];
|
|
72
94
|
|
|
73
|
-
// 确保数组格式
|
|
74
95
|
if (!Array.isArray(currentResponses)) {
|
|
75
96
|
try {
|
|
76
97
|
if (typeof currentResponses === 'string') {
|
|
@@ -89,12 +110,12 @@ const ResponseContentEditor = ({
|
|
|
89
110
|
id: newResponseId,
|
|
90
111
|
name: `响应 ${currentResponses.length + 1}`,
|
|
91
112
|
description: '',
|
|
92
|
-
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
113
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
|
|
114
|
+
paramMatchers: []
|
|
93
115
|
};
|
|
94
116
|
|
|
95
117
|
const updatedResponses = [...currentResponses, newResponse];
|
|
96
118
|
|
|
97
|
-
// 更新表单字段
|
|
98
119
|
setFieldsValue({
|
|
99
120
|
responses: updatedResponses,
|
|
100
121
|
activeResponseId: newResponseId
|
|
@@ -112,7 +133,6 @@ const ResponseContentEditor = ({
|
|
|
112
133
|
|
|
113
134
|
const updatedResponses = currentResponses.filter(resp => resp.id !== responseId);
|
|
114
135
|
|
|
115
|
-
// 如果删除的是当前选中的响应,切换到第一个响应
|
|
116
136
|
let newActiveId = activeResponseId;
|
|
117
137
|
if (responseId === activeResponseId) {
|
|
118
138
|
newActiveId = updatedResponses[0]?.id;
|
|
@@ -170,6 +190,44 @@ const ResponseContentEditor = ({
|
|
|
170
190
|
}
|
|
171
191
|
};
|
|
172
192
|
|
|
193
|
+
// 更新当前响应的 paramMatchers
|
|
194
|
+
const updateActiveResponseParamMatchers = (paramMatchers) => {
|
|
195
|
+
if (activeResponseIndex >= 0) {
|
|
196
|
+
const currentResponses = getFieldValue('responses') || [];
|
|
197
|
+
const newResponses = [...currentResponses];
|
|
198
|
+
newResponses[activeResponseIndex] = {
|
|
199
|
+
...newResponses[activeResponseIndex],
|
|
200
|
+
paramMatchers
|
|
201
|
+
};
|
|
202
|
+
setFieldsValue({ responses: newResponses });
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// 添加入参匹配规则
|
|
207
|
+
const handleAddParamMatcher = () => {
|
|
208
|
+
const currentMatchers = (activeResponse && activeResponse.paramMatchers) || [];
|
|
209
|
+
updateActiveResponseParamMatchers([
|
|
210
|
+
...currentMatchers,
|
|
211
|
+
{ paramPath: '', paramValue: '', matchType: 'exact' }
|
|
212
|
+
]);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// 删除入参匹配规则
|
|
216
|
+
const handleRemoveParamMatcher = (index) => {
|
|
217
|
+
const currentMatchers = (activeResponse && activeResponse.paramMatchers) || [];
|
|
218
|
+
const newMatchers = currentMatchers.filter((_, i) => i !== index);
|
|
219
|
+
updateActiveResponseParamMatchers(newMatchers);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 更新入参匹配规则某个字段
|
|
223
|
+
const handleParamMatcherChange = (index, field, value) => {
|
|
224
|
+
const currentMatchers = (activeResponse && activeResponse.paramMatchers) || [];
|
|
225
|
+
const newMatchers = currentMatchers.map((m, i) =>
|
|
226
|
+
i === index ? { ...m, [field]: value } : m
|
|
227
|
+
);
|
|
228
|
+
updateActiveResponseParamMatchers(newMatchers);
|
|
229
|
+
};
|
|
230
|
+
|
|
173
231
|
// 格式化JSON内容
|
|
174
232
|
const formatJsonContent = () => {
|
|
175
233
|
if (!activeResponse || !activeResponse.content) {
|
|
@@ -233,6 +291,81 @@ const ResponseContentEditor = ({
|
|
|
233
291
|
);
|
|
234
292
|
};
|
|
235
293
|
|
|
294
|
+
// 渲染响应级入参匹配规则区域
|
|
295
|
+
const renderParamMatchers = () => {
|
|
296
|
+
if (!activeResponse) return null;
|
|
297
|
+
|
|
298
|
+
const matchers = activeResponse.paramMatchers || [];
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div style={{ marginBottom: '12px' }}>
|
|
302
|
+
<Divider orientation="left" orientationMargin={0} style={{ margin: '0 0 10px 0', fontSize: '12px' }}>
|
|
303
|
+
<Space size={6}>
|
|
304
|
+
<FilterOutlined style={{ color: '#1890ff' }} />
|
|
305
|
+
<span style={{ fontWeight: 'bold', color: '#333' }}>入参匹配规则</span>
|
|
306
|
+
{matchers.length > 0 && (
|
|
307
|
+
<Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matchers.length}条</Tag>
|
|
308
|
+
)}
|
|
309
|
+
<Tooltip title="满足所有规则时返回此响应;无规则的响应作为默认兜底。响应按列表顺序依次检查,第一个匹配的响应被返回。">
|
|
310
|
+
<QuestionCircleOutlined style={{ color: '#bbb', cursor: 'help', fontSize: '12px' }} />
|
|
311
|
+
</Tooltip>
|
|
312
|
+
</Space>
|
|
313
|
+
</Divider>
|
|
314
|
+
<div>
|
|
315
|
+
{matchers.map((matcher, index) => (
|
|
316
|
+
<div key={index} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
317
|
+
<Input
|
|
318
|
+
style={{ flex: 1 }}
|
|
319
|
+
placeholder="参数路径(如:userId 或 data.user.id)"
|
|
320
|
+
value={matcher.paramPath || ''}
|
|
321
|
+
onChange={(e) => handleParamMatcherChange(index, 'paramPath', e.target.value)}
|
|
322
|
+
size="small"
|
|
323
|
+
/>
|
|
324
|
+
<Input
|
|
325
|
+
style={{ flex: 1 }}
|
|
326
|
+
placeholder="期望值(如:123 或 admin)"
|
|
327
|
+
value={matcher.paramValue || ''}
|
|
328
|
+
onChange={(e) => handleParamMatcherChange(index, 'paramValue', e.target.value)}
|
|
329
|
+
size="small"
|
|
330
|
+
/>
|
|
331
|
+
<Select
|
|
332
|
+
style={{ width: 90 }}
|
|
333
|
+
value={matcher.matchType || 'exact'}
|
|
334
|
+
onChange={(val) => handleParamMatcherChange(index, 'matchType', val)}
|
|
335
|
+
size="small"
|
|
336
|
+
>
|
|
337
|
+
<Option value="exact">精确匹配</Option>
|
|
338
|
+
<Option value="contains">包含</Option>
|
|
339
|
+
<Option value="regex">正则</Option>
|
|
340
|
+
</Select>
|
|
341
|
+
<Button
|
|
342
|
+
type="text"
|
|
343
|
+
danger
|
|
344
|
+
icon={<DeleteOutlined />}
|
|
345
|
+
size="small"
|
|
346
|
+
onClick={() => handleRemoveParamMatcher(index)}
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
))}
|
|
350
|
+
<Button
|
|
351
|
+
type="dashed"
|
|
352
|
+
size="small"
|
|
353
|
+
icon={<PlusOutlined />}
|
|
354
|
+
onClick={handleAddParamMatcher}
|
|
355
|
+
style={{ width: '100%' }}
|
|
356
|
+
>
|
|
357
|
+
添加匹配规则
|
|
358
|
+
</Button>
|
|
359
|
+
{matchers.length === 0 && (
|
|
360
|
+
<div style={{ color: '#bbb', fontSize: '12px', marginTop: '6px', textAlign: 'center' }}>
|
|
361
|
+
无规则时此响应作为默认兜底响应
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
|
|
236
369
|
// 如果没有响应数据,显示创建按钮
|
|
237
370
|
if (!Array.isArray(responses) || responses.length === 0) {
|
|
238
371
|
return (
|
|
@@ -383,7 +516,6 @@ const ResponseContentEditor = ({
|
|
|
383
516
|
|
|
384
517
|
<div style={{ position: 'relative' }}>
|
|
385
518
|
{isEditing ? (
|
|
386
|
-
// 编辑模式:显示输入框
|
|
387
519
|
<Input.Group compact>
|
|
388
520
|
<Input
|
|
389
521
|
style={{ width: 'calc(100% - 80px)' }}
|
|
@@ -407,41 +539,16 @@ const ResponseContentEditor = ({
|
|
|
407
539
|
/>
|
|
408
540
|
</Input.Group>
|
|
409
541
|
) : (
|
|
410
|
-
// 选择模式:显示下拉选择器
|
|
411
542
|
<Input.Group compact>
|
|
412
543
|
<Select
|
|
413
544
|
style={{ width: 'calc(100% - 40px)' }}
|
|
414
545
|
placeholder="选择要编辑的响应"
|
|
415
546
|
value={activeResponseId}
|
|
416
547
|
onChange={handleResponseSelect}
|
|
417
|
-
optionLabelProp="label"
|
|
418
548
|
>
|
|
419
549
|
{responses.map((resp, index) => (
|
|
420
|
-
<Option
|
|
421
|
-
|
|
422
|
-
value={resp.id}
|
|
423
|
-
label={
|
|
424
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
425
|
-
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
|
426
|
-
<span>{resp.name || `响应 ${index + 1}`}</span>
|
|
427
|
-
</div>
|
|
428
|
-
}
|
|
429
|
-
>
|
|
430
|
-
<div style={{ padding: '4px 0' }}>
|
|
431
|
-
<div style={{
|
|
432
|
-
display: 'flex',
|
|
433
|
-
alignItems: 'center',
|
|
434
|
-
justifyContent: 'space-between',
|
|
435
|
-
marginBottom: '4px'
|
|
436
|
-
}}>
|
|
437
|
-
<span style={{ fontWeight: 'bold' }}>
|
|
438
|
-
{resp.name || `响应 ${index + 1}`}
|
|
439
|
-
</span>
|
|
440
|
-
{resp.id === activeResponseId && (
|
|
441
|
-
<Badge status="processing" text="当前选中" />
|
|
442
|
-
)}
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
550
|
+
<Option key={resp.id} value={resp.id}>
|
|
551
|
+
{resp.name || `响应 ${index + 1}`}
|
|
445
552
|
</Option>
|
|
446
553
|
))}
|
|
447
554
|
</Select>
|
|
@@ -484,6 +591,9 @@ const ResponseContentEditor = ({
|
|
|
484
591
|
</Row>
|
|
485
592
|
</div>
|
|
486
593
|
|
|
594
|
+
{/* 响应级入参匹配规则区域 */}
|
|
595
|
+
{activeResponse && responseParamMatcherEnabled && renderParamMatchers()}
|
|
596
|
+
|
|
487
597
|
{/* 响应内容编辑区域 */}
|
|
488
598
|
{activeResponse && (
|
|
489
599
|
<>
|
|
@@ -535,4 +645,4 @@ const ResponseContentEditor = ({
|
|
|
535
645
|
);
|
|
536
646
|
};
|
|
537
647
|
|
|
538
|
-
export default ResponseContentEditor;
|
|
648
|
+
export default ResponseContentEditor;
|
|
@@ -2,5 +2,6 @@
|
|
|
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';
|
|
5
6
|
export * from './constants';
|
|
6
7
|
export * from './utils';
|