whistle.mock-plugins 1.2.2-beta.23 → 1.2.2-beta.28

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.
@@ -0,0 +1,165 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Input, message, Space, Tabs, Tag } from 'antd';
3
+ import { DownloadOutlined, ThunderboltOutlined, CopyOutlined } from '@ant-design/icons';
4
+ import axios from 'axios';
5
+
6
+ const { TextArea } = Input;
7
+
8
+ /**
9
+ * JSON 转 Mock.js 模板
10
+ * 简单转换:字符串 -> '@string', 数字 -> '@integer(1,100)', 布尔 -> '@boolean'
11
+ * 保留对象结构
12
+ */
13
+ const jsonToMockTemplate = (obj, depth = 0) => {
14
+ if (obj === null) return null;
15
+ if (typeof obj === 'string') {
16
+ // 保留特定格式字符串的提示
17
+ if (/^\d{4}-\d{2}-\d{2}/.test(obj)) return '@date';
18
+ if (/^\d{4}-\d{2}-\d{2}T/.test(obj)) return '@datetime';
19
+ if (/^https?:\/\//.test(obj)) return '@url';
20
+ if (/^\w+@\w+\.\w+/.test(obj)) return '@email';
21
+ if (/^1[3-9]\d{9}$/.test(obj)) return '@string("number", 11)';
22
+ return '@csentence(3, 10)';
23
+ }
24
+ if (typeof obj === 'number') {
25
+ if (Number.isInteger(obj)) return '@integer(1, 999)';
26
+ return '@float(1, 100, 2, 2)';
27
+ }
28
+ if (typeof obj === 'boolean') return '@boolean';
29
+ if (Array.isArray(obj)) {
30
+ if (obj.length === 0) return [];
31
+ const template = jsonToMockTemplate(obj[0], depth + 1);
32
+ // 用 |1-5 表示数组长度随机
33
+ return [template];
34
+ }
35
+ if (typeof obj === 'object') {
36
+ const result = {};
37
+ for (const [key, value] of Object.entries(obj)) {
38
+ result[key] = jsonToMockTemplate(value, depth + 1);
39
+ }
40
+ return result;
41
+ }
42
+ return obj;
43
+ };
44
+
45
+ const RealResponseRecorder = ({ urlPattern, onApply }) => {
46
+ const [fetchUrl, setFetchUrl] = useState('');
47
+ const [loading, setLoading] = useState(false);
48
+ const [response, setResponse] = useState(null);
49
+ const [activeTab, setActiveTab] = useState('preview');
50
+
51
+ const handleFetch = async () => {
52
+ const targetUrl = fetchUrl.trim() || urlPattern;
53
+ if (!targetUrl) {
54
+ message.warning('请输入目标 URL');
55
+ return;
56
+ }
57
+ setLoading(true);
58
+ try {
59
+ const res = await axios.post('/cgi-bin/proxy-fetch', {
60
+ url: targetUrl,
61
+ method: 'GET',
62
+ });
63
+ if (res.data.code === 0) {
64
+ setResponse(res.data.data);
65
+ setActiveTab('preview');
66
+ message.success('抓取成功');
67
+ } else {
68
+ throw new Error(res.data.message);
69
+ }
70
+ } catch (err) {
71
+ message.error('抓取失败: ' + (err.response?.data?.message || err.message));
72
+ } finally {
73
+ setLoading(false);
74
+ }
75
+ };
76
+
77
+ const handleGenerateMock = () => {
78
+ if (!response?.body) return;
79
+ try {
80
+ const json = JSON.parse(response.body);
81
+ const template = jsonToMockTemplate(json);
82
+ const mockTemplate = JSON.stringify(template, null, 2);
83
+ onApply?.(mockTemplate);
84
+ message.success('Mock 模板已生成');
85
+ } catch (e) {
86
+ message.error('响应不是 JSON,无法生成模板: ' + e.message);
87
+ }
88
+ };
89
+
90
+ const handleCopy = () => {
91
+ if (response?.body) {
92
+ navigator.clipboard.writeText(response.body);
93
+ message.success('已复制');
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div style={{ padding: '16px 0' }}>
99
+ <Space direction="vertical" style={{ width: '100%' }}>
100
+ <Space style={{ width: '100%' }}>
101
+ <Input
102
+ placeholder={urlPattern || '输入真实接口 URL'}
103
+ value={fetchUrl}
104
+ onChange={(e) => setFetchUrl(e.target.value)}
105
+ style={{ width: 400 }}
106
+ onPressEnter={handleFetch}
107
+ />
108
+ <Button
109
+ type="primary"
110
+ icon={<ThunderboltOutlined />}
111
+ loading={loading}
112
+ onClick={handleFetch}
113
+ >
114
+ 抓取真实响应
115
+ </Button>
116
+ </Space>
117
+
118
+ {response && (
119
+ <>
120
+ <Space>
121
+ <Tag color={response.status >= 200 && response.status < 300 ? 'success' : 'error'}>
122
+ {response.status} {response.statusText}
123
+ </Tag>
124
+ <Button size="small" icon={<CopyOutlined />} onClick={handleCopy}>
125
+ 复制原文
126
+ </Button>
127
+ <Button size="small" icon={<DownloadOutlined />} onClick={handleGenerateMock} type="primary">
128
+ 生成 Mock 模板
129
+ </Button>
130
+ </Space>
131
+
132
+ <Tabs activeKey={activeTab} onChange={setActiveTab}>
133
+ <Tabs.TabPane tab="响应预览" key="preview">
134
+ <TextArea
135
+ value={response.body}
136
+ readOnly
137
+ rows={12}
138
+ style={{ fontFamily: 'monospace', fontSize: 12 }}
139
+ />
140
+ </Tabs.TabPane>
141
+ <Tabs.TabPane tab="生成的 Mock 模板" key="mock">
142
+ <TextArea
143
+ value={(() => {
144
+ try {
145
+ const json = JSON.parse(response.body);
146
+ const template = jsonToMockTemplate(json);
147
+ return JSON.stringify(template, null, 2);
148
+ } catch (e) {
149
+ return '// 响应不是 JSON,无法生成模板\n' + response.body;
150
+ }
151
+ })()}
152
+ readOnly
153
+ rows={12}
154
+ style={{ fontFamily: 'monospace', fontSize: 12 }}
155
+ />
156
+ </Tabs.TabPane>
157
+ </Tabs>
158
+ </>
159
+ )}
160
+ </Space>
161
+ </div>
162
+ );
163
+ };
164
+
165
+ export default RealResponseRecorder;
@@ -274,7 +274,7 @@ const ResponseContentEditor = ({
274
274
  onClick={toggleTextareaFullscreen}
275
275
  size="small"
276
276
  type="text"
277
- style={{ color: '#ffffff' }}
277
+ style={{ color: '#595959' }}
278
278
  >
279
279
  退出全屏 (ESC)
280
280
  </Button>
@@ -301,7 +301,7 @@ const ResponseContentEditor = ({
301
301
  <div style={{ marginBottom: '12px' }}>
302
302
  <Divider orientation="left" orientationMargin={0} style={{ margin: '0 0 10px 0', fontSize: '12px' }}>
303
303
  <Space size={6}>
304
- <FilterOutlined style={{ color: '#1890ff' }} />
304
+ <FilterOutlined style={{ color: '#1677ff' }} />
305
305
  <span style={{ fontWeight: 'bold', color: '#333' }}>入参匹配规则</span>
306
306
  {matchers.length > 0 && (
307
307
  <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matchers.length}条</Tag>
@@ -382,11 +382,10 @@ const ResponseContentEditor = ({
382
382
  <div style={{
383
383
  textAlign: 'center',
384
384
  padding: '40px 20px',
385
- background: '#fafafa',
386
385
  borderRadius: '6px',
387
386
  border: '1px dashed #d9d9d9'
388
387
  }}>
389
- <FileTextOutlined style={{ fontSize: '32px', color: '#d9d9d9', marginBottom: '12px' }} />
388
+ <FileTextOutlined style={{ fontSize: '32px', color: '#bfbfbf', marginBottom: '12px' }} />
390
389
  <div style={{ color: '#999', marginBottom: '16px' }}>暂无响应数据</div>
391
390
  <Button
392
391
  type="primary"
@@ -433,9 +432,7 @@ const ResponseContentEditor = ({
433
432
  style={{
434
433
  display: 'flex',
435
434
  alignItems: 'center',
436
- gap: '4px',
437
- borderColor: '#1890ff',
438
- color: '#1890ff'
435
+ gap: '4px'
439
436
  }}
440
437
  >
441
438
  添加响应
@@ -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 数据工厂</title>
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,19 @@ 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'; // 导入antd样式,使用适合的版本
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
+
10
13
  // 导入页面组件
11
14
  import MockData from './pages/MockDataV2';
12
15
  import InterfaceManagement from './pages/InterfaceManagementV2';
13
16
  import Settings from './pages/Settings';
17
+ import Stats from './pages/Stats';
18
+ import Sniffer from './pages/Sniffer';
14
19
 
15
20
  const App = () => {
16
21
  return (
@@ -20,6 +25,8 @@ const App = () => {
20
25
  <Route exact path="/" component={MockData} />
21
26
  <Route exact path="/interface" component={InterfaceManagement} />
22
27
  <Route path="/interface/:featureId" component={InterfaceManagement} />
28
+ <Route path="/stats" component={Stats} />
29
+ <Route path="/sniffer" component={Sniffer} />
23
30
  <Route path="/settings" component={Settings} />
24
31
  <Redirect to="/" />
25
32
  </Switch>
@@ -37,4 +44,4 @@ if (ReactDOM.createRoot) {
37
44
  } else {
38
45
  // 旧版本React
39
46
  ReactDOM.render(<App />, root);
40
- }
47
+ }
@@ -2,7 +2,7 @@
2
2
  * InterfaceManagementV2.js - 接口管理页面
3
3
  */
4
4
 
5
- import React, { useState, useEffect } from 'react';
5
+ import React, { useState, useEffect, useRef } from 'react';
6
6
  import { useParams, useHistory } from 'react-router-dom';
7
7
  import AppLayout from '../components/AppLayout';
8
8
  import {
@@ -87,6 +87,7 @@ const InterfaceManagement = () => {
87
87
  const [currentResponseId, setCurrentResponseId] = useState(null);
88
88
 
89
89
  // 搜索状态
90
+ const searchRef = useRef(null);
90
91
  const [searchValue, setSearchValue] = useState('');
91
92
  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
92
93
  const [drawerVisible, setDrawerVisible] = useState(false);
@@ -136,6 +137,29 @@ const InterfaceManagement = () => {
136
137
  }
137
138
  }, [selectedFeatureId]);
138
139
 
140
+ // 键盘快捷键监听
141
+ useEffect(() => {
142
+ const handleSave = () => {
143
+ if (modalVisible) {
144
+ handleSubmit();
145
+ }
146
+ };
147
+ const handleNew = () => {
148
+ handleAddInterface();
149
+ };
150
+ const handleFocusSearch = () => {
151
+ searchRef.current?.focus();
152
+ };
153
+ window.addEventListener('shortcut-save', handleSave);
154
+ window.addEventListener('shortcut-new-interface', handleNew);
155
+ window.addEventListener('shortcut-focus-search', handleFocusSearch);
156
+ return () => {
157
+ window.removeEventListener('shortcut-save', handleSave);
158
+ window.removeEventListener('shortcut-new-interface', handleNew);
159
+ window.removeEventListener('shortcut-focus-search', handleFocusSearch);
160
+ };
161
+ }, [modalVisible]);
162
+
139
163
  // 监听数据管理显示模式变化
140
164
  useEffect(() => {
141
165
  const savedMode = localStorage.getItem('dataManagementDisplayMode');
@@ -1012,7 +1036,7 @@ const InterfaceManagement = () => {
1012
1036
  ))}
1013
1037
  </div>
1014
1038
  }>
1015
- <span style={{ color: '#1890ff' }}>
1039
+ <span style={{ color: '#1677ff' }}>
1016
1040
  {count}个
1017
1041
  {hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
1018
1042
  </span>
@@ -1067,7 +1091,7 @@ const InterfaceManagement = () => {
1067
1091
  )}
1068
1092
  {responsesWithMatchers.length > 0 && (
1069
1093
  <div>
1070
- <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1890ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
1094
+ <div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1677ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
1071
1095
  {responsesWithMatchers.map((resp, ri) => (
1072
1096
  <div key={ri} style={{ marginBottom: 4 }}>
1073
1097
  <div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
@@ -1231,6 +1255,7 @@ const InterfaceManagement = () => {
1231
1255
  </div>
1232
1256
 
1233
1257
  <Search
1258
+ ref={searchRef}
1234
1259
  placeholder="搜索接口名称或URL"
1235
1260
  onSearch={handleSearch}
1236
1261
  value={searchValue}
@@ -1500,7 +1525,6 @@ const InterfaceManagement = () => {
1500
1525
  destroyOnClose
1501
1526
  bodyStyle={{
1502
1527
  padding: '0',
1503
- background: '#fafafa',
1504
1528
  overflowY: 'auto'
1505
1529
  }}
1506
1530
  footer={
@@ -1744,8 +1768,7 @@ const InterfaceManagement = () => {
1744
1768
  bodyStyle={{
1745
1769
  maxHeight: '75vh',
1746
1770
  overflow: 'auto',
1747
- padding: '0',
1748
- background: '#fafafa'
1771
+ padding: '0'
1749
1772
  }}
1750
1773
  className="interface-modal"
1751
1774
  >
@@ -3,6 +3,7 @@
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
9
  import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd';
@@ -57,6 +58,24 @@ const MockData = () => {
57
58
  fetchFeatures();
58
59
  }, []);
59
60
 
61
+ // 键盘快捷键监听
62
+ useEffect(() => {
63
+ const handleSave = () => {
64
+ if (showModal) {
65
+ handleSubmit();
66
+ }
67
+ };
68
+ const handleNew = () => {
69
+ openModal();
70
+ };
71
+ window.addEventListener('shortcut-save', handleSave);
72
+ window.addEventListener('shortcut-new-interface', handleNew);
73
+ return () => {
74
+ window.removeEventListener('shortcut-save', handleSave);
75
+ window.removeEventListener('shortcut-new-interface', handleNew);
76
+ };
77
+ }, [showModal]);
78
+
60
79
  // 获取所有功能模块
61
80
  const fetchFeatures = async () => {
62
81
  try {
@@ -566,7 +585,7 @@ const MockData = () => {
566
585
  width: 100,
567
586
  align: 'center',
568
587
  render: (count) => (
569
- <Badge count={count || 0} showZero style={{ backgroundColor: '#1890ff' }} />
588
+ <Badge count={count || 0} showZero style={{ backgroundColor: '#1677ff' }} />
570
589
  ),
571
590
  },
572
591
  {
@@ -661,17 +680,6 @@ const MockData = () => {
661
680
  <div className="title-section">
662
681
  <div className="title-with-badge">
663
682
  <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
683
  </div>
676
684
  <div className="page-description">
677
685
  创建和管理功能模块,为每个功能配置独立的接口
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import AppLayout from '../components/AppLayout';
3
- import { Card, Typography, Switch, Space, Modal, Button } from 'antd';
3
+ import { Card, Typography, Switch, Space, Modal, Button, message } from 'antd';
4
4
  import { QuestionCircleOutlined } from '@ant-design/icons';
5
5
  import axios from 'axios';
6
6
  import '../styles/settings.css';
@@ -16,6 +16,7 @@ const Settings = () => {
16
16
  const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
17
17
  const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
18
18
  const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
19
+ const [cyberThemeEnabled, setCyberThemeEnabled] = useState(true);
19
20
 
20
21
  useEffect(() => {
21
22
  const savedMode = localStorage.getItem('dataManagementDisplayMode');
@@ -39,6 +40,10 @@ const Settings = () => {
39
40
  const savedResponseParamMatcher = localStorage.getItem('responseParamMatcherEnabled');
40
41
  if (savedResponseParamMatcher !== null) setResponseParamMatcherEnabled(savedResponseParamMatcher !== 'false');
41
42
  });
43
+
44
+ // 读取赛博朋克主题开关(默认开启)
45
+ const savedCyber = localStorage.getItem('cyberTheme');
46
+ setCyberThemeEnabled(savedCyber !== 'false');
42
47
  }, []);
43
48
 
44
49
  const handleDisplayModeChange = (checked) => {
@@ -69,6 +74,13 @@ const Settings = () => {
69
74
  window.dispatchEvent(new Event('storage'));
70
75
  };
71
76
 
77
+ const handleCyberThemeChange = (checked) => {
78
+ setCyberThemeEnabled(checked);
79
+ localStorage.setItem('cyberTheme', String(checked));
80
+ window.dispatchEvent(new Event('storage'));
81
+ message.success(checked ? '赛博朋克主题已开启' : '经典主题已开启');
82
+ };
83
+
72
84
  const showMatcherRuleModal = (type) => {
73
85
  if (type === 'interface') {
74
86
  Modal.info({
@@ -229,6 +241,28 @@ const Settings = () => {
229
241
  </div>
230
242
  </div>
231
243
  </Card>
244
+
245
+ <Card title="主题设置" className="settings-card" style={{ marginTop: 24 }}>
246
+ <div className="settings-item">
247
+ <div className="settings-item-header">
248
+ <div>
249
+ <div className="settings-item-title">赛博朋克风格</div>
250
+ <div className="settings-item-description">
251
+ 启用深色赛博朋克霓虹主题,关闭则恢复经典亮色主题
252
+ </div>
253
+ </div>
254
+ <Space>
255
+ <Text type="secondary">
256
+ {cyberThemeEnabled ? '已开启' : '已关闭'}
257
+ </Text>
258
+ <Switch
259
+ checked={cyberThemeEnabled}
260
+ onChange={handleCyberThemeChange}
261
+ />
262
+ </Space>
263
+ </div>
264
+ </div>
265
+ </Card>
232
266
  </div>
233
267
  </AppLayout>
234
268
  );