feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -16,7 +16,15 @@ export interface TocItem {
|
||||
* - IntersectionObserver 高亮当前小节;
|
||||
* - 当前项被滚动到目录可视区外时,列表自动最小化跟随(首屏不滚动、尊重 reduced-motion)。
|
||||
*/
|
||||
export default function PostToc({ items }: { items: TocItem[] }) {
|
||||
export default function PostToc({
|
||||
items,
|
||||
title = "目录",
|
||||
bare = false,
|
||||
}: {
|
||||
items: TocItem[];
|
||||
title?: string;
|
||||
bare?: boolean;
|
||||
}) {
|
||||
const [active, setActive] = useState<string>(items[0]?.id ?? "");
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
// 首次挂载时 active 取首项,不触发跟随滚动,避免进页跳动
|
||||
@@ -38,15 +46,25 @@ export default function PostToc({ items }: { items: TocItem[] }) {
|
||||
return () => obs.disconnect();
|
||||
}, [items]);
|
||||
|
||||
// active 变化时把当前目录项滚入列表可视区(block: nearest 仅在贴边时最小移动)
|
||||
// active 变化时把当前目录项滚入列表可视区(仅在列表容器内最小滚动)。
|
||||
// 不用 scrollIntoView:移动端目录下沉到文末(普通文档流、容器本身不可滚),
|
||||
// scrollIntoView(nearest) 会连带滚动窗口,把正在阅读的页面拽到底部目录,
|
||||
// 表现为"进页自动下滚 / 无法向上滑动"。
|
||||
useEffect(() => {
|
||||
if (skipFollow.current) {
|
||||
skipFollow.current = false;
|
||||
return;
|
||||
}
|
||||
const el = navRef.current?.querySelector<HTMLElement>(`[data-toc-id="${active}"]`);
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ block: "nearest", behavior: scrollBehavior() });
|
||||
const list = navRef.current?.querySelector<HTMLElement>(".j13-rail-scroll");
|
||||
const el = list?.querySelector<HTMLElement>(`[data-toc-id="${active}"]`);
|
||||
if (!list || !el) return;
|
||||
const listRect = list.getBoundingClientRect();
|
||||
const elRect = el.getBoundingClientRect();
|
||||
if (elRect.top < listRect.top) {
|
||||
list.scrollBy({ top: elRect.top - listRect.top, behavior: scrollBehavior() });
|
||||
} else if (elRect.bottom > listRect.bottom) {
|
||||
list.scrollBy({ top: elRect.bottom - listRect.bottom, behavior: scrollBehavior() });
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const jump = (id: string) => (e: React.MouseEvent) => {
|
||||
@@ -59,16 +77,29 @@ export default function PostToc({ items }: { items: TocItem[] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav ref={navRef} aria-label="文章目录" className="panel p-4 flex min-h-0 flex-col">
|
||||
<nav
|
||||
ref={navRef}
|
||||
aria-label="文章目录"
|
||||
className={`${bare ? "py-4" : "panel p-4"} flex min-h-0 flex-col`}
|
||||
style={
|
||||
bare
|
||||
? {
|
||||
background: "var(--panel)",
|
||||
borderTop: "1px solid var(--line)",
|
||||
borderBottom: "1px solid var(--line)",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<p
|
||||
className="text-[11.5px] font-bold tracking-[0.08em] uppercase mb-2.5 px-1 shrink-0"
|
||||
style={{ color: "var(--ink-3)" }}
|
||||
>
|
||||
目录
|
||||
{title}
|
||||
</p>
|
||||
<ul
|
||||
className="j13-rail-scroll space-y-0.5 pr-0.5"
|
||||
style={{ borderLeft: "1px solid var(--line)" }}
|
||||
className="j13-rail-scroll min-h-0 flex-1 space-y-0.5 pr-0.5"
|
||||
style={bare ? undefined : { borderLeft: "1px solid var(--line)" }}
|
||||
>
|
||||
{items.map((it) => {
|
||||
const isActive = it.id === active;
|
||||
|
||||
Reference in New Issue
Block a user