nex_basse/frontend/src/layout/AppLayout.tsx

171 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-02-25 08:48:31 +00:00
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<MenuNode[]>([]);
const [displayName, setDisplayName] = useState("管理员");
const [username, setUsername] = useState("");
const [userRole, setUserRole] = useState("Admin");
const [avatar, setAvatar] = useState<string | null>(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 (
<Layout className="app-layout-root">
{/* Sidebar on the left, full height */}
<ModernSidebar
collapsed={collapsed}
onCollapse={setCollapsed}
logo={null}
platformName={platformName}
menuGroups={menuGroups}
activeKey={activeKey}
onNavigate={onNavigate}
user={{
name: displayName,
role: username,
avatar: resolveUrl(avatar)
}}
onLogout={handleLogout}
onProfileClick={() => navigate("/profile")}
style={{ height: '100vh', zIndex: 200 }}
/>
{/* Right side: Header + Content */}
<Layout className="app-layout-right">
<AppHeader
displayName={displayName}
onLogout={handleLogout}
onProfileClick={() => navigate("/profile")}
/>
<Layout className="app-layout-content-wrapper">
<Content className="app-content-area">
<Outlet />
</Content>
</Layout>
</Layout>
</Layout>
);
}