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
|
@@ -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,196 @@ 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
|
-
|
|
21
|
+
|
|
22
|
+
const THEMES = ['light', 'cyber', 'earth'];
|
|
18
23
|
|
|
19
24
|
const AppLayout = ({ children }) => {
|
|
20
25
|
const location = useLocation();
|
|
21
26
|
const currentPath = location.pathname;
|
|
22
27
|
const [collapsed, setCollapsed] = useState(false);
|
|
23
28
|
const [versionModalVisible, setVersionModalVisible] = useState(false);
|
|
24
|
-
const
|
|
29
|
+
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
|
|
30
|
+
|
|
31
|
+
// 注册全局键盘快捷键
|
|
32
|
+
useKeyboardShortcuts();
|
|
33
|
+
|
|
34
|
+
// 监听快捷键帮助事件
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const handleHelp = () => setShortcutsHelpVisible(true);
|
|
37
|
+
window.addEventListener('keyboard-shortcuts-help', handleHelp);
|
|
38
|
+
return () => window.removeEventListener('keyboard-shortcuts-help', handleHelp);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// 主题切换(兼容旧版 cyberTheme 存储)
|
|
42
|
+
const [currentTheme, setCurrentTheme] = useState(() => {
|
|
43
|
+
const saved = localStorage.getItem('theme');
|
|
44
|
+
if (saved && THEMES.includes(saved)) return saved;
|
|
45
|
+
// 兼容旧版 boolean 存储
|
|
46
|
+
const oldCyber = localStorage.getItem('cyberTheme');
|
|
47
|
+
return oldCyber === 'false' ? 'light' : oldCyber === 'true' ? 'cyber' : 'light';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
document.body.classList.remove('cyber-theme', 'earth-theme');
|
|
52
|
+
if (currentTheme !== 'light') {
|
|
53
|
+
document.body.classList.add(`${currentTheme}-theme`);
|
|
54
|
+
}
|
|
55
|
+
}, [currentTheme]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const handleStorage = () => {
|
|
59
|
+
const saved = localStorage.getItem('theme');
|
|
60
|
+
if (saved && THEMES.includes(saved)) {
|
|
61
|
+
setCurrentTheme(saved);
|
|
62
|
+
} else {
|
|
63
|
+
const oldCyber = localStorage.getItem('cyberTheme');
|
|
64
|
+
setCurrentTheme(oldCyber === 'false' ? 'light' : 'cyber');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener('storage', handleStorage);
|
|
68
|
+
return () => window.removeEventListener('storage', handleStorage);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const cyberTheme = useMemo(() => ({
|
|
72
|
+
token: {
|
|
73
|
+
colorPrimary: '#00f0ff',
|
|
74
|
+
colorPrimaryHover: '#33f3ff',
|
|
75
|
+
colorPrimaryActive: '#00c2cc',
|
|
76
|
+
colorSuccess: '#00ff41',
|
|
77
|
+
colorWarning: '#f0e800',
|
|
78
|
+
colorError: '#ff4d4f',
|
|
79
|
+
colorInfo: '#00f0ff',
|
|
80
|
+
colorText: '#e0e0e0',
|
|
81
|
+
colorTextSecondary: '#a0a0a0',
|
|
82
|
+
colorTextTertiary: '#6b7280',
|
|
83
|
+
colorBorder: '#1a1a2e',
|
|
84
|
+
colorBorderSecondary: '#1a1a2e',
|
|
85
|
+
colorBgBase: '#050508',
|
|
86
|
+
colorBgContainer: '#0f0f1a',
|
|
87
|
+
colorBgElevated: '#141420',
|
|
88
|
+
colorBgLayout: '#0a0a12',
|
|
89
|
+
colorBgSpotlight: '#141420',
|
|
90
|
+
borderRadius: 6,
|
|
91
|
+
borderRadiusSM: 4,
|
|
92
|
+
borderRadiusLG: 8,
|
|
93
|
+
wireframe: false,
|
|
94
|
+
fontFamily: "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
95
|
+
fontFamilyCode: "'Share Tech Mono', 'SFMono-Regular', Consolas, monospace",
|
|
96
|
+
},
|
|
97
|
+
algorithm: theme.darkAlgorithm,
|
|
98
|
+
}), []);
|
|
99
|
+
|
|
100
|
+
const earthTheme = useMemo(() => ({
|
|
101
|
+
token: {
|
|
102
|
+
colorPrimary: '#b87333',
|
|
103
|
+
colorPrimaryHover: '#c9894d',
|
|
104
|
+
colorPrimaryActive: '#9a5f2a',
|
|
105
|
+
colorSuccess: '#6b8e23',
|
|
106
|
+
colorWarning: '#d4a017',
|
|
107
|
+
colorError: '#c0392b',
|
|
108
|
+
colorInfo: '#8d6e63',
|
|
109
|
+
colorText: '#3d3630',
|
|
110
|
+
colorTextSecondary: '#6d6459',
|
|
111
|
+
colorTextTertiary: '#9e9488',
|
|
112
|
+
colorBorder: '#e0d8cf',
|
|
113
|
+
colorBorderSecondary: '#d5cdc2',
|
|
114
|
+
colorBgBase: '#faf6f0',
|
|
115
|
+
colorBgContainer: '#fdfcfa',
|
|
116
|
+
colorBgElevated: '#f5f0e8',
|
|
117
|
+
colorBgLayout: '#f0ebe3',
|
|
118
|
+
colorBgSpotlight: '#f5f0e8',
|
|
119
|
+
borderRadius: 6,
|
|
120
|
+
borderRadiusSM: 4,
|
|
121
|
+
borderRadiusLG: 8,
|
|
122
|
+
wireframe: false,
|
|
123
|
+
fontFamily: "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
124
|
+
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
|
125
|
+
},
|
|
126
|
+
algorithm: theme.defaultAlgorithm,
|
|
127
|
+
}), []);
|
|
128
|
+
|
|
129
|
+
const lightTheme = useMemo(() => ({
|
|
130
|
+
token: {
|
|
131
|
+
borderRadius: 6,
|
|
132
|
+
borderRadiusSM: 4,
|
|
133
|
+
borderRadiusLG: 8,
|
|
134
|
+
},
|
|
135
|
+
algorithm: theme.defaultAlgorithm,
|
|
136
|
+
}), []);
|
|
137
|
+
|
|
138
|
+
const activeTheme = useMemo(() => {
|
|
139
|
+
if (currentTheme === 'cyber') return cyberTheme;
|
|
140
|
+
if (currentTheme === 'earth') return earthTheme;
|
|
141
|
+
return lightTheme;
|
|
142
|
+
}, [currentTheme, cyberTheme, earthTheme, lightTheme]);
|
|
143
|
+
|
|
144
|
+
// 主题颜色映射(用于 inline style)
|
|
145
|
+
const t = useMemo(() => {
|
|
146
|
+
switch (currentTheme) {
|
|
147
|
+
case 'cyber':
|
|
148
|
+
return {
|
|
149
|
+
headerBg: '#0f0f1a',
|
|
150
|
+
headerBorder: '#1a1a2e',
|
|
151
|
+
logoIcon: '#00ff41',
|
|
152
|
+
logoGradient: 'linear-gradient(135deg, #00f0ff, #00ff41)',
|
|
153
|
+
versionColor: '#6b7280',
|
|
154
|
+
siderBg: '#0f0f1a',
|
|
155
|
+
wrapperBg: '#0a0a12',
|
|
156
|
+
breadcrumbLink: '#a0a0a0',
|
|
157
|
+
breadcrumbCurrent: '#e0e0e0',
|
|
158
|
+
titleColor: '#e0e0e0',
|
|
159
|
+
titleFont: "'Orbitron', sans-serif",
|
|
160
|
+
titleLetterSpacing: '0.5px',
|
|
161
|
+
contentBg: '#0f0f1a',
|
|
162
|
+
contentBorder: '#1a1a2e',
|
|
163
|
+
};
|
|
164
|
+
case 'earth':
|
|
165
|
+
return {
|
|
166
|
+
headerBg: '#fdfcfa',
|
|
167
|
+
headerBorder: '#e0d8cf',
|
|
168
|
+
logoIcon: '#b87333',
|
|
169
|
+
logoGradient: 'linear-gradient(135deg, #b87333, #d4a017)',
|
|
170
|
+
versionColor: '#6d6459',
|
|
171
|
+
siderBg: '#f5f0e8',
|
|
172
|
+
wrapperBg: '#f0ebe3',
|
|
173
|
+
breadcrumbLink: '#6d6459',
|
|
174
|
+
breadcrumbCurrent: '#3d3630',
|
|
175
|
+
titleColor: '#3d3630',
|
|
176
|
+
titleFont: undefined,
|
|
177
|
+
titleLetterSpacing: undefined,
|
|
178
|
+
contentBg: '#fdfcfa',
|
|
179
|
+
contentBorder: '#e0d8cf',
|
|
180
|
+
};
|
|
181
|
+
default: // light
|
|
182
|
+
return {
|
|
183
|
+
headerBg: '#ffffff',
|
|
184
|
+
headerBorder: '#f0f0f0',
|
|
185
|
+
logoIcon: '#52c41a',
|
|
186
|
+
logoGradient: 'linear-gradient(135deg, #1890ff, #52c41a)',
|
|
187
|
+
versionColor: '#000000d9',
|
|
188
|
+
siderBg: '#ffffff',
|
|
189
|
+
wrapperBg: '#f5f5f5',
|
|
190
|
+
breadcrumbLink: '#000000d9',
|
|
191
|
+
breadcrumbCurrent: '#000000d9',
|
|
192
|
+
titleColor: '#000000d9',
|
|
193
|
+
titleFont: undefined,
|
|
194
|
+
titleLetterSpacing: undefined,
|
|
195
|
+
contentBg: '#ffffff',
|
|
196
|
+
contentBorder: '#f0f0f0',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}, [currentTheme]);
|
|
25
200
|
|
|
26
201
|
// 版本弹窗控制方法
|
|
27
202
|
const showVersionModal = () => setVersionModalVisible(true);
|
|
@@ -31,6 +206,8 @@ const AppLayout = ({ children }) => {
|
|
|
31
206
|
const getSelectedKeys = () => {
|
|
32
207
|
if (currentPath === '/' || currentPath === '#/') return ['1'];
|
|
33
208
|
if (currentPath.startsWith('/interface') || currentPath.includes('/interface')) return ['2'];
|
|
209
|
+
if (currentPath === '/stats' || currentPath.includes('/stats')) return ['4'];
|
|
210
|
+
if (currentPath === '/sniffer' || currentPath.includes('/sniffer')) return ['5'];
|
|
34
211
|
if (currentPath === '/settings' || currentPath.includes('/settings')) return ['3'];
|
|
35
212
|
return [];
|
|
36
213
|
};
|
|
@@ -48,6 +225,10 @@ const AppLayout = ({ children }) => {
|
|
|
48
225
|
if (currentPath.includes('/interface/') && currentPath !== '/interface/') {
|
|
49
226
|
breadcrumbs.push({ path: currentPath, title: '功能接口详情' });
|
|
50
227
|
}
|
|
228
|
+
} else if (currentPath === '/stats') {
|
|
229
|
+
breadcrumbs.push({ path: '/stats', title: '请求统计' });
|
|
230
|
+
} else if (currentPath === '/sniffer') {
|
|
231
|
+
breadcrumbs.push({ path: '/sniffer', title: '接口嗅探' });
|
|
51
232
|
} else if (currentPath === '/settings') {
|
|
52
233
|
breadcrumbs.push({ path: '/settings', title: '系统设置' });
|
|
53
234
|
}
|
|
@@ -59,6 +240,8 @@ const AppLayout = ({ children }) => {
|
|
|
59
240
|
const getPageTitle = () => {
|
|
60
241
|
if (currentPath === '/' || currentPath === '#/') return '功能模块管理';
|
|
61
242
|
if (currentPath.startsWith('/interface')) return '接口配置管理';
|
|
243
|
+
if (currentPath === '/stats') return '请求统计';
|
|
244
|
+
if (currentPath === '/sniffer') return '接口嗅探';
|
|
62
245
|
if (currentPath === '/settings') return '系统设置';
|
|
63
246
|
return 'Whistle Mock Plugin';
|
|
64
247
|
};
|
|
@@ -75,6 +258,16 @@ const AppLayout = ({ children }) => {
|
|
|
75
258
|
icon: <ApiOutlined />,
|
|
76
259
|
label: <Link to="/interface">接口管理</Link>,
|
|
77
260
|
},
|
|
261
|
+
{
|
|
262
|
+
key: '4',
|
|
263
|
+
icon: <BarChartOutlined />,
|
|
264
|
+
label: <Link to="/stats">请求统计</Link>,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: '5',
|
|
268
|
+
icon: <ThunderboltOutlined />,
|
|
269
|
+
label: <Link to="/sniffer">接口嗅探</Link>,
|
|
270
|
+
},
|
|
78
271
|
{
|
|
79
272
|
key: '3',
|
|
80
273
|
icon: <SettingOutlined />,
|
|
@@ -83,88 +276,132 @@ const AppLayout = ({ children }) => {
|
|
|
83
276
|
];
|
|
84
277
|
|
|
85
278
|
const breadcrumbs = getBreadcrumbs();
|
|
86
|
-
const colorBgContainer = token.colorBgContainer;
|
|
87
|
-
const colorBgLayout = token.colorBgLayout;
|
|
88
|
-
const borderRadius = token.borderRadius;
|
|
89
279
|
|
|
90
280
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
<div className="
|
|
96
|
-
<div className="logo-
|
|
97
|
-
<
|
|
281
|
+
<ConfigProvider theme={activeTheme}>
|
|
282
|
+
<Layout className="app-layout">
|
|
283
|
+
{/* 顶部导航栏 */}
|
|
284
|
+
<Header className="app-header" style={{ background: t.headerBg, borderBottom: `1px solid ${t.headerBorder}` }}>
|
|
285
|
+
<div className="header-left">
|
|
286
|
+
<div className="logo-container">
|
|
287
|
+
<div className="logo-icon-group">
|
|
288
|
+
<ExperimentOutlined className="logo-icon logo-icon-secondary" style={{ color: t.logoIcon }} />
|
|
289
|
+
</div>
|
|
290
|
+
<div
|
|
291
|
+
className="logo-text"
|
|
292
|
+
style={{
|
|
293
|
+
background: t.logoGradient,
|
|
294
|
+
WebkitBackgroundClip: 'text',
|
|
295
|
+
WebkitTextFillColor: 'transparent',
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
MockMaster
|
|
299
|
+
</div>
|
|
98
300
|
</div>
|
|
99
|
-
<
|
|
301
|
+
<Button
|
|
302
|
+
type="text"
|
|
303
|
+
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
304
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
305
|
+
className="collapse-button"
|
|
306
|
+
/>
|
|
100
307
|
</div>
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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>
|
|
143
|
-
|
|
144
|
-
{/* 主内容区 */}
|
|
145
|
-
<Layout className="app-content-wrapper" style={{ padding: 0, background: colorBgLayout }}>
|
|
146
|
-
{/* 内容区 */}
|
|
147
|
-
<Content
|
|
148
|
-
className="app-content"
|
|
149
|
-
style={{
|
|
150
|
-
background: colorBgContainer,
|
|
151
|
-
padding: 24,
|
|
152
|
-
borderRadius: borderRadius,
|
|
153
|
-
minHeight: 280,
|
|
154
|
-
}}
|
|
308
|
+
<div className="header-right">
|
|
309
|
+
<Space>
|
|
310
|
+
<Button
|
|
311
|
+
type="text"
|
|
312
|
+
size="small"
|
|
313
|
+
className="version-button"
|
|
314
|
+
onClick={showVersionModal}
|
|
315
|
+
>
|
|
316
|
+
<span style={{ color: t.versionColor, fontFamily: "'Share Tech Mono', monospace" }}>
|
|
317
|
+
v{__APP_VERSION__}
|
|
318
|
+
</span>
|
|
319
|
+
</Button>
|
|
320
|
+
</Space>
|
|
321
|
+
</div>
|
|
322
|
+
</Header>
|
|
323
|
+
|
|
324
|
+
<Layout>
|
|
325
|
+
{/* 侧边导航 */}
|
|
326
|
+
<Sider
|
|
327
|
+
width={220}
|
|
328
|
+
className="app-sider"
|
|
329
|
+
collapsed={collapsed}
|
|
330
|
+
collapsible
|
|
331
|
+
trigger={null}
|
|
332
|
+
style={{ background: t.siderBg }}
|
|
155
333
|
>
|
|
156
|
-
|
|
157
|
-
|
|
334
|
+
<Menu
|
|
335
|
+
mode="inline"
|
|
336
|
+
selectedKeys={getSelectedKeys()}
|
|
337
|
+
items={menuItems}
|
|
338
|
+
style={{
|
|
339
|
+
height: '100%',
|
|
340
|
+
borderRight: 0,
|
|
341
|
+
padding: '8px',
|
|
342
|
+
background: t.siderBg,
|
|
343
|
+
}}
|
|
344
|
+
/>
|
|
345
|
+
</Sider>
|
|
346
|
+
|
|
347
|
+
{/* 主内容区 */}
|
|
348
|
+
<Layout className="app-content-wrapper" style={{ padding: 0, background: t.wrapperBg }}>
|
|
349
|
+
{/* 面包屑和标题 */}
|
|
350
|
+
<div style={{ padding: '16px 24px 0' }}>
|
|
351
|
+
<Breadcrumb className="app-breadcrumb" style={{ marginBottom: 16 }}>
|
|
352
|
+
{breadcrumbs.map((crumb, index) => (
|
|
353
|
+
<Breadcrumb.Item key={index}>
|
|
354
|
+
{index < breadcrumbs.length - 1 ? (
|
|
355
|
+
<Link to={crumb.path} style={{ color: t.breadcrumbLink }}>{crumb.title}</Link>
|
|
356
|
+
) : (
|
|
357
|
+
<span style={{ color: t.breadcrumbCurrent, fontWeight: 500 }}>{crumb.title}</span>
|
|
358
|
+
)}
|
|
359
|
+
</Breadcrumb.Item>
|
|
360
|
+
))}
|
|
361
|
+
</Breadcrumb>
|
|
362
|
+
<h1 style={{
|
|
363
|
+
margin: 0,
|
|
364
|
+
fontSize: 22,
|
|
365
|
+
fontWeight: 700,
|
|
366
|
+
color: t.titleColor,
|
|
367
|
+
fontFamily: t.titleFont,
|
|
368
|
+
letterSpacing: t.titleLetterSpacing,
|
|
369
|
+
}}>
|
|
370
|
+
{getPageTitle()}
|
|
371
|
+
</h1>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* 内容区 */}
|
|
375
|
+
<Content
|
|
376
|
+
className="app-content"
|
|
377
|
+
style={{
|
|
378
|
+
background: t.contentBg,
|
|
379
|
+
padding: 24,
|
|
380
|
+
borderRadius: 6,
|
|
381
|
+
minHeight: 280,
|
|
382
|
+
margin: '16px 24px 24px',
|
|
383
|
+
border: `1px solid ${t.contentBorder}`,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
{children}
|
|
387
|
+
</Content>
|
|
388
|
+
</Layout>
|
|
158
389
|
</Layout>
|
|
390
|
+
|
|
391
|
+
{/* 版本信息弹窗 */}
|
|
392
|
+
<VersionModal
|
|
393
|
+
visible={versionModalVisible}
|
|
394
|
+
onCancel={hideVersionModal}
|
|
395
|
+
/>
|
|
396
|
+
|
|
397
|
+
{/* 快捷键帮助弹窗 */}
|
|
398
|
+
<KeyboardShortcutsHelp
|
|
399
|
+
visible={shortcutsHelpVisible}
|
|
400
|
+
onClose={() => setShortcutsHelpVisible(false)}
|
|
401
|
+
/>
|
|
159
402
|
</Layout>
|
|
160
|
-
|
|
161
|
-
{/* 版本信息弹窗 */}
|
|
162
|
-
<VersionModal
|
|
163
|
-
visible={versionModalVisible}
|
|
164
|
-
onCancel={hideVersionModal}
|
|
165
|
-
/>
|
|
166
|
-
</Layout>
|
|
403
|
+
</ConfigProvider>
|
|
167
404
|
);
|
|
168
405
|
};
|
|
169
406
|
|
|
170
|
-
export default AppLayout;
|
|
407
|
+
export default AppLayout;
|
|
@@ -4,9 +4,14 @@ import '../styles/feature-card.css';
|
|
|
4
4
|
const FeatureCard = ({ feature }) => {
|
|
5
5
|
const { name, description, active, interfaceCount, createdAt } = feature;
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 生成随机的主题色(根据当前主题选择色系)
|
|
8
8
|
const getRandomColor = () => {
|
|
9
|
-
const
|
|
9
|
+
const isEarth = document.body.classList.contains('earth-theme');
|
|
10
|
+
if (isEarth) {
|
|
11
|
+
const colors = ['#b87333', '#6b8e23', '#d4a017', '#8d6e63', '#c0392b'];
|
|
12
|
+
return colors[Math.floor(Math.random() * colors.length)];
|
|
13
|
+
}
|
|
14
|
+
const colors = ['#00f0ff', '#00ff41', '#f0e800', '#ff00a0', '#bd00ff'];
|
|
10
15
|
return colors[Math.floor(Math.random() * colors.length)];
|
|
11
16
|
};
|
|
12
17
|
|
|
@@ -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;
|
|
@@ -1,47 +1,8 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Modal, Spin, Alert } from 'antd';
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
4
|
import '../styles/version-modal.css';
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* 简单的Markdown渲染器组件
|
|
7
|
-
* @param {Object} props - 组件属性
|
|
8
|
-
* @param {string} props.children - 要渲染的Markdown文本
|
|
9
|
-
*/
|
|
10
|
-
const SimpleMarkdownRenderer = ({ children }) => {
|
|
11
|
-
const renderMarkdown = (text) => {
|
|
12
|
-
return text
|
|
13
|
-
.split('\n')
|
|
14
|
-
.map((line, index) => {
|
|
15
|
-
// 处理标题
|
|
16
|
-
if (line.startsWith('# ')) {
|
|
17
|
-
return <h1 key={index}>{line.substring(2)}</h1>;
|
|
18
|
-
}
|
|
19
|
-
if (line.startsWith('## ')) {
|
|
20
|
-
return <h2 key={index}>{line.substring(3)}</h2>;
|
|
21
|
-
}
|
|
22
|
-
if (line.startsWith('### ')) {
|
|
23
|
-
return <h3 key={index}>{line.substring(4)}</h3>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 处理列表项
|
|
27
|
-
if (line.startsWith('- ')) {
|
|
28
|
-
return <li key={index}>{line.substring(2)}</li>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 处理空行
|
|
32
|
-
if (line.trim() === '') {
|
|
33
|
-
return <br key={index} />;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 处理加粗文本
|
|
37
|
-
const boldText = line.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
38
|
-
return <p key={index} dangerouslySetInnerHTML={{ __html: boldText }} />;
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return <div className="simple-markdown">{renderMarkdown(children)}</div>;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
6
|
/**
|
|
46
7
|
* 获取版本信息的请求函数
|
|
47
8
|
* @returns {Promise<Object>} 版本信息
|
|
@@ -125,7 +86,9 @@ const VersionModal = ({ visible, onCancel }) => {
|
|
|
125
86
|
|
|
126
87
|
{!loading && !error && versionContent && (
|
|
127
88
|
<div className="version-markdown-content">
|
|
128
|
-
<
|
|
89
|
+
<ReactMarkdown>
|
|
90
|
+
{versionContent.replace(/^#\s*🔄?\s*版本更新记录\s*\n+/, '')}
|
|
91
|
+
</ReactMarkdown>
|
|
129
92
|
</div>
|
|
130
93
|
)}
|
|
131
94
|
</div>
|