import { useEffect, useMemo, useState } from "react"; import { Layout } from "antd"; import { AppstoreOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { api } from "../api"; import { clearTokens, getRefreshToken } from "../auth"; import Toast from "../components/Toast/Toast"; import ModernSidebar, { SidebarGroup, SidebarItem } from "../components/ModernSidebar/ModernSidebar"; import AppHeader from "./AppHeader"; import { getIcon } from "../utils/icons"; import { resolveUrl } from "../utils/url"; import "./AppLayout.css"; const { Content } = Layout; type MenuNode = { id: number; name: string; code: string; type: string; level: number; path?: string | null; icon?: string | null; avatar?: string | null; children?: MenuNode[]; }; export default function AppLayout() { const navigate = useNavigate(); const location = useLocation(); const [collapsed, setCollapsed] = useState(false); const [menus, setMenus] = useState([]); const [displayName, setDisplayName] = useState("管理员"); const [username, setUsername] = useState(""); const [userRole, setUserRole] = useState("Admin"); const [avatar, setAvatar] = useState(null); const [platformName, setPlatformName] = useState("NexDocus"); useEffect(() => { // Fetch platform name api.listParams().then((params) => { const p = params.find((x: any) => x.param_key.toUpperCase() === "PLATFORM_NAME"); if (p) { setPlatformName(p.param_value); } }).catch(() => {}); // ignore error const fetchUser = () => { api.me().then((res) => { setDisplayName(res.display_name || res.username); setUsername(res.username); setAvatar(res.avatar || null); setUserRole(res.roles && res.roles.length > 0 ? res.roles.join(" / ") : "普通用户"); }).catch(() => { clearTokens(); navigate("/login"); }); }; fetchUser(); window.addEventListener('user-refresh', fetchUser); return () => window.removeEventListener('user-refresh', fetchUser); }, [navigate]); useEffect(() => { const fetchMenu = () => { api .getMenuTree() .then((res) => setMenus(res as MenuNode[])) .catch(() => Toast.error("菜单加载失败")); }; fetchMenu(); window.addEventListener('menu-refresh', fetchMenu); return () => window.removeEventListener('menu-refresh', fetchMenu); }, []); useEffect(() => { if (!menus.length) return; if (location.pathname !== "/" && location.pathname !== "/home") return; const firstGroup = menus[0]; const firstItem = firstGroup?.children?.[0]; if (!firstItem) return; const target = firstItem.path || firstItem.code; if (target) { navigate(target, { replace: true }); } }, [menus, location.pathname, navigate]); const menuGroups: SidebarGroup[] = useMemo(() => { return menus.map((group) => ({ title: group.name, items: (group.children || []).map((child) => ({ key: child.path || child.code, label: child.name, icon: getIcon(child.icon), path: child.path || "" })), })); }, [menus]); const activeKey = useMemo(() => { return location.pathname; }, [location.pathname]); const onNavigate = (key: string, item: SidebarItem) => { if (item.path) { navigate(item.path); } else { navigate(key); } }; const handleLogout = async () => { const token = getRefreshToken(); if (token) { try { await api.logout(token); } catch { // ignore } } clearTokens(); navigate("/login"); }; return ( {/* Sidebar on the left, full height */} navigate("/profile")} style={{ height: '100vh', zIndex: 200 }} /> {/* Right side: Header + Content */} navigate("/profile")} /> ); }