Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx

95 lines
3.9 KiB
TypeScript

import {
IconAlarm,
IconBarChartHStroked,
IconBox,
IconChevronLeft,
IconHelpCircle,
IconHome,
IconMapPin,
IconSearch,
IconSetting,
IconUser,
IconExit
} from '@douyinfe/semi-icons';
import { useEffect, useState } from 'react';
import { NavLink, Outlet, useLocation } from 'react-router-dom';
import { usePlatformSession } from '../auth/AuthGate';
import { preloadRoute, scheduleIdleRoutePreloads } from '../routing/routeModules';
const navigation = [
{ to: '/monitor', label: '全局监控', icon: IconHome },
{ to: '/vehicles', label: '车辆查询', icon: IconSearch },
{ to: '/tracks', label: '轨迹回放', icon: IconMapPin },
{ to: '/history', label: '历史数据', icon: IconBarChartHStroked },
{ to: '/statistics', label: '里程查询', icon: IconBarChartHStroked },
{ to: '/alerts', label: '告警中心', icon: IconAlarm },
{ to: '/access', label: '接入管理', icon: IconBox }
];
const pageNames: Record<string, string> = {
monitor: '全局监控',
vehicles: '车辆查询',
tracks: '轨迹回放',
history: '历史数据',
statistics: '里程查询',
alerts: '告警中心',
access: '接入管理',
operations: '运维质量'
};
export function AppShell() {
const location = useLocation();
const [initialPathname] = useState(() => location.pathname);
const section = location.pathname.split('/')[1] || 'monitor';
const { session, logout } = usePlatformSession();
const roleLabel = { viewer: '只读', operator: '处置员', admin: '管理员' }[session.role];
useEffect(() => scheduleIdleRoutePreloads({ activePathname: initialPathname }), [initialPathname]);
return (
<div className="v2-shell">
<Sidebar />
<div className="v2-main">
<header className="v2-topbar">
<h1>{pageNames[section] ?? '车辆数据中台'}</h1>
<div className="v2-topbar-actions">
<button type="button" aria-label="帮助"><IconHelpCircle /></button>
<span className="v2-current-user"><IconUser /><b>{session.name}</b><small>{roleLabel}</small></span>
<button type="button" aria-label="退出登录" title="退出登录" onClick={logout}><IconExit /></button>
</div>
</header>
<main className="v2-content"><Outlet /></main>
</div>
</div>
);
}
function Sidebar() {
const [collapsed, setCollapsed] = useState(false);
const warmRoute = (path: string) => { void preloadRoute(path); };
return (
<aside className={`v2-sidebar${collapsed ? ' is-collapsed' : ''}`}>
<div className="v2-brand">
<span className="v2-brand-mark"><IconBox size="large" /></span>
<strong></strong>
</div>
<nav className="v2-navigation" aria-label="主导航">
{navigation.map(({ to, label, icon: Icon }) => (
<NavLink key={to} to={to} aria-label={label} title={collapsed ? label : undefined} onPointerEnter={() => warmRoute(to)} onFocus={() => warmRoute(to)} onPointerDown={() => warmRoute(to)} className={({ isActive }) => `v2-nav-item ${isActive ? 'is-active' : ''}`}>
<Icon size="large" />
<span className="v2-nav-label">{label}</span>
</NavLink>
))}
</nav>
<NavLink to="/operations" aria-label="运维质量" title={collapsed ? '运维质量' : undefined} onPointerEnter={() => warmRoute('/operations')} onFocus={() => warmRoute('/operations')} onPointerDown={() => warmRoute('/operations')} className={({ isActive }) => `v2-nav-item v2-nav-operations ${isActive ? 'is-active' : ''}`}>
<IconSetting size="large" />
<span className="v2-nav-label"></span>
</NavLink>
<button className="v2-collapse" type="button" onClick={() => setCollapsed((value) => !value)} aria-label={collapsed ? '展开侧栏' : '收起侧栏'}>
<IconChevronLeft />
<span></span>
</button>
</aside>
);
}