完善了MD内部锚点和页面链接

main
mula.liu 2025-11-14 10:50:16 +08:00
parent e9691882f0
commit 1d1f50ec10
6 changed files with 226 additions and 36 deletions

View File

@ -503,7 +503,7 @@ function PageTemplate() {
- 顶部导航栏(搜索、消息、用户信息)
- 内容区域(可滚动)
**详细文档**: [主框架页面设计规范](../docs/pages/main-layout.md)
**详细文档**: [主框架页面设计规范](../docs/layouts/main-layout.md)
**主要特性**
- 基于 NEX Logo 的品牌配色 (#b8178d)

View File

@ -150,7 +150,7 @@
| 阴影 | 0 1px 4px rgba(0, 21, 41, 0.08) |
| 位置 | stickytop: 0 |
| 层级 | z-index: 9 |
| 内边距 | 0 24px |
| 内边距 | 0 16px |
### 2.2 左侧区域
@ -223,14 +223,14 @@
| 属性 | 值 |
|------|---|
| 背景色 | #f5f5f5 |
| 内边距 | 24px |
| 内边距 | 16px |
| 高度 | calc(100vh - 64px) |
| 滚动 | overflow-y: auto |
### 3.2 内容容器
- 最大宽度:根据业务需求,建议 1200-1600px
- 内边距:24px
- 内边距:16px
- 背景色:根据内容类型,卡片为 #fff
### 3.3 滚动行为
@ -239,6 +239,8 @@
- 顶部导航栏和侧边栏保持固定
- 滚动条样式与全局一致
详细设计见:[主内容区布局](./content-area-layout.md)
---
## 4. 响应式适配
@ -378,63 +380,49 @@ MainLayout/
---
## 8. 示例页面
## 8. 可访问性
### 8.1 概览页 (Overview)
作为主框架的示例页面,展示了:
- 统计卡片布局
- 图表展示
- 数据可视化
- 响应式栅格系统
详细设计见:[概览页设计文档](./overview.md)
---
## 9. 可访问性
### 9.1 键盘导航
### 8.1 键盘导航
- 支持 Tab 键在可交互元素间切换
- 支持 Enter 键激活菜单项
- 支持方向键在菜单间导航
### 9.2 语义化标签
### 8.2 语义化标签
- 使用 nav 标签包裹导航菜单
- 使用 header 标签包裹顶部栏
- 使用 main 标签包裹主内容区
### 9.3 对比度
### 8.3 对比度
- 所有文本与背景对比度 ≥ 4.5:1
- 图标与背景对比度 ≥ 3:1
---
## 10. 性能优化
## 9. 性能优化
### 10.1 懒加载
### 9.1 懒加载
- 页面组件使用 React.lazy 懒加载
- 减少首屏加载时间
### 10.2 防抖优化
### 9.2 防抖优化
- 搜索框输入使用防抖处理
- 窗口大小变化使用节流处理
### 10.3 虚拟滚动
### 9.3 虚拟滚动
- 菜单项较多时考虑虚拟滚动
- 长列表使用虚拟化技术
---
## 11. 开发指南
## 10. 开发指南
### 11.1 添加新菜单
### 10.1 添加新菜单
1. 编辑 `src/constants/menuData.json`
2. 添加菜单项配置
@ -442,13 +430,13 @@ MainLayout/
4. 创建对应的页面组件
5. 在 `App.jsx` 中添加路由
### 11.2 自定义主题
### 10.2 自定义主题
1. 编辑 `src/main.jsx` 中的 theme 配置
2. 修改 `tailwind.config.js` 中的颜色系统
3. 更新 `src/styles/globals.css` 中的 CSS 变量
### 11.3 扩展功能
### 10.3 扩展功能
- 添加面包屑导航
- 添加页签 (Tabs) 功能
@ -457,7 +445,7 @@ MainLayout/
---
## 12. 常见问题
## 11. 常见问题
### Q1: 如何修改侧边栏默认展开状态?

51
package-lock.json generated
View File

@ -11,12 +11,14 @@
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.0",
"echarts": "^6.0.0",
"github-slugger": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.20.0",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
@ -2844,6 +2846,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
@ -3071,6 +3079,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-heading-rank": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz",
"integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-is-element": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
@ -3178,6 +3199,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/hast-util-to-string": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
"integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-text": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
@ -6458,6 +6492,23 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-slug": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/rehype-slug/-/rehype-slug-6.0.0.tgz",
"integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"github-slugger": "^2.0.0",
"hast-util-heading-rank": "^3.0.0",
"hast-util-to-string": "^3.0.0",
"unist-util-visit": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz",

View File

@ -14,12 +14,14 @@
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.0",
"echarts": "^6.0.0",
"github-slugger": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.20.0",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {

View File

@ -1,9 +1,10 @@
import { useState, useEffect } from 'react'
import { Layout, Menu, Spin } from 'antd'
import { FileTextOutlined } from '@ant-design/icons'
import { useState, useEffect, useRef } from 'react'
import { Layout, Menu, Spin, FloatButton } from 'antd'
import { FileTextOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import rehypeSlug from 'rehype-slug'
import rehypeHighlight from 'rehype-highlight'
import 'highlight.js/styles/github.css'
import docsMenuData from '../data/docsMenuData.json'
@ -15,6 +16,21 @@ function DocsPage() {
const [selectedKey, setSelectedKey] = useState('design-cookbook')
const [markdownContent, setMarkdownContent] = useState('')
const [loading, setLoading] = useState(false)
const [currentDocPath, setCurrentDocPath] = useState('')
const contentRef = useRef(null)
// key
const buildPathToKeyMap = () => {
const map = {}
docsMenuData.forEach((group) => {
group.children.forEach((item) => {
map[item.path] = item.key
})
})
return map
}
const pathToKeyMap = buildPathToKeyMap()
//
const menuItems = docsMenuData.map((group) => ({
@ -47,6 +63,12 @@ function DocsPage() {
if (response.ok) {
const text = await response.text()
setMarkdownContent(text)
setCurrentDocPath(path)
//
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
}
} else {
setMarkdownContent('# 文档加载失败\n\n无法加载该文档请稍后重试。')
}
@ -64,6 +86,90 @@ function DocsPage() {
loadMarkdown(key)
}
//
const resolvePath = (currentPath, relativePath) => {
//
if (relativePath.startsWith('/')) {
return relativePath
}
//
const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/'))
//
const parts = relativePath.split('/')
const dirParts = currentDir.split('/')
for (const part of parts) {
if (part === '..') {
dirParts.pop()
} else if (part !== '.' && part !== '') {
dirParts.push(part)
}
}
return dirParts.join('/')
}
//
const handleDocLinkClick = (href) => {
//
const absolutePath = resolvePath(currentDocPath, href)
// key
const targetKey = pathToKeyMap[absolutePath]
if (targetKey) {
setSelectedKey(targetKey)
loadMarkdown(targetKey)
} else {
console.warn('未找到文档:', absolutePath)
}
}
// ReactMarkdown
const markdownComponents = {
//
a: ({ node, href, children, ...props }) => {
// .md
if (href && href.endsWith('.md')) {
return (
<a
{...props}
href="#"
onClick={(e) => {
e.preventDefault()
handleDocLinkClick(href)
}}
style={{ color: '#1677ff', cursor: 'pointer', textDecoration: 'underline' }}
>
{children}
</a>
)
}
//
if (href && href.startsWith('#')) {
return (
<a
{...props}
href={href}
style={{ color: '#1677ff', cursor: 'pointer', textDecoration: 'underline' }}
>
{children}
</a>
)
}
//
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
)
},
}
//
useEffect(() => {
loadMarkdown(selectedKey)
@ -88,7 +194,7 @@ function DocsPage() {
</Sider>
{/* 右侧内容 */}
<Content className="docs-content">
<Content className="docs-content" ref={contentRef}>
<div className="docs-content-wrapper">
{loading ? (
<div className="docs-loading">
@ -98,13 +204,26 @@ function DocsPage() {
<div className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight]}
rehypePlugins={[rehypeRaw, rehypeSlug, rehypeHighlight]}
components={markdownComponents}
>
{markdownContent}
</ReactMarkdown>
</div>
)}
</div>
{/* 返回顶部按钮 */}
<FloatButton
icon={<VerticalAlignTopOutlined />}
type="primary"
style={{ right: 24 }}
onClick={() => {
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' })
}
}}
/>
</Content>
</Layout>
</div>

View File

@ -1629,6 +1629,11 @@ get-symbol-description@^1.1.0:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
github-slugger@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/github-slugger/-/github-slugger-2.0.0.tgz"
integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
@ -1749,6 +1754,13 @@ hast-util-from-parse5@^8.0.0:
vfile-location "^5.0.0"
web-namespaces "^2.0.0"
hast-util-heading-rank@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz"
integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==
dependencies:
"@types/hast" "^3.0.0"
hast-util-is-element@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz"
@ -1816,6 +1828,13 @@ hast-util-to-parse5@^8.0.0:
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-to-string@^3.0.0:
version "3.0.1"
resolved "https://registry.npmmirror.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz"
integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==
dependencies:
"@types/hast" "^3.0.0"
hast-util-to-text@^4.0.0:
version "4.0.2"
resolved "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz"
@ -3574,6 +3593,17 @@ rehype-raw@^7.0.0:
hast-util-raw "^9.0.0"
vfile "^6.0.0"
rehype-slug@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/rehype-slug/-/rehype-slug-6.0.0.tgz"
integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==
dependencies:
"@types/hast" "^3.0.0"
github-slugger "^2.0.0"
hast-util-heading-rank "^3.0.0"
hast-util-to-string "^3.0.0"
unist-util-visit "^5.0.0"
remark-gfm@^4.0.1:
version "4.0.1"
resolved "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz"