353 lines
10 KiB
TypeScript
353 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||
import {
|
||
Layout, Menu, Button, Space, ConfigProvider, theme, Select, Avatar, Card, Tag
|
||
} from 'antd';
|
||
import zhCN from 'antd/locale/zh_CN';
|
||
import {
|
||
DashboardOutlined, ExperimentOutlined, VideoCameraOutlined,
|
||
BellOutlined, UserOutlined, LogoutOutlined,
|
||
AppstoreOutlined, SettingOutlined, MenuUnfoldOutlined, MenuFoldOutlined,
|
||
CompressOutlined,
|
||
ExpandOutlined
|
||
} 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 { setCurrentStation, clearStation } from '../store/stationSlice';
|
||
import { getMenuList } from '../api/mune';
|
||
import { getStationByUserId } from '../api/stationManage';
|
||
import envConfig from '../../env';
|
||
|
||
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 [stationList, setStationList] = useState([]);
|
||
const [isFullScreen, setIsFullScreen] = useState(false); // 新增全屏标识
|
||
|
||
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
const dispatch = useAppDispatch();
|
||
|
||
const { userInfo } = useSelector((state: RootState) => state.user);
|
||
const { currentStation, stationId } = useSelector((state: RootState) => state.station);
|
||
|
||
const getStationData = () => {
|
||
|
||
getStationByUserId().then(res => {
|
||
if (res.code === 200) {
|
||
const list = res.data || [];
|
||
setStationList(list);
|
||
if (!currentStation && list.length > 0) {
|
||
dispatch(setCurrentStation(list[0]));
|
||
}
|
||
}
|
||
});
|
||
};
|
||
const toggleFullScreen = () => {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen();
|
||
setIsFullScreen(true);
|
||
} else {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
setIsFullScreen(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleStationChange = (id: number) => {
|
||
const station = stationList.find(s => s.id === id);
|
||
if (station) {
|
||
dispatch(setCurrentStation(station));
|
||
}
|
||
};
|
||
|
||
const getIcon = (path) => {
|
||
if (path.includes('overview')) return <DashboardOutlined />;
|
||
if (path.includes('device')) return <ExperimentOutlined />;
|
||
if (path.includes('monitor')) return <VideoCameraOutlined />;
|
||
if (path.includes('alert')) return <BellOutlined />;
|
||
if (path.includes('user')) return <UserOutlined />;
|
||
if (path.includes('system')) return <SettingOutlined />;
|
||
if (path.includes('menu')) return <AppstoreOutlined />;
|
||
return <AppstoreOutlined />;
|
||
};
|
||
|
||
const buildMenuTree = (list: any[]) => {
|
||
const map = new Map();
|
||
const tree: any[] = [];
|
||
|
||
// 建立映射
|
||
list.forEach(item => {
|
||
map.set(item.menuId, {
|
||
...item,
|
||
key: item.path,
|
||
label: item.menuName,
|
||
icon: getIcon(item.path),
|
||
children: [],
|
||
});
|
||
});
|
||
|
||
// 建树
|
||
list.forEach(item => {
|
||
const node = map.get(item.menuId);
|
||
|
||
if (item.parentId === 0) {
|
||
tree.push(node);
|
||
} else {
|
||
const parent = map.get(item.parentId);
|
||
if (parent) {
|
||
parent.children.push(node);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 排序
|
||
const sortMenu = (menus: any[]) => {
|
||
menus.sort((a, b) => a.orderNum - b.orderNum);
|
||
menus.forEach(menu => {
|
||
if (menu.children.length) {
|
||
sortMenu(menu.children);
|
||
} else {
|
||
delete menu.children;
|
||
}
|
||
});
|
||
};
|
||
|
||
sortMenu(tree);
|
||
|
||
return tree;
|
||
};
|
||
const renderMenuItems = (treeList, isChild = false) => {
|
||
return treeList.map(item => {
|
||
const hasChild = item.children && item.children.length > 0;
|
||
const key = item.path || `/menu/${item.menuId}`;
|
||
return {
|
||
key,
|
||
icon: isChild ? null : getIcon(item.path), // 子菜单无图标
|
||
label: item.menuName,
|
||
...(hasChild && {
|
||
children: renderMenuItems(item.children, true),
|
||
}),
|
||
};
|
||
});
|
||
};
|
||
|
||
|
||
|
||
|
||
const handleLogout = async () => {
|
||
await dispatch(logout());
|
||
dispatch(clearStation());
|
||
navigate('/login', { replace: true });
|
||
};
|
||
|
||
const getMenu = () => {
|
||
getMenuList({
|
||
pageNum: 1,
|
||
pageSize: 10000,
|
||
}).then(res => {
|
||
if (res.code === 200) {
|
||
const menus = buildMenuTree(res.rows || []);
|
||
setMenuList(menus);
|
||
}
|
||
});
|
||
};
|
||
|
||
useEffect(() => {
|
||
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
|
||
getMenu();
|
||
getStationData();
|
||
return () => clearInterval(timer);
|
||
}, [userInfo]);
|
||
|
||
useEffect(() => {
|
||
const fullscreenChange = () => {
|
||
setIsFullScreen(!!document.fullscreenElement);
|
||
};
|
||
document.addEventListener('fullscreenchange', fullscreenChange);
|
||
return () => document.removeEventListener('fullscreenchange', fullscreenChange);
|
||
}, []);
|
||
|
||
return (
|
||
<ConfigProvider
|
||
locale={zhCN}
|
||
theme={{
|
||
algorithm: theme.defaultAlgorithm,
|
||
token: {
|
||
colorPrimary: '#1890ff',
|
||
borderRadius: 2,
|
||
padding: 12,
|
||
paddingLG: 16,
|
||
margin: 12,
|
||
marginLG: 16,
|
||
},
|
||
components: {
|
||
Layout: { headerBg: '#fff', siderBg: '#fff' },
|
||
Menu: { itemBg: '#fff', itemSelectedBg: '#e6f4ff' },
|
||
Card: {
|
||
paddingLG: 12,
|
||
},
|
||
Table: {
|
||
padding: 8,
|
||
paddingXS: 4,
|
||
},
|
||
Form: {
|
||
itemMarginBottom: 12,
|
||
},
|
||
Space: {
|
||
gap: 8,
|
||
}
|
||
}
|
||
}}
|
||
>
|
||
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||
<Header style={{
|
||
background: '#0837a8',
|
||
color: '#fff',
|
||
minHeight: "40px",
|
||
height: "auto",
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '2px 12px',
|
||
flexWrap: 'wrap',
|
||
gap: '4px'
|
||
}}>
|
||
<Space size="large" style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', height: "100%", justifyContent: "center" }}>
|
||
|
||
<Button
|
||
type="text"
|
||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
onClick={() => setCollapsed(!collapsed)}
|
||
style={{
|
||
fontSize: '16px',
|
||
color: '#fff',
|
||
width: 40,
|
||
height: 40,
|
||
}}
|
||
/>
|
||
|
||
<div style={{ height: "100%", display: "flex", gap: "10px", justifyContent: "center", alignItems: "center" }}>
|
||
<img src={logo} alt="Logo" style={{ width: 24, height: 24 }} />
|
||
{!collapsed && (
|
||
<span style={{ fontWeight: 'bold', fontSize: 'clamp(14px, 1.2vw, 16px)', color: '#fff', whiteSpace: 'nowrap' }}>
|
||
光伏电站智能监控与运维系统
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<Select
|
||
value={stationId}
|
||
placeholder="请选择电站"
|
||
onChange={handleStationChange}
|
||
options={stationList.map(s => ({ label: s.siteName, value: s.id }))}
|
||
style={{
|
||
width: 'clamp(160px, 15vw, 220px)',
|
||
backgroundColor: '#0837a8',
|
||
color: '#fff',
|
||
border: 'none',
|
||
|
||
}}
|
||
/>
|
||
</Space>
|
||
|
||
<Space size="middle" style={{ flexWrap: 'wrap' }}>
|
||
<Button
|
||
type="text"
|
||
icon={isFullScreen ? <CompressOutlined /> : <ExpandOutlined />}
|
||
onClick={toggleFullScreen}
|
||
style={{ color: '#fff', fontSize: 'clamp(12px, 1vw, 14px)' }}
|
||
>
|
||
{isFullScreen ? '退出全屏' : '全屏'}
|
||
</Button>
|
||
<span style={{ fontSize: 'clamp(12px, 1vw, 13px)' }}>{time}</span>
|
||
<Space
|
||
style={{ cursor: 'pointer', padding: '0 4px' }}
|
||
onClick={() => navigate('/profile')}
|
||
className="hover:opacity-80"
|
||
>
|
||
{userInfo?.avatar && (
|
||
<Avatar
|
||
size="small"
|
||
src={userInfo.avatar.startsWith('http') ? userInfo.avatar : `${envConfig.baseURL}${userInfo.avatar}`}
|
||
/>
|
||
)}
|
||
<span style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>{userInfo?.nickName || userInfo?.username}</span>
|
||
</Space>
|
||
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout} style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>
|
||
退出登录
|
||
</Button>
|
||
</Space>
|
||
</Header>
|
||
|
||
<Layout style={{ flex: 1, overflow: 'hidden', background: '#fff' }}>
|
||
<Sider
|
||
collapsible
|
||
collapsed={collapsed}
|
||
trigger={null}
|
||
width={180}
|
||
theme="light"
|
||
style={{ borderRight: '1px solid #e5e7eb' }}
|
||
>
|
||
{/* 关键:用Layout实现上下分布 */}
|
||
<Layout style={{ height: '100%', flexDirection: 'column', background: '#fff' }}>
|
||
<Menu
|
||
mode="inline"
|
||
theme="light"
|
||
items={menuList}
|
||
selectedKeys={[location.pathname]}
|
||
onClick={({ key }) => navigate(key)}
|
||
/>
|
||
{/* 场站信息卡片:仅在侧边栏展开时显示 */}
|
||
{!collapsed && currentStation && (
|
||
<Card
|
||
size="small"
|
||
style={{
|
||
border: '1px solid #e5e7eb',
|
||
borderRadius: 8,
|
||
|
||
margin: 4,
|
||
marginBottom: 50
|
||
}}
|
||
styles={{ body: { padding: '12px 4px' } }}
|
||
>
|
||
<div style={{ textAlign: 'left' }}>
|
||
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
|
||
{currentStation.siteName || '1MW 光伏电站'}
|
||
</div>
|
||
<img style={{ width: '100%', borderRadius: "8px", marginBottom: 8 }} src="https://images.unsplash.com/photo-1509391366360-2e959784a276?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80" />
|
||
|
||
{/* <img src="电站图片地址" style={{ width: '100%', borderRadius: 4, marginBottom: 8 }} alt="电站" /> */}
|
||
<div style={{ fontSize: 11, color: '#666', lineHeight: 1.8 }}>
|
||
{/*<div>装机容量:{currentStation.capacity || '1.00'} MWp</div>
|
||
<div>并网时间:{currentStation.gridTime || '2024-01-15'}</div>*/}
|
||
<div>电站地址:{currentStation.address || '河南省林州'}</div>
|
||
<div>电站类型:{currentStation.type || '地面电站'}</div>
|
||
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', color: '#41ad0a' }}>
|
||
<span>运行状态:</span>
|
||
<span style={{ marginLeft: 4 }}>●</span>
|
||
<span style={{ marginLeft: 4 }}>正常运行</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
</Layout>
|
||
</Sider>
|
||
|
||
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
|
||
<Outlet />
|
||
</Content>
|
||
</Layout>
|
||
</Layout>
|
||
</ConfigProvider>
|
||
);
|
||
}
|