Files
FlowStack/frontend/src/pages/Docs.tsx
T

108 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<DocKind, { title: string; subtitle: string }> = {
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<DocKind>('guide');
const { data, isLoading, refetch, isRefetching } = useQuery({
queryKey: ['docs'],
queryFn: () => api<any>('/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 (
<div className="h-full flex flex-col" style={{ background: 'var(--canvas-bg)' }}>
{/* Header */}
<div className="px-5 py-3 border-b flex items-center justify-between flex-shrink-0" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<div className="flex items-center gap-3">
<BookOpen className="w-4.5 h-4.5" style={{ color: 'var(--accent-blue)' }} />
<h1 className="text-[15px] font-semibold" style={{ color: 'var(--text-primary)' }}>文档中心</h1>
{/* 指南/接口 切换 */}
<div className="flex items-center rounded-md border overflow-hidden ml-2" style={{ borderColor: 'var(--border-color-light)' }}>
{(['guide', 'api'] as DocKind[]).map((k) => (
<button key={k} onClick={() => setKind(k)} className="px-3 py-[3px] text-[11px] font-medium"
style={{ background: kind === k ? 'var(--accent-blue)' : 'transparent', color: kind === k ? '#fff' : 'var(--text-secondary)' }}>
{meta[k].title}
</button>
))}
</div>
</div>
<button onClick={() => refetch()} className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md border" style={{ borderColor: 'var(--border-color-light)', color: 'var(--text-secondary)' }}>
<RefreshCw className={`w-3.5 h-3.5 ${isRefetching ? 'animate-spin' : ''}`} />刷新
</button>
</div>
<div className="flex-1 flex overflow-hidden">
{/* 左侧目录 */}
<div className="w-56 border-r overflow-y-auto custom-scrollbar flex-shrink-0 py-3" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<div className="px-4 pb-2 text-[11px] font-semibold" style={{ color: 'var(--text-tertiary)' }}>目录</div>
{isLoading ? (
<div className="px-4 py-4 text-[12px]" style={{ color: 'var(--text-tertiary)' }}>加载中…</div>
) : (
toc.map((t) => (
<a key={t.id + t.text} href={`#${t.id}`}
className={`block truncate hover:bg-black/[0.03] ${t.level === 3 ? 'pl-7' : 'pl-4'} pr-3 py-[5px] text-[12px]`}
style={{ color: t.level === 3 ? 'var(--text-tertiary)' : 'var(--text-secondary)' }}>
{t.text}
</a>
))
)}
</div>
{/* 右侧内容 */}
<div className="flex-1 overflow-y-auto custom-scrollbar">
{isLoading ? (
<div className="flex items-center justify-center gap-2 py-20 text-[13px]" style={{ color: 'var(--text-tertiary)' }}>
<Loader2 className="w-4 h-4 animate-spin" />加载文档中…
</div>
) : (
<div className="max-w-3xl mx-auto px-8 py-6">
<div className="mb-4 pb-4 border-b" style={{ borderColor: 'var(--border-color)' }}>
<h2 className="text-[17px] font-bold" style={{ color: 'var(--text-primary)' }}>{meta[kind].title}</h2>
<p className="text-[12px] mt-1" style={{ color: 'var(--text-tertiary)' }}>{meta[kind].subtitle}</p>
</div>
<div className="md-body" dangerouslySetInnerHTML={{ __html: html }} />
</div>
)}
</div>
</div>
</div>
);
}