266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
|
|
"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",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|