189 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|