nex_basse/frontend/src/layout/AppLayout.tsx

189 lines
5.8 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { Layout } from "antd";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { api } from "../api";
import { clearTokens } from "../auth";
import Toast from "../components/Toast/Toast";
import ModernSidebar, { SidebarGroup } from "../components/ModernSidebar/ModernSidebar";
import AppHeader from "./AppHeader";
import { getIcon } from "../utils/icons";
import "./AppLayout.css";
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 { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(false);
const [menus, setMenus] = useState<MenuNode[]>([]);
const [displayName, setDisplayName] = useState("Admin");
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(" / ") : "User");
}).catch(() => {
clearTokens();
navigate("/login");
});
};
fetchUser();
window.addEventListener('user-refresh', fetchUser);
return () => window.removeEventListener('user-refresh', fetchUser);
}, [navigate]);
useEffect(() => {
const fetchMenu = () => {
api
.getMenuTree()
.then((res) => {
const serverMenus = res as MenuNode[];
setMenus(serverMenus);
})
.catch(() => Toast.error(t('common.error')));
};
fetchMenu();
window.addEventListener('menu-refresh', fetchMenu);
return () => window.removeEventListener('menu-refresh', fetchMenu);
}, [t]);
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]);
// Helper to translate menu names
const translateMenuName = (name: string, code: string) => {
// Try to find translation by code (e.g. 'system.user' -> 'menu.userManage')
// This requires a mapping or convention.
// For now, let's try a simple mapping based on the code or just return name if not found.
// Example codes: 'dashboard', 'workspace', 'system'
// Map code to translation key
const codeMap: Record<string, string> = {
'home': 'menu.home',
'dashboard': 'menu.dashboard',
'workspace': 'menu.workspace',
'meeting.realtime': 'menu.realtimeMeeting',
'meeting.history': 'menu.historyMeeting',
'voice.profile': 'menu.voiceProfile',
'knowledge': 'menu.knowledgeBase',
'knowledge.manage': 'menu.kbManage',
'hotwords': 'menu.hotwords',
'system': 'menu.system',
'system.settings': 'menu.systemSettings',
'system.user': 'menu.userManage',
'system.role': 'menu.roleManage',
'system.permission': 'menu.permissionManage',
'system.dict': 'menu.dictManage',
'system.param': 'menu.paramManage',
'system.log': 'menu.logManage',
'monitor.task': 'menu.taskMonitor',
'ai_agent.prompts': 'menu.promptManage',
'ai_agent.models': 'menu.modelManage'
};
if (codeMap[code]) {
return t(codeMap[code]);
}
return name;
};
const menuGroups: SidebarGroup[] = useMemo(() => {
return menus.map((group) => ({
title: translateMenuName(group.name, group.code),
items: (group.children || []).map((child) => ({
key: child.path || child.code,
label: translateMenuName(child.name, child.code),
icon: getIcon(child.icon),
path: child.path
}))
}));
}, [menus, t]);
const handleLogout = () => {
clearTokens();
navigate("/login");
};
return (
<Layout style={{ minHeight: "100vh" }}>
<ModernSidebar
platformName={platformName}
user={{
name: displayName,
role: userRole,
avatar: avatar || undefined
}}
menuGroups={menuGroups}
collapsed={collapsed}
onCollapse={setCollapsed}
activeKey={location.pathname}
onNavigate={(key) => navigate(key)}
onLogout={handleLogout}
onProfileClick={() => navigate('/profile')}
/>
<Layout>
<AppHeader
displayName={displayName}
onLogout={handleLogout}
onProfileClick={() => navigate('/profile')}
/>
<Layout.Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
// background: colorBgContainer, // ModernSidebar handles its own background, but content needs one?
// Actually AppLayout.css or global css might handle it.
// Let's use standard layout for now.
}}
>
<Outlet />
</Layout.Content>
</Layout>
</Layout>
);
}