"use client" import * as React from "react" import Link from "next/link" import {ArrowLeft, Calendar, Check, Copy, FileText} from "lucide-react" import {Button} from "@/components/ui/button" import {cn} from "@/lib/utils" import {motion, useScroll, useSpring} from "motion/react" import type {PolicySection} from "./types" interface LegalPageLayoutProps { title: string lastUpdated: string sections: PolicySection[] description?: React.ReactNode } export function LegalPageLayout({ title, lastUpdated, sections, description, }: LegalPageLayoutProps) { const [activeSection, setActiveSection] = React.useState(sections[0]?.value || "") const [hasCopied, setHasCopied] = React.useState(false) const { scrollYProgress } = useScroll() const contentRef = React.useRef(null) const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001 }) const allSectionIds = React.useMemo(() => { return sections.flatMap(section => [ section.value, ...(section.children?.map(child => child.value) || []) ]) }, [sections]) React.useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveSection(entry.target.id) } }) }, { rootMargin: "-20% 0px -35% 0px" } ) allSectionIds.forEach((id) => { const element = document.getElementById(id) if (element) observer.observe(element) }) return () => observer.disconnect() }, [sections, allSectionIds]) const convertInlineNodes = (nodes: ChildNode[]): string => { return nodes.map((node) => { if (node.nodeType === Node.TEXT_NODE) { return node.textContent || "" } if (node.nodeType !== Node.ELEMENT_NODE) { return "" } const element = node as HTMLElement const tagName = element.tagName.toLowerCase() if (tagName === "code") { return `\`${element.textContent?.trim() || ""}\`` } if (tagName === "strong" || tagName === "b") { return `**${convertInlineNodes(Array.from(element.childNodes))}**` } if (tagName === "em" || tagName === "i") { return `*${convertInlineNodes(Array.from(element.childNodes))}*` } if (tagName === "a") { const text = convertInlineNodes(Array.from(element.childNodes)) || element.textContent || "" const href = element.getAttribute("href") || "" return href ? `[${text}](${href})` : text } if (tagName === "br") { return "\n" } return convertInlineNodes(Array.from(element.childNodes)) }).join("") } const convertList = ( list: HTMLOListElement | HTMLUListElement, indentLevel: number ): string => { const isOrdered = list.tagName.toLowerCase() === "ol" const items = Array.from(list.children).filter((child) => child.tagName.toLowerCase() === "li") return items.map((item, index) => { const prefix = isOrdered ? `${index + 1}. ` : "- " const indent = " ".repeat(indentLevel) const parts: string[] = [] const itemElement = item as HTMLElement const childNodes = Array.from(itemElement.childNodes) const nestedLists: HTMLElement[] = [] childNodes.forEach((child) => { if (child.nodeType === Node.ELEMENT_NODE) { const childElement = child as HTMLElement if (childElement.tagName.toLowerCase() === "ul" || childElement.tagName.toLowerCase() === "ol") { nestedLists.push(childElement) return } } parts.push(convertInlineNodes([child]).trim()) }) const baseLine = `${indent}${prefix}${parts.join("").trim()}` const nestedMarkdown = nestedLists.map((nested) => convertList(nested as HTMLOListElement | HTMLUListElement, indentLevel + 1)) return [baseLine, ...nestedMarkdown].filter(Boolean).join("\n") }).join("\n") } const convertTable = (table: HTMLTableElement): string => { const rows = Array.from(table.querySelectorAll("tr")).map((row) => Array.from(row.querySelectorAll("th,td")).map((cell) => (cell.textContent || "").trim()) ) if (!rows.length) return "" const header = rows[0] const body = rows.slice(1) const headerLine = `| ${header.join(" | ")} |` const separatorLine = `| ${header.map(() => "---").join(" | ")} |` const bodyLines = body.map((row) => `| ${row.join(" | ")} |`) return [headerLine, separatorLine, ...bodyLines].join("\n") } const convertBlockNode = (node: HTMLElement, indentLevel: number): string => { const tagName = node.tagName.toLowerCase() if (tagName === "h3") return `### ${node.textContent?.trim() || ""}` if (tagName === "h4") return `#### ${node.textContent?.trim() || ""}` if (tagName === "p") return convertInlineNodes(Array.from(node.childNodes)).trim() if (tagName === "ul" || tagName === "ol") return convertList(node as HTMLOListElement | HTMLUListElement, indentLevel) if (tagName === "pre") { const code = node.textContent || "" return `\`\`\`\n${code.trim()}\n\`\`\`` } if (tagName === "table") return convertTable(node as HTMLTableElement) if (tagName === "div") { const hasTable = node.querySelector("table") const hasList = node.querySelector("ul,ol") const hasPre = node.querySelector("pre") if (hasTable || hasList || hasPre) { return Array.from(node.children) .map((child) => convertBlockNode(child as HTMLElement, indentLevel)) .filter(Boolean) .join("\n\n") } const text = convertInlineNodes(Array.from(node.childNodes)).trim() return text } return convertInlineNodes(Array.from(node.childNodes)).trim() } const buildMarkdown = (): string => { const contentRoot = contentRef.current if (!contentRoot) return "" const lines: string[] = [] lines.push(`# ${title}`) lines.push("") lines.push(`最后更新:${lastUpdated}`) if (description) { const descriptionElement = contentRoot.querySelector("[data-docs-description]") if (descriptionElement) { const descriptionText = convertInlineNodes(Array.from(descriptionElement.childNodes)).trim() if (descriptionText) { lines.push("") lines.push(descriptionText) } } } lines.push("") const sectionNodes = Array.from(contentRoot.querySelectorAll("[data-docs-section]")) as HTMLElement[] sectionNodes.forEach((section) => { const heading = section.querySelector("h2") if (heading?.textContent) { lines.push(`## ${heading.textContent.trim()}`) lines.push("") } const prose = section.querySelector("[data-docs-content]") if (prose) { const blocks = Array.from(prose.children) .map((child) => convertBlockNode(child as HTMLElement, 0)) .filter(Boolean) if (blocks.length) { lines.push(blocks.join("\n\n")) lines.push("") } } }) return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n" } const handleCopyMarkdown = async () => { const markdown = buildMarkdown() if (!markdown) return try { await navigator.clipboard.writeText(markdown) setHasCopied(true) setTimeout(() => setHasCopied(false), 2000) } catch { const textArea = document.createElement("textarea") textArea.value = markdown textArea.style.position = "fixed" textArea.style.opacity = "0" document.body.appendChild(textArea) textArea.focus() textArea.select() document.execCommand("copy") document.body.removeChild(textArea) setHasCopied(true) setTimeout(() => setHasCopied(false), 2000) } } const scrollToSection = (e: React.MouseEvent, id: string) => { e.preventDefault() const element = document.getElementById(id) if (element) { const offset = 80 const bodyRect = document.body.getBoundingClientRect().top const elementRect = element.getBoundingClientRect().top const elementPosition = elementRect - bodyRect const offsetPosition = elementPosition - offset window.scrollTo({ top: offsetPosition, behavior: "smooth" }) } } return (

{title}

{sections.length} 个内容 最后更新:{lastUpdated}
{description && (
{description}
)}
{sections.map((section, index) => (
{index + 1}

{section.title.replace(/^\d+\.\s*/, '')}

{section.content}
))}

对这些内容有疑问?

) }