"use client"; import { useEffect, useState, useRef, useCallback } from "react"; import Link from "next/link"; import type { EbookData } from "@/app/lib/ebook"; import "github-markdown-css/github-markdown.css"; import styles from "./ebook.module.css"; function LazyChunk({ html, estimatedHeight, forceVisible, }: { html: string; estimatedHeight: number; forceVisible: boolean; }) { const [visible, setVisible] = useState(forceVisible); const ref = useRef(null); const show = forceVisible || visible; useEffect(() => { if (forceVisible || visible) return; const el = ref.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); observer.disconnect(); } }, { rootMargin: "1200px 0px" } ); observer.observe(el); return () => observer.disconnect(); }, [forceVisible, visible]); if (show) { return (
); } return (
); } export default function EbookClient({ data, title = "数字游民", backHref = "/", }: { data: EbookData; title?: string; backHref?: string; }) { const [showDrawer, setShowDrawer] = useState(false); const [readProgress, setReadProgress] = useState(0); const [showBackTop, setShowBackTop] = useState(false); const [forceShowAll, setForceShowAll] = useState(false); const contentRef = useRef(null); const { chunks, headers } = data; useEffect(() => { let ticking = false; const onScroll = () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const docHeight = document.documentElement.scrollHeight - window.innerHeight; setReadProgress( docHeight > 0 ? Math.min(100, (scrollTop / docHeight) * 100) : 0 ); setShowBackTop(scrollTop > 600); ticking = false; }); }; window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const handleJump = useCallback( (index: number) => { setForceShowAll(true); setShowDrawer(false); const run = () => { const root = contentRef.current; if (!root) return; const h1s = root.querySelectorAll("h1"); const h2s = root.querySelectorAll("h2"); const targets = h1s.length > 0 ? h1s : h2s; const el = targets[index] as HTMLElement; if (el) { try { el.scrollIntoView({ behavior: "smooth", block: "start" }); } catch { const top = el.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop) - 60; window.scrollTo({ top: Math.max(0, top), behavior: "smooth" }); } } }; run(); setTimeout(run, 100); setTimeout(run, 300); setTimeout(run, 600); }, [] ); return ( <>
{title}
{chunks.map((chunk, i) => ( ))}
setShowDrawer(false)} aria-hidden="true" /> {showBackTop && ( )} ); }