whistle.mock-plugins 1.2.2-beta.9 → 1.3.0

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 +25 -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 +21 -13
  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
@@ -1,5 +1,5 @@
1
- import React, { useState } from 'react';
2
- import { Layout, Menu, Button, Breadcrumb, Typography, Space, theme, Divider, Badge } from 'antd';
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
- const { Title, Text } = Typography;
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 { token } = theme.useToken();
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
- <Layout className="app-layout">
92
- {/* 顶部导航栏 */}
93
- <Header className="app-header" style={{ background: colorBgContainer }}>
94
- <div className="header-left">
95
- <div className="logo-container">
96
- <div className="logo-icon-group">
97
- <ExperimentOutlined className="logo-icon logo-icon-secondary" />
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
- <div className="logo-text">MockMaster</div>
301
+ <Button
302
+ type="text"
303
+ icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
304
+ onClick={() => setCollapsed(!collapsed)}
305
+ className="collapse-button"
306
+ />
100
307
  </div>
101
- <Button
102
- type="text"
103
- icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
104
- onClick={() => setCollapsed(!collapsed)}
105
- className="collapse-button"
106
- />
107
- </div>
108
- <div className="header-right">
109
- <Space>
110
- <Button
111
- type="text"
112
- size="small"
113
- className="version-button"
114
- onClick={showVersionModal}
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>
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
- {children}
157
- </Content>
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 colors = ['#1890ff', '#13c2c2', '#52c41a', '#faad14', '#722ed1'];
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
- <SimpleMarkdownRenderer>{versionContent}</SimpleMarkdownRenderer>
89
+ <ReactMarkdown>
90
+ {versionContent.replace(/^#\s*🔄?\s*版本更新记录\s*\n+/, '')}
91
+ </ReactMarkdown>
129
92
  </div>
130
93
  )}
131
94
  </div>