Skip to content

Commit e0483c0

Browse files
committed
feat(navigation): 增加汉堡菜单及文档集合配置支持
- 配置文件新增导航项移动端显示控制 showInMobile 字段 - 新增 collections 支持自动扫描文档集合及导航显示配置 - 优化文件夹配置生成,结合 config.yml 中的文档集合配置 - Header 组件支持动态导航分组,移动端显示与汉堡菜单分离 - 新增汉堡菜单组件,支持移动端隐藏导航项的折叠展示 - 添加相应样式,适配移动端导航和汉堡菜单显示 - 国际化中增加教程中心页面及导航文本支持 - 优化 App.jsx 中配置加载逻辑,依赖 siteConfig 后加载动态导航配置
1 parent d14e6c9 commit e0483c0

9 files changed

Lines changed: 436 additions & 80 deletions

File tree

public/config.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,29 @@ social:
2828
url: "https://twitter.com/yourusername"
2929

3030
# 导航菜单
31+
# showInMobile: 控制在移动端是否默认显示(false则放入汉堡菜单)
3132
navigation:
3233
- name: "首页"
3334
path: "/"
35+
showInMobile: true # 始终显示
3436
- name: "关于"
3537
path: "/about"
38+
showInMobile: true # 始终显示
3639
- name: "项目"
3740
path: "/projects"
41+
showInMobile: false # 放入汉堡菜单
3842
- name: "博客"
3943
path: "/posts"
44+
showInMobile: false # 放入汉堡菜单
4045
- name: "文档"
4146
path: "/pages"
47+
showInMobile: false # 放入汉堡菜单
4248
- name: "文件"
4349
path: "/files"
50+
showInMobile: false # 放入汉堡菜单
4451
- name: "动态"
4552
path: "/news"
53+
showInMobile: false # 放入汉堡菜单
4654

4755
# 主题配置
4856
theme:
@@ -60,6 +68,23 @@ content:
6068
filesPath: "/content/files" # PDF 和文档文件目录
6169
assetsPath: "/assets" # 网站资源(图片、图标等)
6270

71+
# 文档集合配置
72+
# 控制哪些文件夹被自动扫描和显示
73+
# enabled: true 表示启用此文件夹的自动扫描
74+
# enabled: false 表示禁用,该文件夹不会出现在导航中
75+
collections:
76+
tutorials:
77+
enabled: true # 是否启用教程中心
78+
showInNavigation: true # 是否在导航栏显示
79+
showInMobile: false # 移动端是否直接显示(false则放入汉堡菜单)
80+
order: 1 # 导航顺序
81+
# 可以添加更多自定义文档集合
82+
# guides:
83+
# enabled: false
84+
# showInNavigation: true
85+
# showInMobile: false
86+
# order: 2
87+
6388
# 文件配置
6489
# 支持两种方式:
6590
# 1. 自动扫描:系统会自动扫描所有 Markdown 中的 /content/files/ 链接

src/App.jsx

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,61 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
3-
import { ConfigProvider, useSiteConfig } from './config/ConfigContext';
4-
import { ThemeProvider } from './components/theme/ThemeContext';
5-
import { I18nProvider } from './i18n/I18nContext';
6-
import { Layout } from './components/layout/Layout';
7-
import { Home } from './pages/Home';
8-
import { About } from './pages/About';
9-
import { Projects } from './pages/Projects';
10-
import { Posts } from './pages/Posts';
11-
import { Pages } from './pages/Pages';
12-
import { Files } from './pages/Files';
13-
import { News } from './pages/News';
14-
import { DynamicDocumentPage } from './pages/DynamicDocumentPage';
15-
import { generateFolderConfigs } from './utils/folderScanner';
1+
import React, { useEffect, useState } from 'react'
2+
import { BrowserRouter, Routes, Route } from 'react-router-dom'
3+
import { ConfigProvider, useSiteConfig } from './config/ConfigContext'
4+
import { ThemeProvider } from './components/theme/ThemeContext'
5+
import { I18nProvider } from './i18n/I18nContext'
6+
import { Layout } from './components/layout/Layout'
7+
import { Home } from './pages/Home'
8+
import { About } from './pages/About'
9+
import { Projects } from './pages/Projects'
10+
import { Posts } from './pages/Posts'
11+
import { Pages } from './pages/Pages'
12+
import { Files } from './pages/Files'
13+
import { News } from './pages/News'
14+
import { DynamicDocumentPage } from './pages/DynamicDocumentPage'
15+
import { generateFolderConfigs } from './utils/folderScanner'
1616

