Files
web-Iot/src/components/Layout.tsx
2026-08-03 13:19:40 +08:00

353 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}