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.
- package/VERSION.md +78 -2
- package/app/cgi-bin/proxy-fetch.js +117 -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 +18 -0
- package/lib/ruleManager.js +27 -0
- package/lib/ui-server.js +20 -4
- package/package.json +1 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +213 -79
- package/ui/src/components/FeatureCard.js +1 -1
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -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 +4 -7
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +9 -2
- package/ui/src/pages/InterfaceManagementV2.js +29 -6
- package/ui/src/pages/MockDataV2.js +20 -12
- package/ui/src/pages/Settings.js +35 -1
- 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
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
/** */
|
|
74
74
|
|
|
75
|
+
/** */
|
|
76
|
+
|
|
75
77
|
/** */
|
|
76
78
|
|
|
77
79
|
/** */
|
|
@@ -86,6 +88,10 @@
|
|
|
86
88
|
|
|
87
89
|
/** */
|
|
88
90
|
|
|
91
|
+
/** */
|
|
92
|
+
|
|
93
|
+
/** */
|
|
94
|
+
|
|
89
95
|
/** */
|
|
90
96
|
|
|
91
97
|
/** */
|
|
@@ -136,6 +142,8 @@
|
|
|
136
142
|
|
|
137
143
|
/** */
|
|
138
144
|
|
|
145
|
+
/** */
|
|
146
|
+
|
|
139
147
|
/** */
|
|
140
148
|
|
|
141
149
|
/** */
|
|
@@ -166,10 +174,20 @@
|
|
|
166
174
|
|
|
167
175
|
/** */
|
|
168
176
|
|
|
177
|
+
/** */
|
|
178
|
+
|
|
179
|
+
/** */
|
|
180
|
+
|
|
169
181
|
/** */
|
|
170
182
|
|
|
183
|
+
/** */
|
|
184
|
+
|
|
171
185
|
/** */
|
|
172
186
|
|
|
173
187
|
/** */
|
|
174
188
|
|
|
189
|
+
/** */
|
|
190
|
+
|
|
191
|
+
/** */
|
|
192
|
+
|
|
175
193
|
/** */
|
package/lib/ruleManager.js
CHANGED
|
@@ -529,6 +529,31 @@ const ruleManager = {
|
|
|
529
529
|
if (this.dataManager && typeof this.dataManager.logRequest === 'function') {
|
|
530
530
|
const parsedUrl = url.parse(req.url);
|
|
531
531
|
try {
|
|
532
|
+
// 获取功能模块名称
|
|
533
|
+
let featureName = '';
|
|
534
|
+
if (interfaceObj.featureId) {
|
|
535
|
+
try {
|
|
536
|
+
const features = await this.dataManager.getFeatures();
|
|
537
|
+
const feature = features.find(f => f.id === interfaceObj.featureId);
|
|
538
|
+
if (feature && feature.name) {
|
|
539
|
+
featureName = feature.name;
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
this.log(`[规则处理器] 获取功能模块信息失败: ${err.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 获取实际命中的响应名称
|
|
547
|
+
let responseName = '';
|
|
548
|
+
if (result && result.mockInfo && result.mockInfo.responseName) {
|
|
549
|
+
responseName = result.mockInfo.responseName;
|
|
550
|
+
} else {
|
|
551
|
+
const headerResponse = res.getHeader('X-Whistle-Mock-Response');
|
|
552
|
+
if (headerResponse) {
|
|
553
|
+
responseName = headerResponse;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
532
557
|
this.dataManager.logRequest({
|
|
533
558
|
type: 'mock_hit',
|
|
534
559
|
eventType: 'mock_hit',
|
|
@@ -541,6 +566,8 @@ const ruleManager = {
|
|
|
541
566
|
proxyType: proxyType,
|
|
542
567
|
interfaceName: interfaceObj.name,
|
|
543
568
|
interfaceId: interfaceObj.id,
|
|
569
|
+
featureName: featureName,
|
|
570
|
+
responseName: responseName,
|
|
544
571
|
responseData: result,
|
|
545
572
|
responseTime: new Date().toISOString(),
|
|
546
573
|
contentType: res.getHeader('Content-Type') || config.contentType
|
package/lib/ui-server.js
CHANGED
|
@@ -32,7 +32,17 @@ app.use(bodyParser.urlencoded({ extended: true, limit: '100mb'}));
|
|
|
32
32
|
app.use(bodyParser.json({ limit: '100mb' }));
|
|
33
33
|
|
|
34
34
|
// 处理CGI请求
|
|
35
|
-
app.all('/cgi-bin/*', function(req, res) {
|
|
35
|
+
app.all('/cgi-bin/*', async function(req, res) {
|
|
36
|
+
// CORS 预检处理
|
|
37
|
+
if (req.method === 'OPTIONS') {
|
|
38
|
+
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
|
39
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
40
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
41
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
|
42
|
+
res.status(204).end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
if (req.headers.origin) {
|
|
37
47
|
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
|
|
38
48
|
res.setHeader('Access-Control-Allow-Credentials', true);
|
|
@@ -63,10 +73,16 @@ app.all('/cgi-bin/*', function(req, res) {
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
try {
|
|
66
|
-
// 提供数据目录给CGI
|
|
67
|
-
controller.call({ dataDir: DATA_DIR }, req, res);
|
|
76
|
+
// 提供数据目录给CGI处理程序(支持 async controller)
|
|
77
|
+
const result = controller.call({ dataDir: DATA_DIR }, req, res);
|
|
78
|
+
if (result && typeof result.then === 'function') {
|
|
79
|
+
await result;
|
|
80
|
+
}
|
|
68
81
|
} catch(e) {
|
|
69
|
-
|
|
82
|
+
console.error('[ui-server] CGI 执行错误:', e.message || e);
|
|
83
|
+
if (!res.headersSent) {
|
|
84
|
+
res.status(500).json({ code: 500, message: e.message || 'Internal Server Error' });
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
});
|
|
72
88
|
|
package/package.json
CHANGED
package/ui/src/App.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Layout, Menu, Button, Breadcrumb,
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { Layout, Menu, Button, Breadcrumb, Space, ConfigProvider, theme } from 'antd';
|
|
3
3
|
import {
|
|
4
4
|
AppstoreOutlined,
|
|
5
5
|
ApiOutlined,
|
|
@@ -7,21 +7,89 @@ import {
|
|
|
7
7
|
FileTextOutlined,
|
|
8
8
|
MenuFoldOutlined,
|
|
9
9
|
MenuUnfoldOutlined,
|
|
10
|
-
ExperimentOutlined
|
|
10
|
+
ExperimentOutlined,
|
|
11
|
+
BarChartOutlined,
|
|
12
|
+
ThunderboltOutlined
|
|
11
13
|
} from '@ant-design/icons';
|
|
12
14
|
import { Link, useLocation } from 'react-router-dom';
|
|
13
15
|
import VersionModal from './VersionModal';
|
|
16
|
+
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
|
|
17
|
+
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts';
|
|
14
18
|
import '../styles/app-layout.css';
|
|
15
19
|
|
|
16
20
|
const { Header, Content, Sider } = Layout;
|
|
17
|
-
const { Title, Text } = Typography;
|
|
18
21
|
|
|
19
22
|
const AppLayout = ({ children }) => {
|
|
20
23
|
const location = useLocation();
|
|
21
24
|
const currentPath = location.pathname;
|
|
22
25
|
const [collapsed, setCollapsed] = useState(false);
|
|
23
26
|
const [versionModalVisible, setVersionModalVisible] = useState(false);
|
|
24
|
-
const
|
|
27
|
+
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
|
|
28
|
+
|
|
29
|
+
// 注册全局键盘快捷键
|
|
30
|
+
useKeyboardShortcuts();
|
|
31
|
+
|
|
32
|
+
// 监听快捷键帮助事件
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleHelp = () => setShortcutsHelpVisible(true);
|
|
35
|
+
window.addEventListener('keyboard-shortcuts-help', handleHelp);
|
|
36
|
+
return () => window.removeEventListener('keyboard-shortcuts-help', handleHelp);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// 赛博朋克主题开关
|
|
40
|
+
const [enableCyber, setEnableCyber] = useState(() => {
|
|
41
|
+
return localStorage.getItem('cyberTheme') !== 'false';
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
document.body.classList.toggle('cyber-theme', enableCyber);
|
|
46
|
+
}, [enableCyber]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const handleStorage = () => {
|
|
50
|
+
setEnableCyber(localStorage.getItem('cyberTheme') !== 'false');
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener('storage', handleStorage);
|
|
53
|
+
return () => window.removeEventListener('storage', handleStorage);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const cyberTheme = useMemo(() => ({
|
|
57
|
+
token: {
|
|
58
|
+
colorPrimary: '#00f0ff',
|
|
59
|
+
colorPrimaryHover: '#33f3ff',
|
|
60
|
+
colorPrimaryActive: '#00c2cc',
|
|
61
|
+
colorSuccess: '#00ff41',
|
|
62
|
+
colorWarning: '#f0e800',
|
|
63
|
+
colorError: '#ff4d4f',
|
|
64
|
+
colorInfo: '#00f0ff',
|
|
65
|
+
colorText: '#e0e0e0',
|
|
66
|
+
colorTextSecondary: '#a0a0a0',
|
|
67
|
+
colorTextTertiary: '#6b7280',
|
|
68
|
+
colorBorder: '#1a1a2e',
|
|
69
|
+
colorBorderSecondary: '#1a1a2e',
|
|
70
|
+
colorBgBase: '#050508',
|
|
71
|
+
colorBgContainer: '#0f0f1a',
|
|
72
|
+
colorBgElevated: '#141420',
|
|
73
|
+
colorBgLayout: '#0a0a12',
|
|
74
|
+
colorBgSpotlight: '#141420',
|
|
75
|
+
borderRadius: 6,
|
|
76
|
+
borderRadiusSM: 4,
|
|
77
|
+
borderRadiusLG: 8,
|
|
78
|
+
wireframe: false,
|
|
79
|
+
fontFamily: "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
80
|
+
fontFamilyCode: "'Share Tech Mono', 'SFMono-Regular', Consolas, monospace",
|
|
81
|
+
},
|
|
82
|
+
algorithm: theme.darkAlgorithm,
|
|
83
|
+
}), []);
|
|
84
|
+
|
|
85
|
+
const lightTheme = useMemo(() => ({
|
|
86
|
+
token: {
|
|
87
|
+
borderRadius: 6,
|
|
88
|
+
borderRadiusSM: 4,
|
|
89
|
+
borderRadiusLG: 8,
|
|
90
|
+
},
|
|
91
|
+
algorithm: theme.defaultAlgorithm,
|
|
92
|
+
}), []);
|
|
25
93
|
|
|
26
94
|
// 版本弹窗控制方法
|
|
27
95
|
const showVersionModal = () => setVersionModalVisible(true);
|
|
@@ -31,6 +99,8 @@ const AppLayout = ({ children }) => {
|
|
|
31
99
|
const getSelectedKeys = () => {
|
|
32
100
|
if (currentPath === '/' || currentPath === '#/') return ['1'];
|
|
33
101
|
if (currentPath.startsWith('/interface') || currentPath.includes('/interface')) return ['2'];
|
|
102
|
+
if (currentPath === '/stats' || currentPath.includes('/stats')) return ['4'];
|
|
103
|
+
if (currentPath === '/sniffer' || currentPath.includes('/sniffer')) return ['5'];
|
|
34
104
|
if (currentPath === '/settings' || currentPath.includes('/settings')) return ['3'];
|
|
35
105
|
return [];
|
|
36
106
|
};
|
|
@@ -48,6 +118,10 @@ const AppLayout = ({ children }) => {
|
|
|
48
118
|
if (currentPath.includes('/interface/') && currentPath !== '/interface/') {
|
|
49
119
|
breadcrumbs.push({ path: currentPath, title: '功能接口详情' });
|
|
50
120
|
}
|
|
121
|
+
} else if (currentPath === '/stats') {
|
|
122
|
+
breadcrumbs.push({ path: '/stats', title: '请求统计' });
|
|
123
|
+
} else if (currentPath === '/sniffer') {
|
|
124
|
+
breadcrumbs.push({ path: '/sniffer', title: '接口嗅探' });
|
|
51
125
|
} else if (currentPath === '/settings') {
|
|
52
126
|
breadcrumbs.push({ path: '/settings', title: '系统设置' });
|
|
53
127
|
}
|
|
@@ -59,6 +133,8 @@ const AppLayout = ({ children }) => {
|
|
|
59
133
|
const getPageTitle = () => {
|
|
60
134
|
if (currentPath === '/' || currentPath === '#/') return '功能模块管理';
|
|
61
135
|
if (currentPath.startsWith('/interface')) return '接口配置管理';
|
|
136
|
+
if (currentPath === '/stats') return '请求统计';
|
|
137
|
+
if (currentPath === '/sniffer') return '接口嗅探';
|
|
62
138
|
if (currentPath === '/settings') return '系统设置';
|
|
63
139
|
return 'Whistle Mock Plugin';
|
|
64
140
|
};
|
|
@@ -75,6 +151,16 @@ const AppLayout = ({ children }) => {
|
|
|
75
151
|
icon: <ApiOutlined />,
|
|
76
152
|
label: <Link to="/interface">接口管理</Link>,
|
|
77
153
|
},
|
|
154
|
+
{
|
|
155
|
+
key: '4',
|
|
156
|
+
icon: <BarChartOutlined />,
|
|
157
|
+
label: <Link to="/stats">请求统计</Link>,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: '5',
|
|
161
|
+
icon: <ThunderboltOutlined />,
|
|
162
|
+
label: <Link to="/sniffer">接口嗅探</Link>,
|
|
163
|
+
},
|
|
78
164
|
{
|
|
79
165
|
key: '3',
|
|
80
166
|
icon: <SettingOutlined />,
|
|
@@ -83,88 +169,136 @@ const AppLayout = ({ children }) => {
|
|
|
83
169
|
];
|
|
84
170
|
|
|
85
171
|
const breadcrumbs = getBreadcrumbs();
|
|
86
|
-
const colorBgContainer = token.colorBgContainer;
|
|
87
|
-
const colorBgLayout = token.colorBgLayout;
|
|
88
|
-
const borderRadius = token.borderRadius;
|
|
89
172
|
|
|
90
173
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
<div className="
|
|
96
|
-
<div className="logo-
|
|
97
|
-
<
|
|
174
|
+
<ConfigProvider theme={enableCyber ? cyberTheme : lightTheme}>
|
|
175
|
+
<Layout className="app-layout">
|
|
176
|
+
{/* 顶部导航栏 */}
|
|
177
|
+
<Header className="app-header" style={{ background: enableCyber ? '#0f0f1a' : '#ffffff', borderBottom: enableCyber ? '1px solid #1a1a2e' : '1px solid #f0f0f0' }}>
|
|
178
|
+
<div className="header-left">
|
|
179
|
+
<div className="logo-container">
|
|
180
|
+
<div className="logo-icon-group">
|
|
181
|
+
<ExperimentOutlined className="logo-icon logo-icon-secondary" style={{ color: enableCyber ? '#00ff41' : '#52c41a' }} />
|
|
182
|
+
</div>
|
|
183
|
+
<div
|
|
184
|
+
className="logo-text"
|
|
185
|
+
style={enableCyber ? {
|
|
186
|
+
background: 'linear-gradient(135deg, #00f0ff, #00ff41)',
|
|
187
|
+
WebkitBackgroundClip: 'text',
|
|
188
|
+
WebkitTextFillColor: 'transparent',
|
|
189
|
+
} : {
|
|
190
|
+
background: 'linear-gradient(135deg, #1890ff, #52c41a)',
|
|
191
|
+
WebkitBackgroundClip: 'text',
|
|
192
|
+
WebkitTextFillColor: 'transparent',
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
MockMaster
|
|
196
|
+
</div>
|
|
98
197
|
</div>
|
|
99
|
-
<
|
|
198
|
+
<Button
|
|
199
|
+
type="text"
|
|
200
|
+
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
201
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
202
|
+
className="collapse-button"
|
|
203
|
+
/>
|
|
100
204
|
</div>
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<Text type="secondary">v{__APP_VERSION__}</Text>
|
|
117
|
-
</Button>
|
|
118
|
-
</Space>
|
|
119
|
-
</div>
|
|
120
|
-
</Header>
|
|
121
|
-
|
|
122
|
-
<Layout>
|
|
123
|
-
{/* 侧边导航 */}
|
|
124
|
-
<Sider
|
|
125
|
-
width={220}
|
|
126
|
-
className="app-sider"
|
|
127
|
-
collapsed={collapsed}
|
|
128
|
-
collapsible
|
|
129
|
-
trigger={null}
|
|
130
|
-
style={{ background: colorBgContainer }}
|
|
131
|
-
>
|
|
132
|
-
<Menu
|
|
133
|
-
mode="inline"
|
|
134
|
-
selectedKeys={getSelectedKeys()}
|
|
135
|
-
items={menuItems}
|
|
136
|
-
style={{
|
|
137
|
-
height: '100%',
|
|
138
|
-
borderRight: 0,
|
|
139
|
-
padding: '8px',
|
|
140
|
-
}}
|
|
141
|
-
/>
|
|
142
|
-
</Sider>
|
|
205
|
+
<div className="header-right">
|
|
206
|
+
<Space>
|
|
207
|
+
<Button
|
|
208
|
+
type="text"
|
|
209
|
+
size="small"
|
|
210
|
+
className="version-button"
|
|
211
|
+
onClick={showVersionModal}
|
|
212
|
+
>
|
|
213
|
+
<span style={{ color: enableCyber ? '#6b7280' : '#000000d9', fontFamily: "'Share Tech Mono', monospace" }}>
|
|
214
|
+
v{__APP_VERSION__}
|
|
215
|
+
</span>
|
|
216
|
+
</Button>
|
|
217
|
+
</Space>
|
|
218
|
+
</div>
|
|
219
|
+
</Header>
|
|
143
220
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
className="app-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
minHeight: 280,
|
|
154
|
-
}}
|
|
221
|
+
<Layout>
|
|
222
|
+
{/* 侧边导航 */}
|
|
223
|
+
<Sider
|
|
224
|
+
width={220}
|
|
225
|
+
className="app-sider"
|
|
226
|
+
collapsed={collapsed}
|
|
227
|
+
collapsible
|
|
228
|
+
trigger={null}
|
|
229
|
+
style={{ background: enableCyber ? '#0f0f1a' : '#ffffff' }}
|
|
155
230
|
>
|
|
156
|
-
|
|
157
|
-
|
|
231
|
+
<Menu
|
|
232
|
+
mode="inline"
|
|
233
|
+
selectedKeys={getSelectedKeys()}
|
|
234
|
+
items={menuItems}
|
|
235
|
+
style={{
|
|
236
|
+
height: '100%',
|
|
237
|
+
borderRight: 0,
|
|
238
|
+
padding: '8px',
|
|
239
|
+
background: enableCyber ? '#0f0f1a' : '#ffffff',
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
</Sider>
|
|
243
|
+
|
|
244
|
+
{/* 主内容区 */}
|
|
245
|
+
<Layout className="app-content-wrapper" style={{ padding: 0, background: enableCyber ? '#0a0a12' : '#f5f5f5' }}>
|
|
246
|
+
{/* 面包屑和标题 */}
|
|
247
|
+
<div style={{ padding: '16px 24px 0' }}>
|
|
248
|
+
<Breadcrumb className="app-breadcrumb" style={{ marginBottom: 16 }}>
|
|
249
|
+
{breadcrumbs.map((crumb, index) => (
|
|
250
|
+
<Breadcrumb.Item key={index}>
|
|
251
|
+
{index < breadcrumbs.length - 1 ? (
|
|
252
|
+
<Link to={crumb.path} style={{ color: enableCyber ? '#a0a0a0' : '#000000d9' }}>{crumb.title}</Link>
|
|
253
|
+
) : (
|
|
254
|
+
<span style={{ color: enableCyber ? '#e0e0e0' : '#000000d9', fontWeight: 500 }}>{crumb.title}</span>
|
|
255
|
+
)}
|
|
256
|
+
</Breadcrumb.Item>
|
|
257
|
+
))}
|
|
258
|
+
</Breadcrumb>
|
|
259
|
+
<h1 style={{
|
|
260
|
+
margin: 0,
|
|
261
|
+
fontSize: 22,
|
|
262
|
+
fontWeight: 700,
|
|
263
|
+
color: enableCyber ? '#e0e0e0' : '#000000d9',
|
|
264
|
+
fontFamily: enableCyber ? "'Orbitron', sans-serif" : undefined,
|
|
265
|
+
letterSpacing: enableCyber ? '0.5px' : undefined,
|
|
266
|
+
}}>
|
|
267
|
+
{getPageTitle()}
|
|
268
|
+
</h1>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{/* 内容区 */}
|
|
272
|
+
<Content
|
|
273
|
+
className="app-content"
|
|
274
|
+
style={{
|
|
275
|
+
background: enableCyber ? '#0f0f1a' : '#ffffff',
|
|
276
|
+
padding: 24,
|
|
277
|
+
borderRadius: 6,
|
|
278
|
+
minHeight: 280,
|
|
279
|
+
margin: '16px 24px 24px',
|
|
280
|
+
border: enableCyber ? '1px solid #1a1a2e' : '1px solid #f0f0f0',
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{children}
|
|
284
|
+
</Content>
|
|
285
|
+
</Layout>
|
|
158
286
|
</Layout>
|
|
287
|
+
|
|
288
|
+
{/* 版本信息弹窗 */}
|
|
289
|
+
<VersionModal
|
|
290
|
+
visible={versionModalVisible}
|
|
291
|
+
onCancel={hideVersionModal}
|
|
292
|
+
/>
|
|
293
|
+
|
|
294
|
+
{/* 快捷键帮助弹窗 */}
|
|
295
|
+
<KeyboardShortcutsHelp
|
|
296
|
+
visible={shortcutsHelpVisible}
|
|
297
|
+
onClose={() => setShortcutsHelpVisible(false)}
|
|
298
|
+
/>
|
|
159
299
|
</Layout>
|
|
160
|
-
|
|
161
|
-
{/* 版本信息弹窗 */}
|
|
162
|
-
<VersionModal
|
|
163
|
-
visible={versionModalVisible}
|
|
164
|
-
onCancel={hideVersionModal}
|
|
165
|
-
/>
|
|
166
|
-
</Layout>
|
|
300
|
+
</ConfigProvider>
|
|
167
301
|
);
|
|
168
302
|
};
|
|
169
303
|
|
|
170
|
-
export default AppLayout;
|
|
304
|
+
export default AppLayout;
|
|
@@ -6,7 +6,7 @@ const FeatureCard = ({ feature }) => {
|
|
|
6
6
|
|
|
7
7
|
// 生成随机的主题色(实际项目中可能会根据某种规则来确定)
|
|
8
8
|
const getRandomColor = () => {
|
|
9
|
-
const colors = ['#
|
|
9
|
+
const colors = ['#00f0ff', '#00ff41', '#f0e800', '#ff00a0', '#bd00ff'];
|
|
10
10
|
return colors[Math.floor(Math.random() * colors.length)];
|
|
11
11
|
};
|
|
12
12
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Table } from 'antd';
|
|
3
|
+
import { getShortcutsList } from '../hooks/useKeyboardShortcuts';
|
|
4
|
+
|
|
5
|
+
const KeyboardShortcutsHelp = ({ visible, onClose }) => {
|
|
6
|
+
const shortcuts = getShortcutsList();
|
|
7
|
+
|
|
8
|
+
const columns = [
|
|
9
|
+
{
|
|
10
|
+
title: '快捷键',
|
|
11
|
+
dataIndex: 'key',
|
|
12
|
+
key: 'key',
|
|
13
|
+
width: 180,
|
|
14
|
+
render: (text) => (
|
|
15
|
+
<kbd style={{
|
|
16
|
+
display: 'inline-block',
|
|
17
|
+
padding: '4px 8px',
|
|
18
|
+
fontSize: '12px',
|
|
19
|
+
fontFamily: 'monospace',
|
|
20
|
+
lineHeight: 1,
|
|
21
|
+
color: '#333',
|
|
22
|
+
background: '#f5f5f5',
|
|
23
|
+
border: '1px solid #d9d9d9',
|
|
24
|
+
borderRadius: '4px',
|
|
25
|
+
boxShadow: '0 1px 0 rgba(0,0,0,0.1)',
|
|
26
|
+
}}>
|
|
27
|
+
{text}
|
|
28
|
+
</kbd>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: '功能',
|
|
33
|
+
dataIndex: 'desc',
|
|
34
|
+
key: 'desc',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Modal
|
|
40
|
+
title="⌨️ 键盘快捷键"
|
|
41
|
+
open={visible}
|
|
42
|
+
onCancel={onClose}
|
|
43
|
+
footer={null}
|
|
44
|
+
width={480}
|
|
45
|
+
>
|
|
46
|
+
<Table
|
|
47
|
+
dataSource={shortcuts.map((s, i) => ({ ...s, id: i }))}
|
|
48
|
+
columns={columns}
|
|
49
|
+
rowKey="id"
|
|
50
|
+
pagination={false}
|
|
51
|
+
size="small"
|
|
52
|
+
showHeader={false}
|
|
53
|
+
/>
|
|
54
|
+
</Modal>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default KeyboardShortcutsHelp;
|
|
@@ -22,22 +22,20 @@ const PreviewModal = ({
|
|
|
22
22
|
<div style={{
|
|
23
23
|
marginBottom: '16px',
|
|
24
24
|
padding: '8px',
|
|
25
|
-
background: '#f9f9f9',
|
|
26
25
|
borderRadius: '4px',
|
|
27
|
-
borderLeft: '4px solid #
|
|
26
|
+
borderLeft: '4px solid #00f0ff'
|
|
28
27
|
}}>
|
|
29
28
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>描述:</div>
|
|
30
|
-
<div>{previewContent.description}</div>
|
|
29
|
+
<div style={{ opacity: 0.7 }}>{previewContent.description}</div>
|
|
31
30
|
</div>
|
|
32
31
|
)}
|
|
33
32
|
|
|
34
33
|
<div style={{
|
|
35
34
|
maxHeight: '60vh',
|
|
36
35
|
overflow: 'auto',
|
|
37
|
-
backgroundColor: '#f5f5f5',
|
|
38
36
|
padding: '16px',
|
|
39
37
|
borderRadius: '4px',
|
|
40
|
-
fontFamily: 'monospace
|
|
38
|
+
fontFamily: "'Share Tech Mono', monospace",
|
|
41
39
|
}}>
|
|
42
40
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
|
43
41
|
{previewContent?.content}
|
|
@@ -47,4 +45,4 @@ const PreviewModal = ({
|
|
|
47
45
|
);
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export default PreviewModal;
|
|
48
|
+
export default PreviewModal;
|