163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
import {
|
|
Layout, Menu, Button, Space, ConfigProvider, theme,
|
|
Breadcrumb, Badge, message
|
|
} from 'antd';
|
|
// 先引入中文语言包
|
|
import zhCN from 'antd/locale/zh_CN';
|
|
import {
|
|
DashboardOutlined, ExperimentOutlined, VideoCameraOutlined,
|
|
BellOutlined, DownloadOutlined, UserOutlined, LogoutOutlined,
|
|
MenuUnfoldOutlined, MenuFoldOutlined, TeamOutlined, MenuOutlined
|
|
} from '@ant-design/icons';
|
|
import logo from '../assets/logo.png';
|
|
import { useSelector } from 'react-redux';
|
|
import { RootState, useAppDispatch } from '../store';
|
|
import { logout } from '../store/userSlice';
|
|
import { getMenuList } from '../api/mune';
|
|
|
|
|
|
const { Header, Sider, Content } = Layout;
|
|
|
|
export default function AppLayout() {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [time, setTime] = useState(new Date().toLocaleString());
|
|
const [menuList, setMenuList] = useState([]);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { userInfo } = useSelector((state: RootState) => state.user);
|
|
const dispatch = useAppDispatch();
|
|
|
|
|
|
|
|
const getMenu = () => {
|
|
getMenuList().then(res => {
|
|
if (res.code == 200) {
|
|
setMenuList(res.data);
|
|
} else {
|
|
message.error("获取菜单失败:" + res.msg)
|
|
}
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
|
|
getMenu();
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
await dispatch(logout());
|
|
navigate('/login', { replace: true });
|
|
};
|
|
|
|
const menuItems = [
|
|
{ key: '/', icon: <DashboardOutlined />, label: '总览首页', roles: ['admin', 'operator', 'viewer'] },
|
|
{ key: '/devices', icon: <ExperimentOutlined />, label: '设备管理', roles: ['admin', 'operator'] },
|
|
{ key: '/video', icon: <VideoCameraOutlined />, label: '在线视频', roles: ['admin', 'operator', 'viewer'] },
|
|
{ key: '/alerts', icon: <BellOutlined />, label: '告警中心', roles: ['admin', 'operator'] },
|
|
{ key: '/downloads', icon: <DownloadOutlined />, label: '视频下载', roles: ['admin', 'operator'] },
|
|
{ key: '/serialNumGenerate', icon: <UserOutlined />, label: '序列号生成', roles: ['admin'] },
|
|
{ key: '/users', icon: <TeamOutlined />, label: '用户管理', roles: ['admin'] },
|
|
{ key: '/roles', icon: <UserOutlined />, label: '角色管理', roles: ['admin'] },
|
|
{ key: '/menuList', icon: <MenuOutlined />, label: '菜单管理', roles: ['admin'] },
|
|
{ key: '/organization', icon: <MenuOutlined />, label: '组织管理', roles: ['admin'] },
|
|
{ key: '/station', icon: <MenuOutlined />, label: '场站管理', roles: ['admin'] }
|
|
];
|
|
|
|
const filteredMenu = menuItems;
|
|
const currentPathLabel = menuItems.find(i => i.key === location.pathname)?.label || '总览首页';
|
|
|
|
return (
|
|
<ConfigProvider
|
|
locale={zhCN}
|
|
theme={{
|
|
algorithm: theme.defaultAlgorithm,
|
|
token: { colorPrimary: '#1890ff', borderRadius: 2 },
|
|
components: {
|
|
Layout: { headerBg: '#fff', siderBg: '#001529' },
|
|
Menu: { darkItemBg: '#001529', darkItemSelectedBg: '#1890ff' }
|
|
}
|
|
}}
|
|
>
|
|
<Layout style={{ maxHeight: '100vh' }}>
|
|
{/* 顶部通栏 */}
|
|
<Header className=" px-6 flex items-center justify-between shadow-sm border-b border-gray-100 h-0 shrink-0 z-10" style={{
|
|
background: '#0837a8',
|
|
color: '#fff',
|
|
height: "50px"
|
|
}}>
|
|
<div className="h-16 flex items-center justify-center border-b border-white/10 overflow-hidden">
|
|
<Space size="small" className="px-2">
|
|
<img src={logo} alt="Logo" style={{ width: "24px", height: "24px" }} />
|
|
{!collapsed && (
|
|
<span className="text-white font-bold text-lg whitespace-nowrap">
|
|
光伏电站智能监控与运维系统
|
|
</span>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-3 pr-6 border-r border-gray-100">
|
|
<span className="text-gray-400 font-mono text-[13px]">{time}</span>
|
|
<Badge status="success" />
|
|
<span className="text-white font-medium">{userInfo?.username}</span>
|
|
</div>
|
|
<Button
|
|
type="text"
|
|
danger
|
|
icon={<LogoutOutlined />}
|
|
onClick={handleLogout}
|
|
className="font-bold text-[12px] uppercase"
|
|
>
|
|
退出登录
|
|
</Button>
|
|
</div>
|
|
</Header>
|
|
|
|
<Layout style={{
|
|
overflow: 'auto', background: '#ffffff',
|
|
height: "calc(100vh - 50px)",
|
|
overflowX: 'hidden'
|
|
}}>
|
|
{/* 左侧:菜单 + 下部区域 */}
|
|
<Sider
|
|
trigger={null}
|
|
collapsible
|
|
collapsed={collapsed}
|
|
width={220}
|
|
theme="light"
|
|
style={{
|
|
overflow: 'auto', background: '#ffffff',
|
|
borderRight: '1px solid #e5e7eb',
|
|
}}
|
|
>
|
|
|
|
|
|
<Menu
|
|
theme="light"
|
|
mode="inline"
|
|
selectedKeys={[location.pathname]}
|
|
items={filteredMenu}
|
|
onClick={({ key }) => navigate(key)}
|
|
/>
|
|
|
|
{/* 左侧下方固定区域(你可以放任何内容) */}
|
|
<div className="p-3 text-white text-xs opacity-70 text-center mt-4">
|
|
{!collapsed && '快捷操作面板'}
|
|
</div>
|
|
</Sider>
|
|
|
|
{/* 右侧:主内容 */}
|
|
<Content className=" overflow-y-auto bg-[#f4f7f9] min-h-0 overflow-x-hidden">
|
|
<Outlet />
|
|
</Content>
|
|
</Layout>
|
|
</Layout>
|
|
|
|
</ConfigProvider >
|
|
);
|
|
}
|