|
|
@@ -1,11 +1,6 @@
|
|
|
"use client";
|
|
|
|
|
|
-import {
|
|
|
- HomeOutlined,
|
|
|
- LogoutOutlined,
|
|
|
- TeamOutlined,
|
|
|
- UserOutlined,
|
|
|
-} from "@ant-design/icons";
|
|
|
+import { LogoutOutlined, UserOutlined } from "@ant-design/icons";
|
|
|
import type { MenuProps } from "antd";
|
|
|
import {
|
|
|
Avatar,
|
|
|
@@ -19,69 +14,35 @@ import {
|
|
|
} from "antd";
|
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
|
import type React from "react";
|
|
|
-import { useEffect, useMemo, useState } from "react";
|
|
|
+import { useEffect, useMemo } from "react";
|
|
|
+import MenuSearch from "@/components/MenuSearch";
|
|
|
+import TabBar from "@/components/TabBar";
|
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
|
+import { useMenuStore } from "@/stores/menuStore";
|
|
|
+import { useTabStore } from "@/stores/tabStore";
|
|
|
+import type { MenuItem as MenuItemType } from "@/types/menu";
|
|
|
+import { getParentKeys } from "@/utils/menuUtils";
|
|
|
|
|
|
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;
|
|
|
-}
|
|
|
+type AntMenuItem = Required<MenuProps>["items"][number];
|
|
|
|
|
|
-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: "用户列表" }],
|
|
|
-};
|
|
|
+// Convert our MenuItem to Ant Design MenuItem format
|
|
|
+function convertToAntdMenu(items: MenuItemType[]): AntMenuItem[] {
|
|
|
+ return items.map((item) => ({
|
|
|
+ key: item.key,
|
|
|
+ icon: item.icon,
|
|
|
+ label: item.label,
|
|
|
+ children: item.children ? convertToAntdMenu(item.children) : undefined,
|
|
|
+ }));
|
|
|
+}
|
|
|
|
|
|
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();
|
|
|
@@ -89,6 +50,33 @@ export default function DashboardLayout({
|
|
|
token: { colorBgContainer, borderRadiusLG },
|
|
|
} = theme.useToken();
|
|
|
|
|
|
+ // Menu store
|
|
|
+ const menus = useMenuStore((state) => state.menus);
|
|
|
+ const collapsed = useMenuStore((state) => state.collapsed);
|
|
|
+ const setCollapsed = useMenuStore((state) => state.setCollapsed);
|
|
|
+ const initMenus = useMenuStore((state) => state.initMenus);
|
|
|
+ const setActiveMenuByPath = useMenuStore(
|
|
|
+ (state) => state.setActiveMenuByPath,
|
|
|
+ );
|
|
|
+ const getBreadcrumb = useMenuStore((state) => state.getBreadcrumb);
|
|
|
+
|
|
|
+ // Tab store
|
|
|
+ const tabs = useTabStore((state) => state.tabs);
|
|
|
+ const initTabs = useTabStore((state) => state.initTabs);
|
|
|
+
|
|
|
+ // Initialize menus and tabs on mount
|
|
|
+ useEffect(() => {
|
|
|
+ if (menus.length === 0) {
|
|
|
+ initMenus();
|
|
|
+ }
|
|
|
+ }, [menus.length, initMenus]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (isAuthenticated && tabs.length === 0) {
|
|
|
+ initTabs();
|
|
|
+ }
|
|
|
+ }, [isAuthenticated, tabs.length, initTabs]);
|
|
|
+
|
|
|
// Redirect to login if not authenticated
|
|
|
useEffect(() => {
|
|
|
if (!isLoading && !isAuthenticated) {
|
|
|
@@ -96,21 +84,62 @@ export default function DashboardLayout({
|
|
|
}
|
|
|
}, [isAuthenticated, isLoading, router]);
|
|
|
|
|
|
- // Get current selected key and open keys based on pathname
|
|
|
+ // Update active menu when pathname changes
|
|
|
+ useEffect(() => {
|
|
|
+ if (pathname) {
|
|
|
+ setActiveMenuByPath(pathname);
|
|
|
+ }
|
|
|
+ }, [pathname, setActiveMenuByPath]);
|
|
|
+
|
|
|
+ // Convert menus to Ant Design format
|
|
|
+ const antdMenuItems = useMemo(() => {
|
|
|
+ return convertToAntdMenu(menus);
|
|
|
+ }, [menus]);
|
|
|
+
|
|
|
+ // Get current selected key based on pathname
|
|
|
const selectedKey = useMemo(() => {
|
|
|
- return pathToKeyMap[pathname] || "home";
|
|
|
- }, [pathname]);
|
|
|
+ // Find menu item by path
|
|
|
+ function findKeyByPath(items: MenuItemType[], path: string): string | null {
|
|
|
+ for (const item of items) {
|
|
|
+ if (item.path === path) {
|
|
|
+ return item.key;
|
|
|
+ }
|
|
|
+ if (item.children) {
|
|
|
+ const found = findKeyByPath(item.children, path);
|
|
|
+ if (found) return found;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return findKeyByPath(menus, pathname) || "home";
|
|
|
+ }, [pathname, menus]);
|
|
|
|
|
|
+ // Get open keys for current path
|
|
|
const defaultOpenKeys = useMemo(() => {
|
|
|
- return keyToOpenKeysMap[selectedKey] || [];
|
|
|
- }, [selectedKey]);
|
|
|
+ return getParentKeys(menus, selectedKey);
|
|
|
+ }, [menus, selectedKey]);
|
|
|
|
|
|
+ // Get breadcrumb items
|
|
|
const breadcrumbItems = useMemo(() => {
|
|
|
- return pathToBreadcrumb[pathname] || [{ title: "首页" }];
|
|
|
- }, [pathname]);
|
|
|
+ const breadcrumb = getBreadcrumb(pathname);
|
|
|
+ return breadcrumb.map((title) => ({ title }));
|
|
|
+ }, [pathname, getBreadcrumb]);
|
|
|
|
|
|
const handleMenuClick = (e: { key: string }) => {
|
|
|
- const path = keyToPathMap[e.key];
|
|
|
+ // Find menu item by key
|
|
|
+ function findPathByKey(items: MenuItemType[], key: string): string | null {
|
|
|
+ for (const item of items) {
|
|
|
+ if (item.key === key) {
|
|
|
+ return item.path;
|
|
|
+ }
|
|
|
+ if (item.children) {
|
|
|
+ const found = findPathByKey(item.children, key);
|
|
|
+ if (found) return found;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const path = findPathByKey(menus, e.key);
|
|
|
if (path) {
|
|
|
router.push(path);
|
|
|
}
|
|
|
@@ -137,14 +166,27 @@ export default function DashboardLayout({
|
|
|
collapsible
|
|
|
collapsed={collapsed}
|
|
|
onCollapse={(value) => setCollapsed(value)}
|
|
|
+ theme="dark"
|
|
|
>
|
|
|
- <div className="demo-logo-vertical" />
|
|
|
+ <div
|
|
|
+ className="flex items-center justify-center"
|
|
|
+ style={{
|
|
|
+ height: "32px",
|
|
|
+ margin: "16px",
|
|
|
+ fontSize: "18px",
|
|
|
+ color: "#fff",
|
|
|
+ fontWeight: "bold",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {!collapsed && "Lanu OP"}
|
|
|
+ </div>
|
|
|
+ <MenuSearch collapsed={collapsed} />
|
|
|
<Menu
|
|
|
theme="dark"
|
|
|
selectedKeys={[selectedKey]}
|
|
|
defaultOpenKeys={defaultOpenKeys}
|
|
|
mode="inline"
|
|
|
- items={items}
|
|
|
+ items={antdMenuItems}
|
|
|
onClick={handleMenuClick}
|
|
|
/>
|
|
|
</Sider>
|
|
|
@@ -168,6 +210,7 @@ export default function DashboardLayout({
|
|
|
</Space>
|
|
|
</Dropdown>
|
|
|
</Header>
|
|
|
+ <TabBar />
|
|
|
<Content style={{ margin: "0 16px" }}>
|
|
|
<Breadcrumb style={{ margin: "16px 0" }} items={breadcrumbItems} />
|
|
|
<div
|