"use client"; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { cn } from "@/lib/utils"; import type { TocItem } from "@/lib/toc"; /** * 文章目录(右侧栏)。 * * 用 IntersectionObserver 做滚动高亮——比监听 scroll 事件性能更好, * 且不需要手动计算元素偏移。根边距设置为顶部 96px(避开粘性导航) * 到底部 -70%,让"当前阅读位置"落在视口上部三分之一处。 */ export function TableOfContents({ items }: { items: TocItem[] }) { const t = useTranslations("Post"); const [activeId, setActiveId] = useState(items[0]?.id ?? ""); useEffect(() => { if (items.length === 0) { return; } const headings = items .map((item) => document.getElementById(item.id)) .filter((element): element is HTMLElement => element !== null); if (headings.length === 0) { return; } const observer = new IntersectionObserver( (entries) => { // 取当前可见且位置最靠上的标题作为激活项 const visible = entries .filter((entry) => entry.isIntersecting) .sort( (a, b) => a.boundingClientRect.top - b.boundingClientRect.top, ); if (visible.length > 0) { setActiveId(visible[0].target.id); } }, { rootMargin: "-96px 0px -70% 0px", threshold: 0, }, ); for (const heading of headings) { observer.observe(heading); } return () => observer.disconnect(); }, [items]); if (items.length === 0) { return null; } return ( ); }