HelpLayout.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import React, { useEffect, useMemo, useState } from 'react';
  2. import { Layout, Menu } from 'antd';
  3. import { useLocation, useNavigate, useParams } from 'react-router-dom';
  4. import { helpMenu, HelpMenuItem } from '../menu';
  5. import MarkdownViewer, { TocItem } from './MarkdownViewer';
  6. const { Sider, Content } = Layout;
  7. // 动态加载 md 文件(Vite 6 写法)
  8. const files = import.meta.glob('/src/help/docs/**/*.md', { query: '?raw', import: 'default' });
  9. function findMenuByPath(pathname: string): HelpMenuItem | undefined {
  10. const flat: HelpMenuItem[] = [];
  11. const dfs = (arr: HelpMenuItem[]) => arr.forEach((i) => { flat.push(i); i.children && dfs(i.children); });
  12. dfs(helpMenu);
  13. return flat.find((i) => i.path.split('#')[0] === pathname);
  14. }
  15. const HelpLayout: React.FC = () => {
  16. const params = useParams();
  17. const navigate = useNavigate();
  18. const location = useLocation();
  19. const pathname = location.pathname;
  20. const [md, setMd] = useState('');
  21. const [toc, setToc] = useState<TocItem[]>([]);
  22. const current = useMemo(() => findMenuByPath(pathname), [pathname]);
  23. useEffect(() => {
  24. // 根据菜单项的 doc 加载 md
  25. const doc = current?.doc;
  26. if (!doc) { setMd('# 欢迎使用帮助中心'); return; }
  27. const key = `/src/help/docs/${doc}`;
  28. const loader = (files as any)[key];
  29. if (loader) {
  30. (loader as () => Promise<string> )().then((raw) => setMd(raw));
  31. } else {
  32. setMd('> 文档未找到: ' + doc);
  33. }
  34. }, [current?.doc]);
  35. useEffect(() => {
  36. // 定位到 hash 标题
  37. if (!location.hash) return;
  38. const id = decodeURIComponent(location.hash.slice(1));
  39. const el = document.getElementById(id);
  40. if (el) {
  41. el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  42. }
  43. }, [md, location.hash]);
  44. // build antd menu items
  45. const menuItems = useMemo(() => {
  46. const toAntd = (items: HelpMenuItem[]): any[] => items.map((it) => ({
  47. key: it.path,
  48. label: it.title,
  49. children: it.children ? toAntd(it.children) : undefined,
  50. }));
  51. return toAntd(helpMenu);
  52. }, []);
  53. const defaultOpenKeys = useMemo(() => {
  54. const keys: string[] = [];
  55. const dfs = (items: HelpMenuItem[]) => {
  56. items.forEach((it) => {
  57. if (it.children && it.children.length) {
  58. keys.push(it.path);
  59. dfs(it.children);
  60. }
  61. });
  62. };
  63. dfs(helpMenu);
  64. return keys;
  65. }, []);
  66. return (
  67. <Layout style={{ height: '100%', background: 'transparent' }}>
  68. <Sider width={240} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
  69. <div style={{ padding: 12, fontWeight: 600 }}>帮助文档</div>
  70. <Menu
  71. mode="inline"
  72. selectedKeys={[pathname + (location.hash || '')]}
  73. defaultOpenKeys={defaultOpenKeys}
  74. items={menuItems}
  75. onClick={(e) => navigate(e.key)}
  76. />
  77. </Sider>
  78. <Content style={{ padding: 16, overflow: 'auto' }}>
  79. <div style={{ display: 'flex', gap: 24 }}>
  80. <div style={{ flex: 1, minWidth: 0 }}>
  81. <MarkdownViewer content={md} onToc={setToc} />
  82. </div>
  83. <div style={{ width: 220 }}>
  84. <div style={{ position: 'sticky', top: 16 }}>
  85. <div style={{ marginBottom: 8, fontWeight: 600 }}>On this page</div>
  86. <div>
  87. {toc.map((t) => (
  88. <div key={t.id} style={{ marginLeft: (t.level - 1) * 12 }}>
  89. <a href={`#${t.id}`}>{t.text}</a>
  90. </div>
  91. ))}
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. </Content>
  97. </Layout>
  98. );
  99. };
  100. export default HelpLayout;