Files
rag_agent/rag-web-ui/frontend/src/components/layout/dashboard-layout.tsx

291 lines
9.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import Link from "next/link";
import {
Book,
ChevronRight,
FileText,
LogOut,
Menu,
MessageSquare,
Search,
User,
} from "lucide-react";
import Breadcrumb from "@/components/ui/breadcrumb";
import { cn } from "@/lib/utils";
type NavigationChild = {
name: string;
href: string;
};
type NavigationItem = {
name: string;
icon: React.ComponentType<{ className?: string }>;
href?: string;
children?: NavigationChild[];
defaultOpen?: boolean;
};
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({
knowledge: pathname.startsWith("/dashboard/knowledge"),
"doc-processing": pathname.startsWith("/dashboard/doc-processing"),
});
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
router.push("/login");
}
}, [router]);
const handleLogout = () => {
localStorage.removeItem("token");
router.push("/login");
};
useEffect(() => {
if (pathname.startsWith("/dashboard/knowledge")) {
setOpenGroups((prev) => ({ ...prev, knowledge: true }));
}
if (pathname.startsWith("/dashboard/doc-processing")) {
setOpenGroups((prev) => ({ ...prev, "doc-processing": true }));
}
}, [pathname]);
const toggleGroup = (key: string) => {
setOpenGroups((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const navigation: NavigationItem[] = [
{
name: "知识库",
icon: Book,
defaultOpen: true,
children: [
{ name: "文档知识库", href: "/dashboard/knowledge/document" },
{ name: "代码知识库", href: "/dashboard/knowledge/code" },
],
},
{ name: "对话", href: "/dashboard/chat", icon: MessageSquare },
{
name: "文档处理",
icon: FileText,
children: [
{ name: "需求提取", href: "/dashboard/doc-processing/extract" },
{
name: "测试用例生成",
href: "/dashboard/doc-processing/test-case-gen",
},
],
},
{
name: "一致性分析",
href: "/dashboard/consistency-analysis",
icon: Search,
},
{ name: "API 密钥", href: "/dashboard/api-keys", icon: User },
];
return (
<div className="min-h-screen bg-background">
<div className="fixed left-0 top-0 z-50 m-4 lg:hidden">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="rounded-md bg-primary p-2 text-primary-foreground"
>
<Menu className="h-6 w-6" />
</button>
</div>
<div
className={`fixed inset-y-0 left-0 z-40 w-64 transform border-r bg-card transition-transform duration-200 ease-in-out lg:translate-x-0 ${
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="flex h-full flex-col">
<div className="flex h-16 items-center border-b pl-8">
<Link
href="/dashboard"
className="flex items-center text-lg font-semibold transition-colors hover:text-primary"
>
<img src="/logo.svg" alt="标志" className="h-16 w-16 rounded-lg" />
RAG
</Link>
</div>
<nav className="flex-1 space-y-2 px-4 py-6">
{navigation.map((item) => {
if (item.children) {
const groupKey =
item.children[0]?.href.split("/")[2] === "knowledge"
? "knowledge"
: "doc-processing";
const isOpen = openGroups[groupKey] ?? item.defaultOpen ?? false;
const hasActiveChild = item.children.some((child) =>
pathname.startsWith(child.href)
);
return (
<div key={item.name} className="space-y-2">
<button
type="button"
onClick={() => toggleGroup(groupKey)}
className={cn(
"group flex w-full items-center rounded-lg px-4 py-3 text-sm font-medium transition-all duration-200",
hasActiveChild
? "bg-gradient-to-r from-primary/10 to-primary/5 text-primary shadow-sm"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground hover:shadow-sm"
)}
>
<item.icon
className={cn(
"mr-3 h-5 w-5 transition-transform duration-200",
hasActiveChild
? "scale-110 text-primary"
: "group-hover:scale-110"
)}
/>
<span className="font-medium">{item.name}</span>
<ChevronRight
className={cn(
"ml-auto h-4 w-4 transition-transform duration-200",
isOpen && "rotate-90"
)}
/>
</button>
{isOpen && (
<div className="ml-7 space-y-1 border-l pl-3">
{item.children.map((child) => {
const isChildActive = pathname.startsWith(child.href);
return (
<Link
key={child.name}
href={child.href}
className={cn(
"flex items-center rounded-md px-3 py-2 text-sm transition-colors",
isChildActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)}
>
<span
className={cn(
"mr-2 h-1.5 w-1.5 rounded-full",
isChildActive
? "bg-primary"
: "bg-muted-foreground/60"
)}
/>
{child.name}
</Link>
);
})}
</div>
)}
</div>
);
}
const isActive = pathname.startsWith(item.href || "");
return (
<Link
key={item.name}
href={item.href || "/dashboard"}
className={cn(
"group flex items-center rounded-lg px-4 py-3 text-sm font-medium transition-all duration-200",
isActive
? "bg-gradient-to-r from-primary/10 to-primary/5 text-primary shadow-sm"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground hover:shadow-sm"
)}
>
<item.icon
className={cn(
"mr-3 h-5 w-5 transition-transform duration-200",
isActive
? "scale-110 text-primary"
: "group-hover:scale-110"
)}
/>
<span className="font-medium">{item.name}</span>
{isActive && (
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-primary" />
)}
</Link>
);
})}
</nav>
<div className="space-y-4 border-t p-4">
<button
onClick={handleLogout}
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-destructive transition-colors duration-200 hover:bg-destructive/10"
>
<LogOut className="mr-3 h-4 w-4" />
退
</button>
</div>
</div>
</div>
<div className="lg:pl-64">
<main className="min-h-screen px-4 py-6 sm:px-6 lg:px-8">
<Breadcrumb />
{children}
</main>
</div>
</div>
);
}
export const dashboardConfig = {
mainNav: [],
sidebarNav: [
{
title: "文档知识库",
href: "/dashboard/knowledge/document",
icon: "database",
},
{
title: "代码知识库",
href: "/dashboard/knowledge/code",
icon: "braces",
},
{
title: "对话",
href: "/dashboard/chat",
icon: "messageSquare",
},
{
title: "文档处理",
href: "/dashboard/doc-processing/extract",
icon: "fileText",
},
{
title: "一致性分析",
href: "/dashboard/consistency-analysis",
icon: "search",
},
{
title: "API 密钥",
href: "/dashboard/api-keys",
icon: "key",
},
],
};