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.
Files changed (38) hide show
  1. package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
  2. package/TEST_GUIDE.md +156 -0
  3. package/VERSION.md +57 -3
  4. package/app/cgi-bin/proxy-fetch.js +117 -0
  5. package/app/cgi-bin/settings.js +37 -0
  6. package/app/cgi-bin/stats.js +149 -0
  7. package/app/public/index.html +1 -1
  8. package/app/public/js/index.js +1 -1
  9. package/app/public/js/index.js.LICENSE.txt +27 -2
  10. package/lib/models.js +9 -0
  11. package/lib/routes/interfaces.js +6 -3
  12. package/lib/routes/settings.js +51 -0
  13. package/lib/ruleManager.js +122 -63
  14. package/lib/server.js +4 -0
  15. package/lib/ui-server.js +20 -4
  16. package/package.json +2 -1
  17. package/ui/src/App.css +1 -1
  18. package/ui/src/components/AppLayout.js +317 -80
  19. package/ui/src/components/FeatureCard.js +7 -2
  20. package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
  21. package/ui/src/components/VersionModal.js +4 -41
  22. package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
  23. package/ui/src/components/interface-management/PreviewModal.js +4 -6
  24. package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
  25. package/ui/src/components/interface-management/ResponseContentEditor.js +123 -177
  26. package/ui/src/components/interface-management/index.js +1 -0
  27. package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
  28. package/ui/src/index.html +5 -2
  29. package/ui/src/index.js +12 -2
  30. package/ui/src/pages/InterfaceManagementV2.js +563 -147
  31. package/ui/src/pages/MockDataV2.js +255 -83
  32. package/ui/src/pages/Settings.js +275 -8
  33. package/ui/src/pages/Sniffer.js +521 -0
  34. package/ui/src/pages/Stats.js +288 -0
  35. package/ui/src/styles/cyber-theme.css +1234 -0
  36. package/ui/src/styles/earth-theme.css +618 -0
  37. package/ui/src/styles/interface-management.css +37 -1
  38. 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 config = JSON.parse(event.target.result);
270
-
271
- // 验证导入的配置
272
- if (!config.name) {
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
- const featureData = {
279
- name: config.name,
280
- description: config.description || '',
281
- active: config.active !== false
282
- };
283
-
284
- // 保存功能
285
- const featureResponse = await fetch('/cgi-bin/features', {
286
- method: 'POST',
287
- headers: {
288
- 'Content-Type': 'application/json'
289
- },
290
- body: JSON.stringify(featureData)
291
- });
292
-
293
- const featureResult = await featureResponse.json();
294
-
295
- if (featureResult.code === 0) {
296
- const newFeature = featureResult.data;
297
-
298
- // 导入接口配置
299
- if (Array.isArray(config.interfaces) && config.interfaces.length > 0) {
300
- message.loading({ content: '正在导入接口配置...', key: 'importInfo' });
301
-
302
- let successCount = 0;
303
- let errorCount = 0;
304
-
305
- for (const interfaceItem of config.interfaces) {
306
- try {
307
- // 创建接口,使用新功能ID
308
- const interfaceData = {
309
- ...interfaceItem,
310
- featureId: newFeature.id,
311
- id: undefined // 不使用原接口ID
312
- };
313
-
314
- await fetch('/cgi-bin/interfaces', {
315
- method: 'POST',
316
- headers: {
317
- 'Content-Type': 'application/json'
318
- },
319
- body: JSON.stringify(interfaceData)
320
- });
321
-
322
- successCount++;
323
- } catch (error) {
324
- console.error('导入接口配置失败:', error);
325
- errorCount++;
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
- message.success({ content: `成功导入 ${successCount} 个接口`, key: 'importInfo', duration: 2 });
420
+ featureError++;
421
+ console.error('导入功能失败:', featureResult.message);
333
422
  }
334
- } else {
335
- message.success('功能导入成功(无接口配置)');
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: '#1890ff' }} />
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
  };