Files
web-Iot/src/components/Layout.tsx

324 lines
9.7 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, Select, Avatar, Card, Tag
} from 'antd';
import {
DashboardOutlined, ExperimentOutlined, VideoCameraOutlined,
BellOutlined, UserOutlined, LogoutOutlined,
AppstoreOutlined, SettingOutlined, MenuUnfoldOutlined, MenuFoldOutlined,
CompressOutlined,
ExpandOutlined
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
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 LanguageSwitcher from './LanguageSwitcher';
import envConfig from '../../env';
const { Header, Sider, Content } = Layout;
export default function AppLayout() {
const { t } = useTranslation();
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, {
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 (
<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' }}>
{t('layout.systemName')}
</span>
)}
</div>
<Select
value={stationId}
placeholder={t('layout.selectStation')}
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 ? t('layout.exitFullScreen') : t('layout.fullScreen')}
</Button>
<LanguageSwitcher />
<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>
{userInfo?.roleKey === "admin" && <Tag color="red">管理员</Tag>}
{userInfo?.roleKey === "manager" && <Tag color="orange">组织管理员</Tag>}
{userInfo?.roleKey === "siteManager" && <Tag color="blue">场站管理员</Tag>}
</Space>
<Button type="text" danger icon={<LogoutOutlined />} onClick={handleLogout} style={{ fontSize: 'clamp(12px, 1vw, 14px)' }}>
{t('layout.logout')}
</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
style={{
height: '100%',
flexDirection: 'column',
background: '#fff',
overflow: 'hidden'
}}
>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
<Menu
mode="inline"
theme="light"
items={menuList}
selectedKeys={[location.pathname]}
onClick={({ key }) => navigate(key)}
style={{ borderRight: 0 }}
/>
</div>
{!collapsed && currentStation && (
<Card
size="small"
style={{
border: '1px solid #e5e7eb',
borderRadius: 8,
margin: 4,
marginBottom: 8
}}
styles={{ body: { padding: '12px 4px' } }}
>
<div style={{ textAlign: 'left' }}>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
{currentStation.siteName || t('layout.defaultStationName')}
</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"
/>
<div style={{ fontSize: 11, color: '#666', lineHeight: 1.8 }}>
{/*<div>{JSON.stringify(currentStation, null, 2)}</div>*/}
<div>{t('layout.stationAddress')}:{currentStation.address || t('layout.defaultStationAddress')}</div>
<div>{t('layout.stationType')}:{currentStation.type || t('layout.groundStation')}</div>
<div style={{ marginTop: 4, display: 'flex', alignItems: 'center', color: '#41ad0a' }}>
<span>{t('layout.runningStatus')}:</span>
<span style={{ marginLeft: 4 }}>●</span>
<span style={{ marginLeft: 4 }}>{t('layout.normalRunning')}</span>
</div>
</div>
</div>
</Card>
)}
</Layout>
</Sider>
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
}