import { useMemo, useState } from 'react'; import { BookOpen, RefreshCw, Loader2 } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { Marked } from 'marked'; import { gfmHeadingId } from 'marked-gfm-heading-id'; import { api } from '@/lib/api'; type DocKind = 'guide' | 'api'; const meta: Record = { guide: { title: '使用指南', subtitle: '每个功能一句话讲清:是什么 → 怎么用 → 实用技巧' }, api: { title: '接口文档', subtitle: '与当前后端代码对齐;调用需登录 Token,敏感字段只写不回显' }, }; // 从 Markdown 提取 ## 级标题做目录(marked-gfm-heading-id 生成锚点 id) function extractToc(md: string): { text: string; id: string; level: number }[] { const out: { text: string; id: string; level: number }[] = []; for (const line of md.split('\n')) { const m = /^(#{2,3})\s+(.+)$/.exec(line.trim()); if (!m) continue; const text = m[2].trim(); out.push({ text, id: headingId(text), level: m[1].length }); } return out; } // 与 marked-gfm-heading-id 的默认 slug 规则保持一致(小写、去标点、空格转 -) function headingId(text: string): string { return text.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, '').trim().replace(/\s+/g, '-'); } export default function Docs() { const [kind, setKind] = useState('guide'); const { data, isLoading, refetch, isRefetching } = useQuery({ queryKey: ['docs'], queryFn: () => api('/api/docs'), }); const raw = (kind === 'guide' ? data?.guide : data?.api) || ''; const html = useMemo(() => { const marked = new Marked(gfmHeadingId()); return marked.parse(raw, { async: false }) as string; }, [raw]); const toc = useMemo(() => extractToc(raw), [raw]); return (
{/* Header */}

文档

{/* 指南/接口 切换 */}
{(['guide', 'api'] as DocKind[]).map((k) => ( ))}
{/* 左侧目录 */}
目录
{isLoading ? (
加载中…
) : ( toc.map((t) => ( {t.text} )) )}
{/* 右侧内容 */}
{isLoading ? (
加载文档中…
) : (

{meta[kind].title}

{meta[kind].subtitle}

)}
); }