131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
|
|
import Link from 'next/link';
|
|||
|
|
import { ChevronRight } from 'lucide-react';
|
|||
|
|
import { ManualTreeNav } from './ManualTreeNav';
|
|||
|
|
import { ContentView } from './ContentView';
|
|||
|
|
import type { Manual, ManualTreeNode } from '@/lib/types';
|
|||
|
|
|
|||
|
|
interface ManualLayoutProps {
|
|||
|
|
tree: ManualTreeNode[];
|
|||
|
|
/** 当前文档(含正文);为 null 表示无选中 */
|
|||
|
|
doc: Manual | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 扁平化树为「仅文档」节点,便于查找前一篇/后一篇 */
|
|||
|
|
function flattenDocs(nodes: ManualTreeNode[]): { id: number; title: string }[] {
|
|||
|
|
const out: { id: number; title: string }[] = [];
|
|||
|
|
const walk = (list: ManualTreeNode[]) => {
|
|||
|
|
for (const n of list) {
|
|||
|
|
if (n.type === 1) out.push({ id: n.id, title: n.title });
|
|||
|
|
if (n.children.length > 0) walk(n.children);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
walk(nodes);
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 构建从根到当前节点的标题面包屑路径 */
|
|||
|
|
function findPath(
|
|||
|
|
nodes: ManualTreeNode[],
|
|||
|
|
id: number,
|
|||
|
|
prefix: string[] = [],
|
|||
|
|
): string[] | null {
|
|||
|
|
for (const n of nodes) {
|
|||
|
|
const path = [...prefix, n.title];
|
|||
|
|
if (n.id === id) return path;
|
|||
|
|
if (n.children.length > 0) {
|
|||
|
|
const sub = findPath(n.children, id, path);
|
|||
|
|
if (sub) return sub;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 使用手册左右布局外壳 */
|
|||
|
|
export function ManualLayout({ tree, doc }: ManualLayoutProps) {
|
|||
|
|
const docs = flattenDocs(tree);
|
|||
|
|
const currentIndex = doc ? docs.findIndex((d) => d.id === doc.id) : -1;
|
|||
|
|
const prev = currentIndex > 0 ? docs[currentIndex - 1] : null;
|
|||
|
|
const next =
|
|||
|
|
currentIndex >= 0 && currentIndex < docs.length - 1
|
|||
|
|
? docs[currentIndex + 1]
|
|||
|
|
: null;
|
|||
|
|
const breadcrumb = doc ? findPath(tree, doc.id) : null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="container-page py-8">
|
|||
|
|
<div className="grid gap-6 md:grid-cols-[260px_minmax(0,1fr)] lg:gap-10">
|
|||
|
|
{/* 左:树形菜单 */}
|
|||
|
|
<aside className="md:sticky md:top-20 md:max-h-[calc(100vh-6rem)] md:overflow-y-auto">
|
|||
|
|
<div className="mb-3 px-2 text-sm font-semibold text-slate-900">
|
|||
|
|
使用手册
|
|||
|
|
</div>
|
|||
|
|
<ManualTreeNav nodes={tree} />
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
{/* 右:正文 */}
|
|||
|
|
<article className="min-w-0">
|
|||
|
|
{doc ? (
|
|||
|
|
<>
|
|||
|
|
{breadcrumb && breadcrumb.length > 1 && (
|
|||
|
|
<nav className="mb-3 flex flex-wrap items-center gap-1 text-xs text-slate-500">
|
|||
|
|
{breadcrumb.slice(0, -1).map((seg, i) => (
|
|||
|
|
<span key={i} className="flex items-center gap-1">
|
|||
|
|
<span>{seg}</span>
|
|||
|
|
<ChevronRight className="h-3 w-3" />
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</nav>
|
|||
|
|
)}
|
|||
|
|
<h1 className="border-b border-slate-200 pb-3 text-2xl font-bold text-slate-900">
|
|||
|
|
{doc.title}
|
|||
|
|
</h1>
|
|||
|
|
{doc.content ? (
|
|||
|
|
<ContentView
|
|||
|
|
content={doc.content}
|
|||
|
|
format={doc.contentFormat}
|
|||
|
|
className="mt-6"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<p className="mt-6 text-sm text-slate-400">本文档暂无内容</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 上一篇 / 下一篇 */}
|
|||
|
|
{(prev || next) && (
|
|||
|
|
<div className="mt-10 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4 text-sm">
|
|||
|
|
{prev ? (
|
|||
|
|
<Link
|
|||
|
|
href={`/manual/${prev.id}`}
|
|||
|
|
className="flex items-center gap-1 text-slate-600 hover:text-brand-600"
|
|||
|
|
>
|
|||
|
|
<span className="text-slate-400">上一篇:</span>
|
|||
|
|
<span className="truncate">{prev.title}</span>
|
|||
|
|
</Link>
|
|||
|
|
) : (
|
|||
|
|
<span />
|
|||
|
|
)}
|
|||
|
|
{next && (
|
|||
|
|
<Link
|
|||
|
|
href={`/manual/${next.id}`}
|
|||
|
|
className="flex items-center gap-1 text-slate-600 hover:text-brand-600"
|
|||
|
|
>
|
|||
|
|
<span className="text-slate-400">下一篇:</span>
|
|||
|
|
<span className="truncate">{next.title}</span>
|
|||
|
|
</Link>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<div className="rounded-lg border border-dashed border-slate-200 bg-slate-50/50 p-12 text-center">
|
|||
|
|
<p className="text-slate-500">暂无可显示的文档,请从左侧菜单选择。</p>
|
|||
|
|
<p className="mt-1 text-xs text-slate-400">
|
|||
|
|
若有权限,可在后台「使用手册」中新增文档。
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</article>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|