AnchorNav.tsx 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { Anchor } from "antd";
  2. import { useEffect, useState, useRef } from "react";
  3. import { convertToPlain } from "../../utils";
  4. const { Link } = Anchor;
  5. interface HeadingNode {
  6. key: string;
  7. label: string;
  8. level: number;
  9. children?: HeadingNode[];
  10. }
  11. interface Props {
  12. open?: boolean;
  13. containerSelector?: string; // 可指定扫描范围
  14. }
  15. /** 构建树结构 */
  16. function buildTree(list: HeadingNode[]): HeadingNode[] {
  17. const root: HeadingNode = { key: "root", label: "", level: 0, children: [] };
  18. const stack = [root];
  19. for (const node of list) {
  20. while (stack.length && stack[stack.length - 1].level >= node.level) {
  21. stack.pop();
  22. }
  23. const parent = stack[stack.length - 1];
  24. parent.children ??= [];
  25. parent.children.push(node);
  26. stack.push(node);
  27. }
  28. return root.children ?? [];
  29. }
  30. /** 递归渲染 */
  31. function renderLinks(nodes: HeadingNode[]): React.ReactNode {
  32. return nodes.map((node) => (
  33. <Link key={node.key} href={node.key} title={node.label}>
  34. {node.children && renderLinks(node.children)}
  35. </Link>
  36. ));
  37. }
  38. const AnchorNavWidget = ({ open = false, containerSelector }: Props) => {
  39. const [tree, setTree] = useState<HeadingNode[]>([]);
  40. const containerRef = useRef<HTMLElement | null>(null);
  41. /** 获取容器 */
  42. useEffect(() => {
  43. containerRef.current = containerSelector
  44. ? document.querySelector(containerSelector)
  45. : document.body;
  46. }, [containerSelector]);
  47. /** 扫描 heading */
  48. useEffect(() => {
  49. if (!open || !containerRef.current) return;
  50. const headings = Array.from(
  51. containerRef.current.querySelectorAll("h1,h2,h3,h4,h5,h6")
  52. );
  53. const list: HeadingNode[] = headings
  54. .map((el) => {
  55. if (!el.id) return null;
  56. return {
  57. key: `#${el.id}`,
  58. label: convertToPlain(el.innerHTML).slice(0, 30),
  59. level: Number(el.tagName[1]),
  60. };
  61. })
  62. .filter(Boolean) as HeadingNode[];
  63. setTree(buildTree(list));
  64. }, [open]);
  65. if (!open || tree.length === 0) return null;
  66. return (
  67. <div className="article_anchor paper_zh">
  68. <Anchor offsetTop={50}>{renderLinks(tree)}</Anchor>
  69. </div>
  70. );
  71. };
  72. export default AnchorNavWidget;