website-01/components/front/ManualLayout.tsx

131 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-06-22 14:43:46 +08:00
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>
);
}