diff --git a/docs/DESIGN_COOKBOOK.md b/docs/DESIGN_COOKBOOK.md index 231a221..68d56e5 100644 --- a/docs/DESIGN_COOKBOOK.md +++ b/docs/DESIGN_COOKBOOK.md @@ -503,7 +503,7 @@ function PageTemplate() { - 顶部导航栏(搜索、消息、用户信息) - 内容区域(可滚动) -**详细文档**: [主框架页面设计规范](../docs/pages/main-layout.md) +**详细文档**: [主框架页面设计规范](../docs/layouts/main-layout.md) **主要特性**: - 基于 NEX Logo 的品牌配色 (#b8178d) diff --git a/docs/layouts/main-layout.md b/docs/layouts/main-layout.md index 1560792..547faaa 100644 --- a/docs/layouts/main-layout.md +++ b/docs/layouts/main-layout.md @@ -150,7 +150,7 @@ | 阴影 | 0 1px 4px rgba(0, 21, 41, 0.08) | | 位置 | sticky,top: 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: 如何修改侧边栏默认展开状态? diff --git a/package-lock.json b/package-lock.json index cd189e4..0a1b6ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d5ec381..b89d02e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/pages/DocsPage.jsx b/src/pages/DocsPage.jsx index 5a1ccac..e20f754 100644 --- a/src/pages/DocsPage.jsx +++ b/src/pages/DocsPage.jsx @@ -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 ( + { + e.preventDefault() + handleDocLinkClick(href) + }} + style={{ color: '#1677ff', cursor: 'pointer', textDecoration: 'underline' }} + > + {children} + + ) + } + + // 判断是否为锚点链接(页面内跳转) + if (href && href.startsWith('#')) { + return ( + + {children} + + ) + } + + // 外部链接 + return ( + + {children} + + ) + }, + } + // 初始加载默认文档 useEffect(() => { loadMarkdown(selectedKey) @@ -88,7 +194,7 @@ function DocsPage() { {/* 右侧内容 */} - +
{loading ? (
@@ -98,13 +204,26 @@ function DocsPage() {
{markdownContent}
)}
+ + {/* 返回顶部按钮 */} + } + type="primary" + style={{ right: 24 }} + onClick={() => { + if (contentRef.current) { + contentRef.current.scrollTo({ top: 0, behavior: 'smooth' }) + } + }} + />
diff --git a/yarn.lock b/yarn.lock index 6fbbda8..2141f74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"