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

266 lines
8.5 KiB
TypeScript
Raw Normal View History

2026-04-13 11:34:23 +08:00
"use client";
import { useState, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import Link from "next/link";
import {
Book,
ChevronRight,
FileText,
MessageSquare,
LogOut,
Menu,
Search,
User,
} from "lucide-react";
import Breadcrumb from "@/components/ui/breadcrumb";
import { cn } from "@/lib/utils";
type NavigationItem = {
name: string;
icon: React.ComponentType<{ className?: string }>;
href?: string;
children?: Array<{
name: string;
href: string;
}>;
};
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isDocProcessingOpen, setIsDocProcessingOpen] = useState(
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/doc-processing")) {
setIsDocProcessingOpen(true);
}
}, [pathname]);
const navigation: NavigationItem[] = [
{ name: "知识库", href: "/dashboard/knowledge", icon: Book },
{ 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">
{/* Mobile menu button */}
<div className="lg:hidden fixed top-0 left-0 m-4 z-50">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="p-2 rounded-md bg-primary text-primary-foreground"
>
<Menu className="h-6 w-6" />
</button>
</div>
{/* Sidebar */}
<div
className={`fixed inset-y-0 left-0 z-40 w-64 transform bg-card border-r 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">
{/* Sidebar header */}
<div className="flex h-16 items-center border-b pl-8">
<Link
href="/dashboard"
className="flex items-center text-lg font-semibold hover:text-primary transition-colors"
>
<img
src="/logo.svg"
alt="标志"
className="w-16 h-16 rounded-lg"
/>
RAG
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 space-y-2 px-4 py-6">
{navigation.map((item) => {
if (item.children) {
const hasActiveChild = item.children.some((child) =>
pathname.startsWith(child.href)
);
return (
<div key={item.name} className="space-y-2">
<button
type="button"
onClick={() => setIsDocProcessingOpen((prev) => !prev)}
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
? "text-primary scale-110"
: "group-hover:scale-110"
)}
/>
<span className="font-medium">{item.name}</span>
<ChevronRight
className={cn(
"ml-auto h-4 w-4 transition-transform duration-200",
isDocProcessingOpen ? "rotate-90" : ""
)}
/>
</button>
{isDocProcessingOpen && (
<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={`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={`mr-3 h-5 w-5 transition-transform duration-200 ${
isActive
? "text-primary scale-110"
: "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>
{/* User profile and logout */}
<div className="border-t p-4 space-y-4">
<button
onClick={handleLogout}
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors duration-200"
>
<LogOut className="mr-3 h-4 w-4" />
退
</button>
</div>
</div>
</div>
{/* Main content */}
<div className="lg:pl-64">
<main className="min-h-screen py-6 px-4 sm:px-6 lg:px-8">
<Breadcrumb />
{children}
</main>
</div>
</div>
);
}
export const dashboardConfig = {
mainNav: [],
sidebarNav: [
{
title: "知识库",
href: "/dashboard/knowledge",
icon: "database",
},
{
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",
},
],
};