1717
function AppContent() {
18-
const siteConfig = useSiteConfig();
19-
const [folderConfigs, setFolderConfigs] = useState([]);
20-
const [loading, setLoading] = useState(true);
21-
18+
const siteConfig = useSiteConfig()
19+
const [folderConfigs, setFolderConfigs] = useState([])
20+
const [loading, setLoading] = useState(true)
21+
2222
// 动态设置页面标题
2323
useEffect(() => {
2424
if (siteConfig?.title) {
25-
document.title = siteConfig.title;
25+
document.title = siteConfig.title
2626
}
27-
}, [siteConfig?.title]);
28-
27+
}, [siteConfig?.title])
28+
2929
// 加载文件夹配置
3030
useEffect(() => {
3131
async function loadConfigs() {
3232
try {
33-
const configs = await generateFolderConfigs();
33+
// 传递 siteConfig 给 folderScanner
34+
const configs = await generateFolderConfigs(siteConfig)
3435
// 过滤掉 posts 和 pages,它们已经有专门的页面
3536
const dynamicConfigs = configs.filter(
36-
config => config.name !== 'posts' && config.name !== 'pages' && config.name !== 'files'
37-
);
38-
setFolderConfigs(dynamicConfigs);
37+
config =>
38+
config.name !== 'posts' &&
39+
config.name !== 'pages' &&
40+
config.name !== 'files'
41+
)
42+
setFolderConfigs(dynamicConfigs)
3943
} catch (error) {
40-
console.error('加载文件夹配置失败:', error);
44+
console.error('加载文件夹配置失败:', error)
4145
} finally {
42-
setLoading(false);
46+
setLoading(false)
4347
}
4448
}
45-
loadConfigs();
46-
}, []);
47-
49+
50+
// 只有当 siteConfig 加载完成后才执行
51+
if (siteConfig) {
52+
loadConfigs()
53+
}
54+
}, [siteConfig])
55+
4856
// 获取 Vite 配置的基础路径,支持子目录部署
49-
const basename = import.meta.env.BASE_URL || '/';
50-
57+
const basename = import.meta.env.BASE_URL || '/'
58+
5159
return (
5260
<I18nProvider>
5361
<ThemeProvider>
@@ -61,29 +69,29 @@ function AppContent() {
6169
<Route path="pages" element={<Pages />} />
6270
<Route path="files" element={<Files />} />
6371
<Route path="news" element={<News />} />
64-
72+
6573
{/* 动态生成的路由 - 始终渲染,即使 loading */}
6674
{folderConfigs.map(config => (
67-
<Route
75+
<Route
6876
key={config.name}
69-
path={config.name}
70-
element={<DynamicDocumentPage config={config} />}
77+
path={config.name}
78+
element={<DynamicDocumentPage config={config} />}
7179
/>
7280
))}
7381
</Route>
7482
</Routes>
7583
</BrowserRouter>
7684
</ThemeProvider>
7785
</I18nProvider>
78-
);
86+
)
7987
}
8088

8189
function App() {
8290
return (
8391
<ConfigProvider>
8492
<AppContent />
8593
</ConfigProvider>
86-
);
94+
)
8795
}
8896

89-
export default App;
97+
export default App
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useState, useEffect, useRef } from 'react'
2+
import { Link } from 'react-router-dom'
3+
import styles from './HamburgerMenu.module.css'
4+
5+
/**
6+
* 汉堡菜单组件
7+
* @param {Array} items - 菜单项数组
8+
* @param {Function} onItemClick - 点击菜单项的回调
9+
*/
10+
export function HamburgerMenu({ items = [], onItemClick }) {
11+
const [isOpen, setIsOpen] = useState(false)
12+
const menuRef = useRef(null)
13+
14+
// 切换菜单状态
15+
const toggleMenu = () => {
16+
setIsOpen(!isOpen)
17+
}
18+
19+
// 关闭菜单
20+
const closeMenu = () => {
21+
setIsOpen(false)
22+
}
23+
24+
// 点击菜单项
25+
const handleItemClick = item => {
26+
closeMenu()
27+
if (onItemClick) {
28+
onItemClick(item)
29+
}
30+
}
31+
32+
// 点击外部区域关闭菜单
33+
useEffect(() => {
34+
const handleClickOutside = event => {
35+
if (menuRef.current && !menuRef.current.contains(event.target)) {
36+
closeMenu()
37+
}
38+
}
39+
40+
if (isOpen) {
41+
document.addEventListener('mousedown', handleClickOutside)
42+
document.addEventListener('touchstart', handleClickOutside)
43+
}
44+
45+
return () => {
46+
document.removeEventListener('mousedown', handleClickOutside)
47+
document.removeEventListener('touchstart', handleClickOutside)
48+
}
49+
}, [isOpen])
50+
51+
// ESC 键关闭菜单
52+
useEffect(() => {
53+
const handleEscape = event => {
54+
if (event.key === 'Escape') {
55+
closeMenu()
56+
}
57+
}
58+
59+
if (isOpen) {
60+
document.addEventListener('keydown', handleEscape)
61+
}
62+
63+
return () => {
64+
document.removeEventListener('keydown', handleEscape)
65+
}
66+
}, [isOpen])
67+
68+
if (!items || items.length === 0) {
69+
return null
70+
}
71+
72+
return (
73+
<div className={styles.hamburgerContainer} ref={menuRef}>
74+
{/* 汉堡按钮 */}
75+
<button
76+
className={`${styles.hamburgerButton} ${isOpen ? styles.open : ''}`}
77+
onClick={toggleMenu}
78+
aria-label="菜单"
79+
aria-expanded={isOpen}
80+
>
81+
<span className={styles.line}></span>
82+
<span className={styles.line}></span>
83+
<span className={styles.line}></span>
84+
</button>
85+
86+
{/* 下拉菜单 */}
87+
{isOpen && (
88+
<div className={styles.dropdownMenu}>
89+
{items.map(item => (
90+
<Link
91+
key={item.path}
92+
to={item.path}
93+
className={styles.menuItem}
94+
onClick={() => handleItemClick(item)}
95+
>
96+
{item.label}
97+
</Link>
98+
))}
99+
</div>
100+
)}
101+
</div>
102+
)
103+
}

0 commit comments

Comments
 (0)