whistle.mock-plugins 1.2.2-beta.9 → 1.3.1-beta.1
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 +27 -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 +255 -83
- 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
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import React, { useState, useEffect } from 'react';
|
|
6
|
+
import { useRef } from 'react';
|
|
6
7
|
import { useHistory } from 'react-router-dom';
|
|
7
8
|
import AppLayout from '../components/AppLayout';
|
|
8
|
-
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd';
|
|
9
|
-
import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
|
9
|
+
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag, Checkbox } from 'antd';
|
|
10
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, ImportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
|
10
11
|
import '../styles/mock-data.css';
|
|
11
12
|
import axios from 'axios';
|
|
12
13
|
|
|
@@ -30,6 +31,11 @@ const MockData = () => {
|
|
|
30
31
|
const [showModal, setShowModal] = useState(false);
|
|
31
32
|
const [currentFeature, setCurrentFeature] = useState(null);
|
|
32
33
|
|
|
34
|
+
// 导出功能弹窗状态
|
|
35
|
+
const [showExportModal, setShowExportModal] = useState(false);
|
|
36
|
+
const [selectedExportIds, setSelectedExportIds] = useState([]);
|
|
37
|
+
const [exporting, setExporting] = useState(false);
|
|
38
|
+
|
|
33
39
|
const [loading, setLoading] = useState(true);
|
|
34
40
|
const [form] = Form.useForm();
|
|
35
41
|
|
|
@@ -57,6 +63,24 @@ const MockData = () => {
|
|
|
57
63
|
fetchFeatures();
|
|
58
64
|
}, []);
|
|
59
65
|
|
|
66
|
+
// 键盘快捷键监听
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const handleSave = () => {
|
|
69
|
+
if (showModal) {
|
|
70
|
+
handleSubmit();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const handleNew = () => {
|
|
74
|
+
openModal();
|
|
75
|
+
};
|
|
76
|
+
window.addEventListener('shortcut-save', handleSave);
|
|
77
|
+
window.addEventListener('shortcut-new-interface', handleNew);
|
|
78
|
+
return () => {
|
|
79
|
+
window.removeEventListener('shortcut-save', handleSave);
|
|
80
|
+
window.removeEventListener('shortcut-new-interface', handleNew);
|
|
81
|
+
};
|
|
82
|
+
}, [showModal]);
|
|
83
|
+
|
|
60
84
|
// 获取所有功能模块
|
|
61
85
|
const fetchFeatures = async () => {
|
|
62
86
|
try {
|
|
@@ -221,7 +245,7 @@ const MockData = () => {
|
|
|
221
245
|
};
|
|
222
246
|
|
|
223
247
|
const viewInterfaces = (feature) => {
|
|
224
|
-
|
|
248
|
+
localStorage.setItem('selectedFeatureId', feature.id);
|
|
225
249
|
history.push(`/interface/${feature.id}`);
|
|
226
250
|
};
|
|
227
251
|
|
|
@@ -254,6 +278,65 @@ const MockData = () => {
|
|
|
254
278
|
}
|
|
255
279
|
};
|
|
256
280
|
|
|
281
|
+
// 打开导出弹窗(默认全选)
|
|
282
|
+
const openExportModal = () => {
|
|
283
|
+
if (mockFeatures.length === 0) {
|
|
284
|
+
message.warning('暂无可导出的功能模块');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
setSelectedExportIds(mockFeatures.map(f => f.id));
|
|
288
|
+
setShowExportModal(true);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// 导出选中的功能模块
|
|
292
|
+
const handleExportSelected = async () => {
|
|
293
|
+
if (selectedExportIds.length === 0) {
|
|
294
|
+
message.warning('请至少选择一个功能模块');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
setExporting(true);
|
|
300
|
+
|
|
301
|
+
const selectedFeatures = mockFeatures.filter(f => selectedExportIds.includes(f.id));
|
|
302
|
+
const configs = [];
|
|
303
|
+
|
|
304
|
+
// 逐个拉取接口配置,组装成与单功能导出一致的结构
|
|
305
|
+
for (const feature of selectedFeatures) {
|
|
306
|
+
const response = await fetch(`/cgi-bin/interfaces?featureId=${feature.id}`);
|
|
307
|
+
const result = await response.json();
|
|
308
|
+
configs.push({
|
|
309
|
+
...feature,
|
|
310
|
+
interfaces: result.code === 0 ? result.data : []
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 只选一个时导出单个对象,兼容旧版单功能导入;多个时导出数组
|
|
315
|
+
const exportData = configs.length === 1 ? configs[0] : configs;
|
|
316
|
+
const dataStr = JSON.stringify(exportData, null, 2);
|
|
317
|
+
const blob = new Blob([dataStr], { type: 'application/json;charset=utf-8' });
|
|
318
|
+
const url = URL.createObjectURL(blob);
|
|
319
|
+
|
|
320
|
+
const exportFileName = configs.length === 1
|
|
321
|
+
? `whistle-mock-feature-${configs[0].id}.json`
|
|
322
|
+
: `whistle-mock-features-${configs.length}.json`;
|
|
323
|
+
|
|
324
|
+
const linkElement = document.createElement('a');
|
|
325
|
+
linkElement.href = url;
|
|
326
|
+
linkElement.download = exportFileName;
|
|
327
|
+
linkElement.click();
|
|
328
|
+
URL.revokeObjectURL(url);
|
|
329
|
+
|
|
330
|
+
message.success(`成功导出 ${configs.length} 个功能模块`);
|
|
331
|
+
setShowExportModal(false);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('导出配置错误:', error);
|
|
334
|
+
message.error('导出失败: ' + error.message);
|
|
335
|
+
} finally {
|
|
336
|
+
setExporting(false);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
257
340
|
const importFeatureConfig = () => {
|
|
258
341
|
const input = document.createElement('input');
|
|
259
342
|
input.type = 'file';
|
|
@@ -266,80 +349,94 @@ const MockData = () => {
|
|
|
266
349
|
const reader = new FileReader();
|
|
267
350
|
reader.onload = async (event) => {
|
|
268
351
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
352
|
+
const parsed = JSON.parse(event.target.result);
|
|
353
|
+
// 兼容单个功能(对象)与多个功能(数组)两种导出格式
|
|
354
|
+
const configs = Array.isArray(parsed) ? parsed : [parsed];
|
|
355
|
+
|
|
356
|
+
// 过滤掉缺少名称的无效配置
|
|
357
|
+
const validConfigs = configs.filter(config => config && config.name);
|
|
358
|
+
if (validConfigs.length === 0) {
|
|
273
359
|
message.error('无效的配置文件: 缺少功能名称');
|
|
274
360
|
return;
|
|
275
361
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
362
|
+
|
|
363
|
+
message.loading({ content: '正在导入功能配置...', key: 'importInfo' });
|
|
364
|
+
|
|
365
|
+
let featureSuccess = 0;
|
|
366
|
+
let featureError = 0;
|
|
367
|
+
let interfaceSuccess = 0;
|
|
368
|
+
let interfaceError = 0;
|
|
369
|
+
|
|
370
|
+
for (const config of validConfigs) {
|
|
371
|
+
try {
|
|
372
|
+
// 创建新功能
|
|
373
|
+
const featureData = {
|
|
374
|
+
name: config.name,
|
|
375
|
+
description: config.description || '',
|
|
376
|
+
active: config.active !== false
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const featureResponse = await fetch('/cgi-bin/features', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: {
|
|
382
|
+
'Content-Type': 'application/json'
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify(featureData)
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const featureResult = await featureResponse.json();
|
|
388
|
+
|
|
389
|
+
if (featureResult.code === 0) {
|
|
390
|
+
featureSuccess++;
|
|
391
|
+
const newFeature = featureResult.data;
|
|
392
|
+
|
|
393
|
+
// 导入接口配置
|
|
394
|
+
if (Array.isArray(config.interfaces) && config.interfaces.length > 0) {
|
|
395
|
+
for (const interfaceItem of config.interfaces) {
|
|
396
|
+
try {
|
|
397
|
+
// 创建接口,使用新功能ID
|
|
398
|
+
const interfaceData = {
|
|
399
|
+
...interfaceItem,
|
|
400
|
+
featureId: newFeature.id,
|
|
401
|
+
id: undefined // 不使用原接口ID
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
await fetch('/cgi-bin/interfaces', {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
headers: {
|
|
407
|
+
'Content-Type': 'application/json'
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify(interfaceData)
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
interfaceSuccess++;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error('导入接口配置失败:', error);
|
|
415
|
+
interfaceError++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
326
418
|
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (errorCount > 0) {
|
|
330
|
-
message.info({ content: `导入完成,成功 ${successCount} 个接口,失败 ${errorCount} 个接口`, key: 'importInfo', duration: 3 });
|
|
331
419
|
} else {
|
|
332
|
-
|
|
420
|
+
featureError++;
|
|
421
|
+
console.error('导入功能失败:', featureResult.message);
|
|
333
422
|
}
|
|
334
|
-
}
|
|
335
|
-
|
|
423
|
+
} catch (error) {
|
|
424
|
+
featureError++;
|
|
425
|
+
console.error('导入功能错误:', error);
|
|
336
426
|
}
|
|
337
|
-
|
|
338
|
-
// 刷新功能列表
|
|
339
|
-
fetchFeatures();
|
|
340
|
-
} else {
|
|
341
|
-
message.error('导入功能失败: ' + featureResult.message);
|
|
342
427
|
}
|
|
428
|
+
|
|
429
|
+
const interfaceSummary = interfaceSuccess > 0 || interfaceError > 0
|
|
430
|
+
? `,共导入 ${interfaceSuccess} 个接口${interfaceError > 0 ? `(${interfaceError} 个失败)` : ''}`
|
|
431
|
+
: '';
|
|
432
|
+
message.success({
|
|
433
|
+
content: `导入完成:成功 ${featureSuccess} 个功能${featureError > 0 ? `,失败 ${featureError} 个功能` : ''}${interfaceSummary}`,
|
|
434
|
+
key: 'importInfo',
|
|
435
|
+
duration: 3
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// 刷新功能列表
|
|
439
|
+
fetchFeatures();
|
|
343
440
|
} catch (error) {
|
|
344
441
|
console.error('导入配置错误:', error);
|
|
345
442
|
message.error('导入失败: ' + error.message);
|
|
@@ -566,7 +663,7 @@ const MockData = () => {
|
|
|
566
663
|
width: 100,
|
|
567
664
|
align: 'center',
|
|
568
665
|
render: (count) => (
|
|
569
|
-
<Badge count={count || 0} showZero style={{ backgroundColor: '#
|
|
666
|
+
<Badge count={count || 0} showZero style={{ backgroundColor: '#1677ff' }} />
|
|
570
667
|
),
|
|
571
668
|
},
|
|
572
669
|
{
|
|
@@ -661,17 +758,6 @@ const MockData = () => {
|
|
|
661
758
|
<div className="title-section">
|
|
662
759
|
<div className="title-with-badge">
|
|
663
760
|
<h1 className="page-title">功能模块管理</h1>
|
|
664
|
-
<Badge
|
|
665
|
-
count="V2"
|
|
666
|
-
style={{
|
|
667
|
-
backgroundColor: '#52c41a',
|
|
668
|
-
fontSize: '12px',
|
|
669
|
-
height: '20px',
|
|
670
|
-
lineHeight: '20px',
|
|
671
|
-
borderRadius: '10px',
|
|
672
|
-
marginLeft: '12px'
|
|
673
|
-
}}
|
|
674
|
-
/>
|
|
675
761
|
</div>
|
|
676
762
|
<div className="page-description">
|
|
677
763
|
创建和管理功能模块,为每个功能配置独立的接口
|
|
@@ -685,14 +771,23 @@ const MockData = () => {
|
|
|
685
771
|
>
|
|
686
772
|
新建功能
|
|
687
773
|
</Button>
|
|
688
|
-
<Button
|
|
689
|
-
type="primary"
|
|
774
|
+
<Button
|
|
775
|
+
type="primary"
|
|
690
776
|
icon={<ExportOutlined />}
|
|
691
777
|
onClick={importFeatureConfig}
|
|
692
778
|
style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }}
|
|
693
779
|
>
|
|
694
780
|
导入功能
|
|
695
781
|
</Button>
|
|
782
|
+
<Button
|
|
783
|
+
type="primary"
|
|
784
|
+
icon={<ImportOutlined />}
|
|
785
|
+
onClick={openExportModal}
|
|
786
|
+
disabled={mockFeatures.length === 0}
|
|
787
|
+
style={mockFeatures.length === 0 ? undefined : { backgroundColor: '#fa8c16', borderColor: '#fa8c16' }}
|
|
788
|
+
>
|
|
789
|
+
导出功能
|
|
790
|
+
</Button>
|
|
696
791
|
</div>
|
|
697
792
|
</div>
|
|
698
793
|
|
|
@@ -910,6 +1005,83 @@ const MockData = () => {
|
|
|
910
1005
|
</div>
|
|
911
1006
|
</Form>
|
|
912
1007
|
</Modal>
|
|
1008
|
+
|
|
1009
|
+
{/* 导出功能弹窗 */}
|
|
1010
|
+
<Modal
|
|
1011
|
+
title="导出功能"
|
|
1012
|
+
open={showExportModal}
|
|
1013
|
+
onCancel={() => setShowExportModal(false)}
|
|
1014
|
+
footer={null}
|
|
1015
|
+
destroyOnClose
|
|
1016
|
+
width={520}
|
|
1017
|
+
>
|
|
1018
|
+
<div
|
|
1019
|
+
style={{
|
|
1020
|
+
display: 'flex',
|
|
1021
|
+
justifyContent: 'space-between',
|
|
1022
|
+
alignItems: 'center',
|
|
1023
|
+
marginBottom: 12
|
|
1024
|
+
}}
|
|
1025
|
+
>
|
|
1026
|
+
<Checkbox
|
|
1027
|
+
indeterminate={selectedExportIds.length > 0 && selectedExportIds.length < mockFeatures.length}
|
|
1028
|
+
checked={mockFeatures.length > 0 && selectedExportIds.length === mockFeatures.length}
|
|
1029
|
+
onChange={(e) => setSelectedExportIds(e.target.checked ? mockFeatures.map(f => f.id) : [])}
|
|
1030
|
+
>
|
|
1031
|
+
全选
|
|
1032
|
+
</Checkbox>
|
|
1033
|
+
<Text type="secondary">
|
|
1034
|
+
已选择 {selectedExportIds.length} / {mockFeatures.length}
|
|
1035
|
+
</Text>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<div
|
|
1039
|
+
style={{
|
|
1040
|
+
maxHeight: 360,
|
|
1041
|
+
overflowY: 'auto',
|
|
1042
|
+
border: '1px solid #f0f0f0',
|
|
1043
|
+
borderRadius: 8,
|
|
1044
|
+
padding: '4px 12px'
|
|
1045
|
+
}}
|
|
1046
|
+
>
|
|
1047
|
+
<Checkbox.Group
|
|
1048
|
+
value={selectedExportIds}
|
|
1049
|
+
onChange={(values) => setSelectedExportIds(values)}
|
|
1050
|
+
style={{ width: '100%' }}
|
|
1051
|
+
>
|
|
1052
|
+
{mockFeatures.map(feature => (
|
|
1053
|
+
<div
|
|
1054
|
+
key={feature.id}
|
|
1055
|
+
style={{
|
|
1056
|
+
padding: '8px 0',
|
|
1057
|
+
borderBottom: '1px solid #fafafa'
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
<Checkbox value={feature.id} style={{ width: '100%' }}>
|
|
1061
|
+
<span style={{ fontWeight: 500 }}>{feature.name}</span>
|
|
1062
|
+
<span style={{ color: '#999', marginLeft: 8, fontSize: 12 }}>
|
|
1063
|
+
{feature.interfaceCount || 0} 个接口
|
|
1064
|
+
</span>
|
|
1065
|
+
</Checkbox>
|
|
1066
|
+
</div>
|
|
1067
|
+
))}
|
|
1068
|
+
</Checkbox.Group>
|
|
1069
|
+
</div>
|
|
1070
|
+
|
|
1071
|
+
<div className="form-actions" style={{ marginTop: 16 }}>
|
|
1072
|
+
<Button onClick={() => setShowExportModal(false)}>
|
|
1073
|
+
取消
|
|
1074
|
+
</Button>
|
|
1075
|
+
<Button
|
|
1076
|
+
type="primary"
|
|
1077
|
+
loading={exporting}
|
|
1078
|
+
disabled={selectedExportIds.length === 0}
|
|
1079
|
+
onClick={handleExportSelected}
|
|
1080
|
+
>
|
|
1081
|
+
导出 ({selectedExportIds.length})
|
|
1082
|
+
</Button>
|
|
1083
|
+
</div>
|
|
1084
|
+
</Modal>
|
|
913
1085
|
</AppLayout>
|
|
914
1086
|
);
|
|
915
1087
|
};
|