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