196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { useSession, getSession } from "next-auth/react";
|
|
import { SideNav, NavGroup, NavItem } from "@/components/layout/side-nav";
|
|
import { Header } from "@/components/layout/header";
|
|
import { NotificationContainer } from "@/components/ui/notification-container";
|
|
import { useUserStore } from "@/lib/stores/user-store";
|
|
import {
|
|
LayoutDashboard,
|
|
Sparkles,
|
|
BookOpen,
|
|
BarChart3,
|
|
Swords,
|
|
Share2,
|
|
Heart,
|
|
ScanSearch,
|
|
Settings,
|
|
} from "lucide-react";
|
|
|
|
// ─── Nav Config ───────────────────────────────────────────────────────────────
|
|
|
|
const NAV_GROUPS: NavGroup[] = [
|
|
{
|
|
id: "menu",
|
|
title: "菜单",
|
|
items: [
|
|
{
|
|
id: "dashboard",
|
|
label: "仪表盘",
|
|
href: "/dashboard",
|
|
icon: <LayoutDashboard className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "content",
|
|
label: "内容管理",
|
|
href: "/dashboard/content",
|
|
icon: <Sparkles className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "knowledge",
|
|
label: "知识库",
|
|
href: "/dashboard/knowledge",
|
|
icon: <BookOpen className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "analytics",
|
|
label: "品牌监测",
|
|
href: "/dashboard/monitoring",
|
|
icon: <BarChart3 className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "competitors",
|
|
label: "竞品分析",
|
|
href: "/dashboard/competitors",
|
|
icon: <Swords className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "health-score",
|
|
label: "健康评分",
|
|
href: "/dashboard/health-score",
|
|
icon: <Heart className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "distribution",
|
|
label: "内容分发",
|
|
href: "/dashboard/distribution",
|
|
icon: <Share2 className="h-5 w-5" />,
|
|
},
|
|
{
|
|
id: "detection",
|
|
label: "检测任务",
|
|
href: "/dashboard/detection",
|
|
icon: <ScanSearch className="h-5 w-5" />,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "settings",
|
|
title: "设置",
|
|
items: [
|
|
{
|
|
id: "settings",
|
|
label: "设置",
|
|
href: "/dashboard/settings",
|
|
icon: <Settings className="h-5 w-5" />,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// Flatten all items for activeId lookup
|
|
const ALL_NAV_ITEMS: NavItem[] = NAV_GROUPS.flatMap((g) => g.items);
|
|
|
|
// ─── Layout ───────────────────────────────────────────────────────────────────
|
|
|
|
export default function DashboardLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const { status, update } = useSession();
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const [verifying, setVerifying] = React.useState(false);
|
|
|
|
// 同步 session 到 user store
|
|
const syncFromSession = useUserStore((s) => s.syncFromSession);
|
|
|
|
React.useEffect(() => {
|
|
if (status === "authenticated") {
|
|
getSession().then((session) => {
|
|
syncFromSession(session);
|
|
});
|
|
} else if (status === "unauthenticated") {
|
|
syncFromSession(null);
|
|
}
|
|
}, [status, syncFromSession]);
|
|
|
|
// 当 useSession 返回 unauthenticated 时,用 getSession 双重确认,
|
|
// 避免 SessionProvider 缓存未更新导致的误判重定向
|
|
React.useEffect(() => {
|
|
if (status === "unauthenticated") {
|
|
setVerifying(true);
|
|
getSession().then((session) => {
|
|
setVerifying(false);
|
|
if (session) {
|
|
// SessionProvider 缓存未更新,强制刷新
|
|
update();
|
|
} else {
|
|
router.replace("/login");
|
|
}
|
|
});
|
|
}
|
|
}, [status, router, update]);
|
|
|
|
// Compute activeId: match longest href prefix
|
|
const activeId = React.useMemo(() => {
|
|
const sorted = [...ALL_NAV_ITEMS]
|
|
.filter((item) => item.href)
|
|
.sort((a, b) => (b.href?.length ?? 0) - (a.href?.length ?? 0));
|
|
return sorted.find((item) => item.href && pathname.startsWith(item.href))?.id ?? "dashboard";
|
|
}, [pathname]);
|
|
|
|
// Handle nav click: navigate to href
|
|
const handleNavClick = React.useCallback(
|
|
(item: NavItem) => {
|
|
if (item.href) {
|
|
router.push(item.href);
|
|
}
|
|
},
|
|
[router]
|
|
);
|
|
|
|
if (status === "loading" || verifying) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 border-t-emerald-500" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === "unauthenticated") {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-gray-50">
|
|
{/* Fixed SideNav - left side */}
|
|
<aside className="fixed inset-y-0 left-0 z-40 flex flex-col w-60">
|
|
<SideNav
|
|
groups={NAV_GROUPS}
|
|
activeId={activeId}
|
|
onNavClick={handleNavClick}
|
|
className="h-full"
|
|
/>
|
|
</aside>
|
|
|
|
{/* Main content area - offset by sidebar width */}
|
|
<div className="flex flex-1 flex-col ml-60 min-h-screen">
|
|
{/* Fixed Header at top */}
|
|
<Header />
|
|
|
|
{/* Global Toast/Notification */}
|
|
<NotificationContainer />
|
|
|
|
{/* Scrollable content area */}
|
|
<main className="flex-1 bg-gray-50 p-6">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|