whistle.mock-plugins 1.2.2-beta.9 → 1.3.0
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 +57 -3
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/settings.js +37 -0
- package/app/cgi-bin/stats.js +149 -0
- package/app/public/index.html +1 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +25 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +122 -63
- package/lib/server.js +4 -0
- package/lib/ui-server.js +20 -4
- package/package.json +2 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +317 -80
- package/ui/src/components/FeatureCard.js +7 -2
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/VersionModal.js +4 -41
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/PreviewModal.js +4 -6
- package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +123 -177
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +12 -2
- package/ui/src/pages/InterfaceManagementV2.js +563 -147
- package/ui/src/pages/MockDataV2.js +21 -13
- package/ui/src/pages/Settings.js +275 -8
- package/ui/src/pages/Sniffer.js +521 -0
- package/ui/src/pages/Stats.js +288 -0
- package/ui/src/styles/cyber-theme.css +1234 -0
- package/ui/src/styles/earth-theme.css +618 -0
- package/ui/src/styles/interface-management.css +37 -1
- package/ui/src/styles/settings.css +45 -1
|
@@ -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,
|
|
@@ -15,13 +15,20 @@ import {
|
|
|
15
15
|
FullscreenExitOutlined,
|
|
16
16
|
MenuOutlined,
|
|
17
17
|
FilterOutlined,
|
|
18
|
-
|
|
18
|
+
PlusOutlined,
|
|
19
|
+
QuestionCircleOutlined
|
|
19
20
|
} from '@ant-design/icons';
|
|
20
21
|
import { generateResponseId } from './utils';
|
|
21
22
|
|
|
22
23
|
const { TextArea } = Input;
|
|
23
24
|
const { Option } = Select;
|
|
24
25
|
|
|
26
|
+
const MATCH_TYPE_LABELS = {
|
|
27
|
+
exact: '精确',
|
|
28
|
+
contains: '包含',
|
|
29
|
+
regex: '正则',
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
const ResponseContentEditor = ({
|
|
26
33
|
form,
|
|
27
34
|
responses,
|
|
@@ -35,6 +42,20 @@ const ResponseContentEditor = ({
|
|
|
35
42
|
const [isTextareaFullscreen, setIsTextareaFullscreen] = useState(false);
|
|
36
43
|
const [showSorting, setShowSorting] = useState(false);
|
|
37
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
|
+
|
|
38
59
|
// 查找激活的响应
|
|
39
60
|
const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
|
|
40
61
|
const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
|
|
@@ -48,7 +69,6 @@ const ResponseContentEditor = ({
|
|
|
48
69
|
}, [responses]);
|
|
49
70
|
|
|
50
71
|
const handleAfterSort = (newList) => {
|
|
51
|
-
// 将排序后的列表写回表单 responses 字段;activeResponseId 不变
|
|
52
72
|
setFieldsValue({ responses: newList });
|
|
53
73
|
};
|
|
54
74
|
|
|
@@ -72,7 +92,6 @@ const ResponseContentEditor = ({
|
|
|
72
92
|
const newResponseId = generateResponseId();
|
|
73
93
|
let currentResponses = getFieldValue('responses') || [];
|
|
74
94
|
|
|
75
|
-
// 确保数组格式
|
|
76
95
|
if (!Array.isArray(currentResponses)) {
|
|
77
96
|
try {
|
|
78
97
|
if (typeof currentResponses === 'string') {
|
|
@@ -97,7 +116,6 @@ const ResponseContentEditor = ({
|
|
|
97
116
|
|
|
98
117
|
const updatedResponses = [...currentResponses, newResponse];
|
|
99
118
|
|
|
100
|
-
// 更新表单字段
|
|
101
119
|
setFieldsValue({
|
|
102
120
|
responses: updatedResponses,
|
|
103
121
|
activeResponseId: newResponseId
|
|
@@ -115,7 +133,6 @@ const ResponseContentEditor = ({
|
|
|
115
133
|
|
|
116
134
|
const updatedResponses = currentResponses.filter(resp => resp.id !== responseId);
|
|
117
135
|
|
|
118
|
-
// 如果删除的是当前选中的响应,切换到第一个响应
|
|
119
136
|
let newActiveId = activeResponseId;
|
|
120
137
|
if (responseId === activeResponseId) {
|
|
121
138
|
newActiveId = updatedResponses[0]?.id;
|
|
@@ -186,132 +203,29 @@ const ResponseContentEditor = ({
|
|
|
186
203
|
}
|
|
187
204
|
};
|
|
188
205
|
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
const currentMatchers = activeResponse
|
|
206
|
+
// 添加入参匹配规则
|
|
207
|
+
const handleAddParamMatcher = () => {
|
|
208
|
+
const currentMatchers = (activeResponse && activeResponse.paramMatchers) || [];
|
|
192
209
|
updateActiveResponseParamMatchers([
|
|
193
210
|
...currentMatchers,
|
|
194
211
|
{ paramPath: '', paramValue: '', matchType: 'exact' }
|
|
195
212
|
]);
|
|
196
213
|
};
|
|
197
214
|
|
|
198
|
-
//
|
|
199
|
-
const
|
|
200
|
-
const currentMatchers =
|
|
201
|
-
currentMatchers.
|
|
202
|
-
updateActiveResponseParamMatchers(
|
|
215
|
+
// 删除入参匹配规则
|
|
216
|
+
const handleRemoveParamMatcher = (index) => {
|
|
217
|
+
const currentMatchers = (activeResponse && activeResponse.paramMatchers) || [];
|
|
218
|
+
const newMatchers = currentMatchers.filter((_, i) => i !== index);
|
|
219
|
+
updateActiveResponseParamMatchers(newMatchers);
|
|
203
220
|
};
|
|
204
221
|
|
|
205
|
-
//
|
|
206
|
-
const
|
|
207
|
-
const currentMatchers =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// 渲染入参匹配规则编辑区域
|
|
213
|
-
const renderParamMatchersSection = () => {
|
|
214
|
-
// 整个接口中是否有任意响应含有匹配规则
|
|
215
|
-
const anyHasMatchers = responses.some(
|
|
216
|
-
r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0
|
|
217
|
-
);
|
|
218
|
-
const currentHasMatchers = activeResponse.paramMatchers && activeResponse.paramMatchers.length > 0;
|
|
219
|
-
// 只有当接口中存在规则时,无规则的响应才叫"兜底"
|
|
220
|
-
const isFallback = anyHasMatchers && !currentHasMatchers;
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div style={{
|
|
224
|
-
marginBottom: '16px',
|
|
225
|
-
border: '1px solid #e8e8e8',
|
|
226
|
-
borderRadius: '6px',
|
|
227
|
-
padding: '12px',
|
|
228
|
-
background: '#fafafa'
|
|
229
|
-
}}>
|
|
230
|
-
<div style={{
|
|
231
|
-
display: 'flex',
|
|
232
|
-
alignItems: 'center',
|
|
233
|
-
justifyContent: 'space-between',
|
|
234
|
-
marginBottom: '10px'
|
|
235
|
-
}}>
|
|
236
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
237
|
-
<FilterOutlined style={{ color: '#1890ff' }} />
|
|
238
|
-
<label style={{ fontSize: '12px', color: '#333', fontWeight: 'bold' }}>
|
|
239
|
-
入参匹配规则
|
|
240
|
-
</label>
|
|
241
|
-
{currentHasMatchers ? (
|
|
242
|
-
<Tag color="blue" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
|
|
243
|
-
{activeResponse.paramMatchers.length}条
|
|
244
|
-
</Tag>
|
|
245
|
-
) : isFallback ? (
|
|
246
|
-
<Tag color="default" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
|
|
247
|
-
兜底响应
|
|
248
|
-
</Tag>
|
|
249
|
-
) : null}
|
|
250
|
-
</div>
|
|
251
|
-
<Button
|
|
252
|
-
type="dashed"
|
|
253
|
-
size="small"
|
|
254
|
-
icon={<PlusCircleOutlined />}
|
|
255
|
-
onClick={handleAddMatcher}
|
|
256
|
-
style={{ fontSize: '12px' }}
|
|
257
|
-
>
|
|
258
|
-
添加规则
|
|
259
|
-
</Button>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
{isFallback && (
|
|
263
|
-
<div style={{
|
|
264
|
-
color: '#999',
|
|
265
|
-
fontSize: '12px',
|
|
266
|
-
textAlign: 'center',
|
|
267
|
-
padding: '8px 0'
|
|
268
|
-
}}>
|
|
269
|
-
无匹配规则,当其他响应均不匹配时作为兜底返回
|
|
270
|
-
</div>
|
|
271
|
-
)}
|
|
272
|
-
|
|
273
|
-
{activeResponse.paramMatchers && activeResponse.paramMatchers.map((matcher, index) => (
|
|
274
|
-
<Row key={index} gutter={8} style={{ marginBottom: '8px' }} align="middle">
|
|
275
|
-
<Col span={8}>
|
|
276
|
-
<Input
|
|
277
|
-
size="small"
|
|
278
|
-
placeholder="参数路径,如 data.type"
|
|
279
|
-
value={matcher.paramPath}
|
|
280
|
-
onChange={(e) => handleMatcherChange(index, 'paramPath', e.target.value)}
|
|
281
|
-
/>
|
|
282
|
-
</Col>
|
|
283
|
-
<Col span={5}>
|
|
284
|
-
<Select
|
|
285
|
-
size="small"
|
|
286
|
-
style={{ width: '100%' }}
|
|
287
|
-
value={matcher.matchType || 'exact'}
|
|
288
|
-
onChange={(val) => handleMatcherChange(index, 'matchType', val)}
|
|
289
|
-
>
|
|
290
|
-
<Option value="exact">精确匹配</Option>
|
|
291
|
-
<Option value="contains">包含</Option>
|
|
292
|
-
<Option value="regex">正则</Option>
|
|
293
|
-
</Select>
|
|
294
|
-
</Col>
|
|
295
|
-
<Col span={9}>
|
|
296
|
-
<Input
|
|
297
|
-
size="small"
|
|
298
|
-
placeholder="期望值"
|
|
299
|
-
value={matcher.paramValue}
|
|
300
|
-
onChange={(e) => handleMatcherChange(index, 'paramValue', e.target.value)}
|
|
301
|
-
/>
|
|
302
|
-
</Col>
|
|
303
|
-
<Col span={2} style={{ textAlign: 'center' }}>
|
|
304
|
-
<Tooltip title="删除此规则">
|
|
305
|
-
<MinusCircleOutlined
|
|
306
|
-
style={{ color: '#ff4d4f', cursor: 'pointer', fontSize: '16px' }}
|
|
307
|
-
onClick={() => handleDeleteMatcher(index)}
|
|
308
|
-
/>
|
|
309
|
-
</Tooltip>
|
|
310
|
-
</Col>
|
|
311
|
-
</Row>
|
|
312
|
-
))}
|
|
313
|
-
</div>
|
|
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
|
|
314
227
|
);
|
|
228
|
+
updateActiveResponseParamMatchers(newMatchers);
|
|
315
229
|
};
|
|
316
230
|
|
|
317
231
|
// 格式化JSON内容
|
|
@@ -360,7 +274,7 @@ const ResponseContentEditor = ({
|
|
|
360
274
|
onClick={toggleTextareaFullscreen}
|
|
361
275
|
size="small"
|
|
362
276
|
type="text"
|
|
363
|
-
style={{ color: '#
|
|
277
|
+
style={{ color: '#595959' }}
|
|
364
278
|
>
|
|
365
279
|
退出全屏 (ESC)
|
|
366
280
|
</Button>
|
|
@@ -377,6 +291,81 @@ const ResponseContentEditor = ({
|
|
|
377
291
|
);
|
|
378
292
|
};
|
|
379
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: '#1677ff' }} />
|
|
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
|
+
|
|
380
369
|
// 如果没有响应数据,显示创建按钮
|
|
381
370
|
if (!Array.isArray(responses) || responses.length === 0) {
|
|
382
371
|
return (
|
|
@@ -393,11 +382,10 @@ const ResponseContentEditor = ({
|
|
|
393
382
|
<div style={{
|
|
394
383
|
textAlign: 'center',
|
|
395
384
|
padding: '40px 20px',
|
|
396
|
-
background: '#fafafa',
|
|
397
385
|
borderRadius: '6px',
|
|
398
386
|
border: '1px dashed #d9d9d9'
|
|
399
387
|
}}>
|
|
400
|
-
<FileTextOutlined style={{ fontSize: '32px', color: '#
|
|
388
|
+
<FileTextOutlined style={{ fontSize: '32px', color: '#bfbfbf', marginBottom: '12px' }} />
|
|
401
389
|
<div style={{ color: '#999', marginBottom: '16px' }}>暂无响应数据</div>
|
|
402
390
|
<Button
|
|
403
391
|
type="primary"
|
|
@@ -444,9 +432,7 @@ const ResponseContentEditor = ({
|
|
|
444
432
|
style={{
|
|
445
433
|
display: 'flex',
|
|
446
434
|
alignItems: 'center',
|
|
447
|
-
gap: '4px'
|
|
448
|
-
borderColor: '#1890ff',
|
|
449
|
-
color: '#1890ff'
|
|
435
|
+
gap: '4px'
|
|
450
436
|
}}
|
|
451
437
|
>
|
|
452
438
|
添加响应
|
|
@@ -527,7 +513,6 @@ const ResponseContentEditor = ({
|
|
|
527
513
|
|
|
528
514
|
<div style={{ position: 'relative' }}>
|
|
529
515
|
{isEditing ? (
|
|
530
|
-
// 编辑模式:显示输入框
|
|
531
516
|
<Input.Group compact>
|
|
532
517
|
<Input
|
|
533
518
|
style={{ width: 'calc(100% - 80px)' }}
|
|
@@ -551,57 +536,18 @@ const ResponseContentEditor = ({
|
|
|
551
536
|
/>
|
|
552
537
|
</Input.Group>
|
|
553
538
|
) : (
|
|
554
|
-
// 选择模式:显示下拉选择器
|
|
555
539
|
<Input.Group compact>
|
|
556
540
|
<Select
|
|
557
541
|
style={{ width: 'calc(100% - 40px)' }}
|
|
558
542
|
placeholder="选择要编辑的响应"
|
|
559
543
|
value={activeResponseId}
|
|
560
544
|
onChange={handleResponseSelect}
|
|
561
|
-
optionLabelProp="label"
|
|
562
545
|
>
|
|
563
|
-
{responses.map((resp, index) =>
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
value={resp.id}
|
|
569
|
-
label={
|
|
570
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
571
|
-
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
|
572
|
-
<span>{resp.name || `响应 ${index + 1}`}</span>
|
|
573
|
-
</div>
|
|
574
|
-
}
|
|
575
|
-
>
|
|
576
|
-
<div style={{ padding: '4px 0' }}>
|
|
577
|
-
<div style={{
|
|
578
|
-
display: 'flex',
|
|
579
|
-
alignItems: 'center',
|
|
580
|
-
justifyContent: 'space-between',
|
|
581
|
-
marginBottom: '4px'
|
|
582
|
-
}}>
|
|
583
|
-
<span style={{ fontWeight: 'bold' }}>
|
|
584
|
-
{resp.name || `响应 ${index + 1}`}
|
|
585
|
-
</span>
|
|
586
|
-
<Space size={4}>
|
|
587
|
-
{matcherCount > 0 ? (
|
|
588
|
-
<Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>
|
|
589
|
-
{matcherCount}条规则
|
|
590
|
-
</Tag>
|
|
591
|
-
) : (
|
|
592
|
-
<Tag color="default" style={{ fontSize: '11px', margin: 0 }}>
|
|
593
|
-
兜底
|
|
594
|
-
</Tag>
|
|
595
|
-
)}
|
|
596
|
-
{resp.id === activeResponseId && (
|
|
597
|
-
<Badge status="processing" text="当前选中" />
|
|
598
|
-
)}
|
|
599
|
-
</Space>
|
|
600
|
-
</div>
|
|
601
|
-
</div>
|
|
602
|
-
</Option>
|
|
603
|
-
);
|
|
604
|
-
})}
|
|
546
|
+
{responses.map((resp, index) => (
|
|
547
|
+
<Option key={resp.id} value={resp.id}>
|
|
548
|
+
{resp.name || `响应 ${index + 1}`}
|
|
549
|
+
</Option>
|
|
550
|
+
))}
|
|
605
551
|
</Select>
|
|
606
552
|
<Tooltip title="编辑响应名称">
|
|
607
553
|
<Button
|
|
@@ -642,8 +588,8 @@ const ResponseContentEditor = ({
|
|
|
642
588
|
</Row>
|
|
643
589
|
</div>
|
|
644
590
|
|
|
645
|
-
{/*
|
|
646
|
-
{activeResponse &&
|
|
591
|
+
{/* 响应级入参匹配规则区域 */}
|
|
592
|
+
{activeResponse && responseParamMatcherEnabled && renderParamMatchers()}
|
|
647
593
|
|
|
648
594
|
{/* 响应内容编辑区域 */}
|
|
649
595
|
{activeResponse && (
|
|
@@ -696,4 +642,4 @@ const ResponseContentEditor = ({
|
|
|
696
642
|
);
|
|
697
643
|
};
|
|
698
644
|
|
|
699
|
-
export default ResponseContentEditor;
|
|
645
|
+
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';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import { useHistory } from 'react-router-dom';
|
|
3
|
+
import { message } from 'antd';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 全局键盘快捷键 Hook
|
|
7
|
+
*
|
|
8
|
+
* 支持的快捷键:
|
|
9
|
+
* - Esc: 关闭当前弹窗/抽屉(触发全局 close-modal 事件)
|
|
10
|
+
* - Ctrl/Cmd + S: 保存(触发全局 save-form 事件)
|
|
11
|
+
* - Ctrl/Cmd + 1: 跳转到功能模块
|
|
12
|
+
* - Ctrl/Cmd + 2: 跳转到接口管理
|
|
13
|
+
* - Ctrl/Cmd + 3: 跳转到系统设置
|
|
14
|
+
* - Ctrl/Cmd + 4: 跳转到请求统计
|
|
15
|
+
* - Ctrl/Cmd + 5: 跳转到接口嗅探
|
|
16
|
+
* - Ctrl/Cmd + Shift + N: 新建接口(在接口管理页触发)
|
|
17
|
+
* - Ctrl/Cmd + K: 聚焦搜索框(触发全局 focus-search 事件)
|
|
18
|
+
* - Shift + ?: 显示快捷键帮助
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SHORTCUTS = [
|
|
22
|
+
{ key: 'Esc', desc: '关闭弹窗/抽屉' },
|
|
23
|
+
{ key: 'Ctrl + S', desc: '保存表单' },
|
|
24
|
+
{ key: 'Ctrl + 1', desc: '功能模块' },
|
|
25
|
+
{ key: 'Ctrl + 2', desc: '接口管理' },
|
|
26
|
+
{ key: 'Ctrl + 3', desc: '系统设置' },
|
|
27
|
+
{ key: 'Ctrl + 4', desc: '请求统计' },
|
|
28
|
+
{ key: 'Ctrl + 5', desc: '接口嗅探' },
|
|
29
|
+
{ key: 'Ctrl + Shift + N', desc: '新建接口' },
|
|
30
|
+
{ key: 'Ctrl + K', desc: '聚焦搜索' },
|
|
31
|
+
{ key: 'Shift + ?', desc: '快捷键帮助' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const getShortcutsList = () => SHORTCUTS;
|
|
35
|
+
|
|
36
|
+
export const useKeyboardShortcuts = () => {
|
|
37
|
+
const history = useHistory();
|
|
38
|
+
|
|
39
|
+
const handleKeyDown = useCallback((e) => {
|
|
40
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
41
|
+
const ctrlOrCmd = isMac ? e.metaKey : e.ctrlKey;
|
|
42
|
+
|
|
43
|
+
// 忽略在输入框、文本域中的快捷键(导航类除外)
|
|
44
|
+
const tagName = e.target?.tagName?.toLowerCase();
|
|
45
|
+
const isInput = tagName === 'input' || tagName === 'textarea' || e.target?.isContentEditable;
|
|
46
|
+
|
|
47
|
+
// Shift + ? 显示帮助(非输入框时)
|
|
48
|
+
if (e.shiftKey && e.key === '?' && !isInput) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
window.dispatchEvent(new CustomEvent('keyboard-shortcuts-help'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Esc 关闭弹窗/抽屉
|
|
55
|
+
if (e.key === 'Escape') {
|
|
56
|
+
window.dispatchEvent(new CustomEvent('shortcut-esc'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Ctrl/Cmd + 数字键 导航
|
|
61
|
+
if (ctrlOrCmd && !e.shiftKey && /^[1-5]$/.test(e.key)) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const routes = ['/', '/interface', '/settings', '/stats', '/sniffer'];
|
|
64
|
+
const names = ['功能模块', '接口管理', '系统设置', '请求统计', '接口嗅探'];
|
|
65
|
+
const idx = parseInt(e.key, 10) - 1;
|
|
66
|
+
history.push(routes[idx]);
|
|
67
|
+
message.success(`已切换到${names[idx]}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Ctrl/Cmd + S 保存
|
|
72
|
+
if (ctrlOrCmd && e.key.toLowerCase() === 's' && !e.shiftKey) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
window.dispatchEvent(new CustomEvent('shortcut-save'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ctrl/Cmd + Shift + N 新建接口
|
|
79
|
+
if (ctrlOrCmd && e.shiftKey && e.key.toLowerCase() === 'n') {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
window.dispatchEvent(new CustomEvent('shortcut-new-interface'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ctrl/Cmd + K 聚焦搜索
|
|
86
|
+
if (ctrlOrCmd && e.key.toLowerCase() === 'k' && !e.shiftKey) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
window.dispatchEvent(new CustomEvent('shortcut-focus-search'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}, [history]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
95
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
96
|
+
}, [handleKeyDown]);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default useKeyboardShortcuts;
|
package/ui/src/index.html
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Whistle Mock
|
|
6
|
+
<title>Whistle Mock Plugin</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<div id="root"></div>
|
|
10
13
|
</body>
|
|
11
|
-
</html>
|
|
14
|
+
</html>
|
package/ui/src/index.js
CHANGED
|
@@ -3,14 +3,22 @@ import ReactDOM from 'react-dom';
|
|
|
3
3
|
import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
|
|
4
4
|
|
|
5
5
|
// 导入样式
|
|
6
|
-
import 'antd/dist/reset.css';
|
|
6
|
+
import 'antd/dist/reset.css';
|
|
7
7
|
import './styles/global.css';
|
|
8
8
|
import './App.css';
|
|
9
9
|
|
|
10
|
+
// 赛博朋克主题 CSS(规则均带 body.cyber-theme 前缀,默认不生效)
|
|
11
|
+
require('./styles/cyber-theme.css');
|
|
12
|
+
|
|
13
|
+
// 大地色系主题 CSS(规则均带 body.earth-theme 前缀,默认不生效)
|
|
14
|
+
require('./styles/earth-theme.css');
|
|
15
|
+
|
|
10
16
|
// 导入页面组件
|
|
11
17
|
import MockData from './pages/MockDataV2';
|
|
12
18
|
import InterfaceManagement from './pages/InterfaceManagementV2';
|
|
13
19
|
import Settings from './pages/Settings';
|
|
20
|
+
import Stats from './pages/Stats';
|
|
21
|
+
import Sniffer from './pages/Sniffer';
|
|
14
22
|
|
|
15
23
|
const App = () => {
|
|
16
24
|
return (
|
|
@@ -20,6 +28,8 @@ const App = () => {
|
|
|
20
28
|
<Route exact path="/" component={MockData} />
|
|
21
29
|
<Route exact path="/interface" component={InterfaceManagement} />
|
|
22
30
|
<Route path="/interface/:featureId" component={InterfaceManagement} />
|
|
31
|
+
<Route path="/stats" component={Stats} />
|
|
32
|
+
<Route path="/sniffer" component={Sniffer} />
|
|
23
33
|
<Route path="/settings" component={Settings} />
|
|
24
34
|
<Redirect to="/" />
|
|
25
35
|
</Switch>
|
|
@@ -37,4 +47,4 @@ if (ReactDOM.createRoot) {
|
|
|
37
47
|
} else {
|
|
38
48
|
// 旧版本React
|
|
39
49
|
ReactDOM.render(<App />, root);
|
|
40
|
-
}
|
|
50
|
+
}
|