|
|
@@ -0,0 +1,190 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import {
|
|
|
+ HomeOutlined,
|
|
|
+ LogoutOutlined,
|
|
|
+ TeamOutlined,
|
|
|
+ UserOutlined,
|
|
|
+} from "@ant-design/icons";
|
|
|
+import type { MenuProps } from "antd";
|
|
|
+import {
|
|
|
+ Avatar,
|
|
|
+ Breadcrumb,
|
|
|
+ Dropdown,
|
|
|
+ Layout,
|
|
|
+ Menu,
|
|
|
+ Space,
|
|
|
+ Typography,
|
|
|
+ theme,
|
|
|
+} from "antd";
|
|
|
+import { usePathname, useRouter } from "next/navigation";
|
|
|
+import type React from "react";
|
|
|
+import { useEffect, useMemo, useState } from "react";
|
|
|
+import { useAuth } from "@/contexts/AuthContext";
|
|
|
+
|
|
|
+const { Text } = Typography;
|
|
|
+
|
|
|
+const { Header, Content, Footer, Sider } = Layout;
|
|
|
+
|
|
|
+type MenuItem = Required<MenuProps>["items"][number];
|
|
|
+
|
|
|
+function getItem(
|
|
|
+ label: React.ReactNode,
|
|
|
+ key: React.Key,
|
|
|
+ icon?: React.ReactNode,
|
|
|
+ children?: MenuItem[],
|
|
|
+): MenuItem {
|
|
|
+ return {
|
|
|
+ key,
|
|
|
+ icon,
|
|
|
+ children,
|
|
|
+ label,
|
|
|
+ } as MenuItem;
|
|
|
+}
|
|
|
+
|
|
|
+const items: MenuItem[] = [
|
|
|
+ getItem("首页", "home", <HomeOutlined />),
|
|
|
+ getItem("陪玩管理", "user", <UserOutlined />, [
|
|
|
+ getItem("陪玩列表", "user-list"),
|
|
|
+ ]),
|
|
|
+ getItem("后台用户管理", "user-admin", <TeamOutlined />, [
|
|
|
+ getItem("用户列表", "user-admin-list"),
|
|
|
+ ]),
|
|
|
+];
|
|
|
+
|
|
|
+// Path mapping
|
|
|
+const pathToKeyMap: Record<string, string> = {
|
|
|
+ "/home": "home",
|
|
|
+ "/user/list": "user-list",
|
|
|
+ "/user-admin/list": "user-admin-list",
|
|
|
+};
|
|
|
+
|
|
|
+const keyToPathMap: Record<string, string> = {
|
|
|
+ home: "/home",
|
|
|
+ "user-list": "/user/list",
|
|
|
+ "user-admin-list": "/user-admin/list",
|
|
|
+};
|
|
|
+
|
|
|
+const keyToOpenKeysMap: Record<string, string[]> = {
|
|
|
+ "user-list": ["user"],
|
|
|
+ "user-admin-list": ["user-admin"],
|
|
|
+};
|
|
|
+
|
|
|
+const pathToBreadcrumb: Record<string, { title: string }[]> = {
|
|
|
+ "/home": [{ title: "首页" }],
|
|
|
+ "/user/list": [{ title: "陪玩管理" }, { title: "陪玩列表" }],
|
|
|
+ "/user-admin/list": [{ title: "后台用户管理" }, { title: "用户列表" }],
|
|
|
+};
|
|
|
+
|
|
|
+export default function DashboardLayout({
|
|
|
+ children,
|
|
|
+}: {
|
|
|
+ children: React.ReactNode;
|
|
|
+}) {
|
|
|
+ const [collapsed, setCollapsed] = useState(false);
|
|
|
+ const router = useRouter();
|
|
|
+ const pathname = usePathname();
|
|
|
+ const { user, logout, isAuthenticated, isLoading } = useAuth();
|
|
|
+ const {
|
|
|
+ token: { colorBgContainer, borderRadiusLG },
|
|
|
+ } = theme.useToken();
|
|
|
+
|
|
|
+ // Redirect to login if not authenticated
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isLoading && !isAuthenticated) {
|
|
|
+ router.push("/login");
|
|
|
+ }
|
|
|
+ }, [isAuthenticated, isLoading, router]);
|
|
|
+
|
|
|
+ // Get current selected key and open keys based on pathname
|
|
|
+ const selectedKey = useMemo(() => {
|
|
|
+ return pathToKeyMap[pathname] || "home";
|
|
|
+ }, [pathname]);
|
|
|
+
|
|
|
+ const defaultOpenKeys = useMemo(() => {
|
|
|
+ return keyToOpenKeysMap[selectedKey] || [];
|
|
|
+ }, [selectedKey]);
|
|
|
+
|
|
|
+ const breadcrumbItems = useMemo(() => {
|
|
|
+ return pathToBreadcrumb[pathname] || [{ title: "首页" }];
|
|
|
+ }, [pathname]);
|
|
|
+
|
|
|
+ const handleMenuClick = (e: { key: string }) => {
|
|
|
+ const path = keyToPathMap[e.key];
|
|
|
+ if (path) {
|
|
|
+ router.push(path);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // User dropdown menu
|
|
|
+ const userMenuItems: MenuProps["items"] = [
|
|
|
+ {
|
|
|
+ key: "logout",
|
|
|
+ icon: <LogoutOutlined />,
|
|
|
+ label: "退出登录",
|
|
|
+ onClick: logout,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // Show loading or redirect
|
|
|
+ if (isLoading || !isAuthenticated) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Layout style={{ minHeight: "100vh" }}>
|
|
|
+ <Sider
|
|
|
+ collapsible
|
|
|
+ collapsed={collapsed}
|
|
|
+ onCollapse={(value) => setCollapsed(value)}
|
|
|
+ >
|
|
|
+ <div className="demo-logo-vertical" />
|
|
|
+ <Menu
|
|
|
+ theme="dark"
|
|
|
+ selectedKeys={[selectedKey]}
|
|
|
+ defaultOpenKeys={defaultOpenKeys}
|
|
|
+ mode="inline"
|
|
|
+ items={items}
|
|
|
+ onClick={handleMenuClick}
|
|
|
+ />
|
|
|
+ </Sider>
|
|
|
+ <Layout>
|
|
|
+ <Header
|
|
|
+ style={{
|
|
|
+ padding: "0 24px",
|
|
|
+ background: colorBgContainer,
|
|
|
+ display: "flex",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "flex-end",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
|
|
+ <Space style={{ cursor: "pointer" }}>
|
|
|
+ <Avatar
|
|
|
+ style={{ backgroundColor: "#667eea" }}
|
|
|
+ icon={<UserOutlined />}
|
|
|
+ />
|
|
|
+ <Text strong>{user?.username}</Text>
|
|
|
+ </Space>
|
|
|
+ </Dropdown>
|
|
|
+ </Header>
|
|
|
+ <Content style={{ margin: "0 16px" }}>
|
|
|
+ <Breadcrumb style={{ margin: "16px 0" }} items={breadcrumbItems} />
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ padding: 24,
|
|
|
+ minHeight: 360,
|
|
|
+ background: colorBgContainer,
|
|
|
+ borderRadius: borderRadiusLG,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
+ </Content>
|
|
|
+ <Footer style={{ textAlign: "center" }}>
|
|
|
+ Lanu ©{new Date().getFullYear()} Created by Lanu FE Team
|
|
|
+ </Footer>
|
|
|
+ </Layout>
|
|
|
+ </Layout>
|
|
|
+ );
|
|
|
+}
|