"use client"; import { useEffect, useState, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; export interface TOCItem { id: string; text: string; level: number; element: HTMLElement; } interface TableOfContentsProps { className?: string; } export default function TableOfContents({ className }: TableOfContentsProps) { const [toc, setToc] = useState([]); const [activeId, setActiveId] = useState(''); const observerRef = useRef(null); // 提取页面中的标题 const extractHeadings = useCallback(() => { const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); const tocItems: TOCItem[] = []; headings.forEach((heading, index) => { const element = heading as HTMLElement; const text = element.textContent || ''; const level = parseInt(element.tagName.charAt(1)); // 为标题添加 ID(如果没有的话) let id = element.id; if (!id) { id = text .toLowerCase() .replace(/[^\w\s-]/g, '') // 移除特殊字符 .replace(/[\s_-]+/g, '-') // 替换空格和下划线为连字符 .replace(/^-+|-+$/g, ''); // 移除首尾的连字符 // 确保 ID 唯一 let uniqueId = id; let counter = 1; while (document.getElementById(uniqueId)) { uniqueId = `${id}-${counter}`; counter++; } element.id = uniqueId; id = uniqueId; } tocItems.push({ id, text, level, element }); }); setToc(tocItems); return tocItems; }, []); // 设置 Intersection Observer 来追踪当前可见的标题 useEffect(() => { const headings = extractHeadings(); if (headings.length === 0) return; observerRef.current = new IntersectionObserver( (entries) => { // 找到所有可见的标题 const visibleHeadings = entries .filter(entry => entry.isIntersecting) .map(entry => entry.target.id); if (visibleHeadings.length > 0) { // 设置第一个可见标题为活跃状态 setActiveId(visibleHeadings[0]); } }, { rootMargin: '-80px 0px -80%', threshold: 0 } ); // 观察所有标题 headings.forEach((item) => { observerRef.current?.observe(item.element); }); return () => { observerRef.current?.disconnect(); }; }, [extractHeadings]); // 点击目录项滚动到对应位置 const scrollToHeading = useCallback((id: string) => { const element = document.getElementById(id); if (element) { const offsetTop = element.getBoundingClientRect().top + window.pageYOffset - 100; window.scrollTo({ top: offsetTop, behavior: 'smooth' }); // 更新活跃状态 setActiveId(id); } else { // console.log('Element not found for id:', id); // 调试日志 } }, []); if (toc.length === 0) { return null; } return ( ); }