2026-08-31 10:30:21 +08:00
|
|
|
|
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 {
|
2026-04-28 11:01:54 +08:00
|
|
|
|
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-28 11:01:54 +08:00
|
|
|
|
|
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-04-28 11:01:54 +08:00
|
|
|
|
|
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>
|
2026-08-31 14:16:36 +08:00
|
|
|
|
{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-04-28 11:01:54 +08:00
|
|
|
|
>
|
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
|
|
|
|
}
|