Files
web-Iot/src/components/Layout.tsx
2026-05-18 12:55:41 +08:00

272 lines
8.5 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
} 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 { getStationList } 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 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 = () => {
const params = {
pageNum: 1,
pageSize: 9999,
orgId: userInfo?.orgId,
};
getStationList(params).then(res => {
if (res.code === 200) {
const list = res.rows || [];
setStationList(list);
if (!currentStation && list.length > 0) {
dispatch(setCurrentStation(list[0]));
}
}
});
};
const handleStationChange = (id: number) => {
const station = stationList.find(s => s.id === id);
if (station) {
dispatch(setCurrentStation(station));
}
};
const getMenu = () => {
const data = { pageNum: 1, pageSize: 10000 };
getMenuList(data).then(res => {
if (res.code == 200) {
const rawList = res.rows || [];
const topMenus = rawList.filter(item => item.parentId === 0);
setMenuList(topMenus);
}
});
};
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 menuItems = menuList.map(item => ({
key: item.path || `/menu/${item.menuId}`,
icon: getIcon(item.path),
label: item.menuName,
}));
const handleLogout = async () => {
await dispatch(logout());
dispatch(clearStation());
navigate('/login', { replace: true });
};
useEffect(() => {
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
getMenu();
getStationData();
return () => clearInterval(timer);
}, [userInfo]);
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(140px, 15vw, 180px)',
backgroundColor: '#0837a8',
color: '#fff',
border: 'none',
}}
/>
</Space>
<Space size="middle" style={{ flexWrap: 'wrap' }}>
<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={150}
theme="light"
style={{ borderRight: '1px solid #e5e7eb' }}
>
{/* 关键:用Layout实现上下分布 */}
<Layout style={{ height: '100%', flexDirection: 'column', background: '#fff' }}>
<Menu
mode="inline"
theme="light"
selectedKeys={[location.pathname]}
items={menuItems}
onClick={({ key }) => navigate(key)}
style={{ flex: 1, overflowY: 'auto', borderRight: 0 }}
/>
{/* 场站信息卡片:仅在侧边栏展开时显示 */}
{!collapsed && currentStation && (
<Card
size="small"
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
//borderLeft: 0,
//borderRight: 0,
//borderBottom: 0,
margin: 4,
marginBottom: 50
}}
styles={{ body: { padding: '12px 8px' } }}
>
<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: 10, 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' }}>
<span style={{ color: '#52c41a' }}>运行状态</span>
<span style={{ color: '#52c41a' }}>●</span>
<span style={{ color: '#52c41a', marginLeft: 4 }}>正常运行</span>
</div>
</div>
</div>
</Card>
)}
</Layout>
</Sider>
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
<Outlet />
</Content>
</Layout>
</Layout>
</ConfigProvider>
);
}