'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}
))}

对这些内容有疑问?

); }