website-01/components/front/ManualLayout.tsx
2026-06-22 14:43:46 +08:00

131 lines
4.7 KiB
TypeScript
Raw Permalink 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 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>
);
}