Files
tianpu-ems/frontend/src/layouts/MainLayout.tsx
Du Wenbo 6a59f9af76 feat: add 3D interactive dashboard and 2D BigScreen pages
- New /bigscreen-3d route: React Three Fiber 3D campus with buildings,
  PV panels, heat pumps, meters, and sensors — all procedural geometry
- Interactive: hover highlight, click to select, camera fly-in to
  device detail views (PV inverter, heat pump, meter, heat meter, sensor)
- Real-time data: 15s polling for overview, 5s for selected device
- Energy flow particles along PV→Building, Grid→Building, Building→HP paths
- HUD overlay with date/clock, bottom metrics bar, device list panel
- New /bigscreen route: 2D dashboard with energy flow diagram, charts
- New /devices route: device management page
- Vite config: optimizeDeps.force for R3F dep consistency
- Data backfill script for testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:43:48 +08:00

102 lines
4.0 KiB
TypeScript

import { useState } from 'react';
import { Layout, Menu, Avatar, Dropdown, Typography, Badge } from 'antd';
import {
DashboardOutlined, MonitorOutlined, BarChartOutlined, AlertOutlined,
FileTextOutlined, CloudOutlined, SettingOutlined, UserOutlined,
MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, BellOutlined,
ThunderboltOutlined, AppstoreOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { getUser, removeToken } from '../utils/auth';
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: '能源总览' },
{ key: '/monitoring', icon: <MonitorOutlined />, label: '实时监控' },
{ key: '/devices', icon: <AppstoreOutlined />, label: '设备管理' },
{ key: '/analysis', icon: <BarChartOutlined />, label: '能耗分析' },
{ key: '/alarms', icon: <AlertOutlined />, label: '告警管理' },
{ key: '/carbon', icon: <CloudOutlined />, label: '碳排放管理' },
{ key: '/reports', icon: <FileTextOutlined />, label: '报表管理' },
{ key: '/system', icon: <SettingOutlined />, label: '系统管理',
children: [
{ key: '/system/users', label: '用户管理' },
{ key: '/system/roles', label: '角色权限' },
{ key: '/system/settings', label: '系统设置' },
],
},
];
export default function MainLayout() {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const user = getUser();
const handleLogout = () => {
removeToken();
localStorage.removeItem('user');
navigate('/login');
};
const userMenu = {
items: [
{ key: 'profile', icon: <UserOutlined />, label: '个人信息' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', onClick: handleLogout },
],
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed} width={220}
style={{ background: '#001529' }}>
<div style={{
height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}>
<ThunderboltOutlined style={{ fontSize: 24, color: '#1890ff', marginRight: collapsed ? 0 : 8 }} />
{!collapsed && <Text strong style={{ color: '#fff', fontSize: 16 }}>EMS</Text>}
</div>
<Menu
theme="dark" mode="inline"
selectedKeys={[location.pathname]}
defaultOpenKeys={['/system']}
items={menuItems}
onClick={({ key }) => navigate(key)}
/>
</Sider>
<Layout>
<Header style={{
padding: '0 24px', background: '#fff', display: 'flex',
alignItems: 'center', justifyContent: 'space-between',
boxShadow: '0 1px 4px rgba(0,0,0,0.08)',
}}>
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setCollapsed(!collapsed)}>
{collapsed ? <MenuUnfoldOutlined style={{ fontSize: 18 }} /> :
<MenuFoldOutlined style={{ fontSize: 18 }} />}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
{/* TODO: fetch notification count from API */}
<Badge count={0} size="small">
<BellOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
</Badge>
<Dropdown menu={userMenu} placement="bottomRight">
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar size="small" icon={<UserOutlined />} style={{ background: '#1890ff' }} />
<Text>{user?.full_name || user?.username || '用户'}</Text>
</div>
</Dropdown>
</div>
</Header>
<Content style={{ margin: 16, padding: 24, background: '#f5f5f5', minHeight: 280 }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
}