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

324 lines
9.7 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
2026-04-17 17:20:41 +08:00
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
2026-04-21 08:51:57 +08:00
import {
2026-08-12 11:29:44 +08:00
Layout, Menu, Button, Space, Select, Avatar, Card, Tag
2026-04-17 17:20:41 +08:00
} from 'antd';
2026-04-21 08:51:57 +08:00
import {
DashboardOutlined, ExperimentOutlined, VideoCameraOutlined,
2026-04-30 10:49:44 +08:00
BellOutlined, UserOutlined, LogoutOutlined,
2026-06-12 11:03:38 +08:00
AppstoreOutlined, SettingOutlined, MenuUnfoldOutlined, MenuFoldOutlined,
CompressOutlined,
ExpandOutlined
2026-04-17 17:20:41 +08:00
} from '@ant-design/icons';
2026-08-12 11:29:44 +08:00
import { useTranslation } from 'react-i18next';
2026-04-21 08:51:57 +08:00
import logo from '../assets/logo.png';
2026-04-21 14:57:52 +08:00
import { useSelector } from 'react-redux';
2026-04-22 09:43:55 +08:00
import { RootState, useAppDispatch } from '../store';
import { logout } from '../store/userSlice';
2026-05-14 08:45:14 +08:00
import { setCurrentStation, clearStation } from '../store/stationSlice';
2026-04-22 09:43:55 +08:00
import { getMenuList } from '../api/mune';
2026-07-09 09:50:46 +08:00
import { getStationByUserId } from '../api/stationManage';
2026-08-12 11:29:44 +08:00
import LanguageSwitcher from './LanguageSwitcher';
2026-05-13 11:12:17 +08:00
import envConfig from '../../env';
2026-04-17 17:20:41 +08:00
const { Header, Sider, Content } = Layout;
export default function AppLayout() {
2026-08-12 11:29:44 +08:00
const { t } = useTranslation();
2026-04-21 08:51:57 +08:00
const [collapsed, setCollapsed] = useState(false);
const [time, setTime] = useState(new Date().toLocaleString());
2026-05-06 16:49:07 +08:00
const [menuList, setMenuList] = useState([]);
const [stationList, setStationList] = useState([]);
2026-08-12 11:29:44 +08:00
const [isFullScreen, setIsFullScreen] = useState(false);
2026-05-06 16:49:07 +08:00
2026-04-21 08:51:57 +08:00
const navigate = useNavigate();
const location = useLocation();
2026-04-22 09:43:55 +08:00
const dispatch = useAppDispatch();
2026-05-06 16:49:07 +08:00
const { userInfo } = useSelector((state: RootState) => state.user);
const { currentStation, stationId } = useSelector((state: RootState) => state.station);
const getStationData = () => {
2026-07-09 09:50:46 +08:00
getStationByUserId().then(res => {
2026-05-06 16:49:07 +08:00
if (res.code === 200) {
2026-07-09 09:50:46 +08:00
const list = res.data || [];
2026-05-06 16:49:07 +08:00
setStationList(list);
if (!currentStation && list.length > 0) {
dispatch(setCurrentStation(list[0]));
}
}
});
};
2026-08-12 11:29:44 +08:00
2026-06-12 11:03:38 +08:00
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
setIsFullScreen(true);
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
setIsFullScreen(false);
}
}
};
2026-05-06 16:49:07 +08:00
const handleStationChange = (id: number) => {
const station = stationList.find(s => s.id === id);
if (station) {
2026-05-14 08:45:14 +08:00
dispatch(setCurrentStation(station));
2026-05-06 16:49:07 +08:00
}
};
2026-04-30 10:49:44 +08:00
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 />;
2026-04-29 15:01:00 +08:00
};
2026-08-03 13:19:40 +08:00
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;
};
2026-08-12 11:29:44 +08:00
2026-08-03 13:19:40 +08:00
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,
2026-08-12 11:29:44 +08:00
icon: isChild ? null : getIcon(item.path),
2026-08-03 13:19:40 +08:00
label: item.menuName,
...(hasChild && {
children: renderMenuItems(item.children, true),
}),
};
});
};
2026-04-22 09:43:55 +08:00
const handleLogout = async () => {
await dispatch(logout());
2026-05-14 08:45:14 +08:00
dispatch(clearStation());
2026-04-22 09:43:55 +08:00
navigate('/login', { replace: true });
2026-04-21 08:51:57 +08:00
};
2026-04-17 17:20:41 +08:00
2026-08-03 13:19:40 +08:00
const getMenu = () => {
getMenuList({
pageNum: 1,
pageSize: 10000,
}).then(res => {
if (res.code === 200) {
const menus = buildMenuTree(res.rows || []);
setMenuList(menus);
}
});
};
2026-05-06 16:49:07 +08:00
useEffect(() => {
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
getMenu();
getStationData();
return () => clearInterval(timer);
}, [userInfo]);
2026-06-12 11:03:38 +08:00
useEffect(() => {
const fullscreenChange = () => {
setIsFullScreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', fullscreenChange);
return () => document.removeEventListener('fullscreenchange', fullscreenChange);
}, []);
2026-04-21 08:51:57 +08:00
return (
2026-08-12 11:29:44 +08:00
<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>}
2026-04-29 15:01:00 +08:00
</Space>
2026-08-12 11:29:44 +08:00
<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'
}}
>
2026-08-12 11:29:44 +08:00
<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>
2026-08-03 13:29:16 +08:00
2026-08-12 11:29:44 +08:00
{!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 }}>
2026-08-21 09:45:02 +08:00
{/*<div>{JSON.stringify(currentStation, null, 2)}</div>*/}
2026-08-12 11:29:44 +08:00
<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>
2026-05-18 12:55:41 +08:00
</div>
</div>
2026-08-12 11:29:44 +08:00
</div>
</Card>
)}
</Layout>
</Sider>
<Content style={{ background: '#f4f7f9', overflowY: 'auto', padding: '4px 8px' }}>
<Outlet />
</Content>
2026-04-21 08:51:57 +08:00
</Layout>
2026-08-12 11:29:44 +08:00
</Layout>
2026-04-21 08:51:57 +08:00
);
2026-04-17 17:20:41 +08:00
}