feat(web): refine Semi UI application shell
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="platform-boot" role="status" aria-live="polite"><div><img class="platform-boot-logo" src="/brand-logo.svg" alt="灵牛智能" /><strong>车辆数据中台</strong><small>正在加载工作台…</small></div></div></div>
|
||||
<div id="root"><div id="platform-boot" role="status" aria-live="polite"><div><img class="platform-boot-logo" src="/brand-logo.svg" alt="羚牛智能" /><strong>车辆数据中台</strong><small>正在加载工作台…</small></div></div></div>
|
||||
<script>
|
||||
(() => {
|
||||
const readyEvent = 'vehicle-platform:ready';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ConfigProvider } from '@douyinfe/semi-ui';
|
||||
import zhCN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN';
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { AppShell } from './layout/AppShell';
|
||||
@@ -50,28 +52,30 @@ function ProtectedRoute({ menu, children }: { menu: string; children: ReactNode
|
||||
|
||||
export function AppV2() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlatformReadySignal />
|
||||
<PlatformErrorBoundary>
|
||||
<AuthGate><BrowserRouter future={ROUTER_FUTURE}>
|
||||
<Routes>
|
||||
<Route element={<AppShell />}>
|
||||
<Route index element={<SessionIndex />} />
|
||||
<Route path="/monitor" element={<ProtectedRoute menu="monitor"><RoutePage page={RoutePages.Monitor} recreatePage={RoutePageFactories.Monitor} label="全局监控" /></ProtectedRoute>} />
|
||||
<Route path="/vehicles/:vin?" element={<ProtectedRoute menu="vehicles"><RoutePage page={RoutePages.Vehicles} recreatePage={RoutePageFactories.Vehicles} label="车辆查询" /></ProtectedRoute>} />
|
||||
<Route path="/tracks" element={<ProtectedRoute menu="tracks"><RoutePage page={RoutePages.Tracks} recreatePage={RoutePageFactories.Tracks} label="轨迹回放" /></ProtectedRoute>} />
|
||||
<Route path="/history" element={<ProtectedRoute menu="history"><RoutePage page={RoutePages.History} recreatePage={RoutePageFactories.History} label="历史数据" /></ProtectedRoute>} />
|
||||
<Route path="/statistics" element={<ProtectedRoute menu="statistics"><RoutePage page={RoutePages.Statistics} recreatePage={RoutePageFactories.Statistics} label="里程查询" /></ProtectedRoute>} />
|
||||
<Route path="/access" element={<ProtectedRoute menu="access"><RoutePage page={RoutePages.Access} recreatePage={RoutePageFactories.Access} label="接入管理" /></ProtectedRoute>} />
|
||||
<Route path="/alerts/*" element={<ProtectedRoute menu="alerts"><RoutePage page={RoutePages.Alerts} recreatePage={RoutePageFactories.Alerts} label="告警中心" /></ProtectedRoute>} />
|
||||
<Route path="/operations" element={<ProtectedRoute menu="operations"><RoutePage page={RoutePages.Operations} recreatePage={RoutePageFactories.Operations} label="运维质量" /></ProtectedRoute>} />
|
||||
<Route path="/users" element={<ProtectedRoute menu="users"><RoutePage page={RoutePages.Users} recreatePage={RoutePageFactories.Users} label="账号管理" /></ProtectedRoute>} />
|
||||
<Route path="*" element={<Navigate to="/monitor" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter></AuthGate>
|
||||
</PlatformErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
<ConfigProvider locale={zhCN} timeZone="Asia/Shanghai" direction="ltr">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlatformReadySignal />
|
||||
<PlatformErrorBoundary>
|
||||
<AuthGate><BrowserRouter future={ROUTER_FUTURE}>
|
||||
<Routes>
|
||||
<Route element={<AppShell />}>
|
||||
<Route index element={<SessionIndex />} />
|
||||
<Route path="/monitor" element={<ProtectedRoute menu="monitor"><RoutePage page={RoutePages.Monitor} recreatePage={RoutePageFactories.Monitor} label="全局监控" /></ProtectedRoute>} />
|
||||
<Route path="/vehicles/:vin?" element={<ProtectedRoute menu="vehicles"><RoutePage page={RoutePages.Vehicles} recreatePage={RoutePageFactories.Vehicles} label="车辆查询" /></ProtectedRoute>} />
|
||||
<Route path="/tracks" element={<ProtectedRoute menu="tracks"><RoutePage page={RoutePages.Tracks} recreatePage={RoutePageFactories.Tracks} label="轨迹回放" /></ProtectedRoute>} />
|
||||
<Route path="/history" element={<ProtectedRoute menu="history"><RoutePage page={RoutePages.History} recreatePage={RoutePageFactories.History} label="历史数据" /></ProtectedRoute>} />
|
||||
<Route path="/statistics" element={<ProtectedRoute menu="statistics"><RoutePage page={RoutePages.Statistics} recreatePage={RoutePageFactories.Statistics} label="里程查询" /></ProtectedRoute>} />
|
||||
<Route path="/access" element={<ProtectedRoute menu="access"><RoutePage page={RoutePages.Access} recreatePage={RoutePageFactories.Access} label="接入管理" /></ProtectedRoute>} />
|
||||
<Route path="/alerts/*" element={<ProtectedRoute menu="alerts"><RoutePage page={RoutePages.Alerts} recreatePage={RoutePageFactories.Alerts} label="告警中心" /></ProtectedRoute>} />
|
||||
<Route path="/operations" element={<ProtectedRoute menu="operations"><RoutePage page={RoutePages.Operations} recreatePage={RoutePageFactories.Operations} label="运维质量" /></ProtectedRoute>} />
|
||||
<Route path="/users" element={<ProtectedRoute menu="users"><RoutePage page={RoutePages.Users} recreatePage={RoutePageFactories.Users} label="账号管理" /></ProtectedRoute>} />
|
||||
<Route path="*" element={<Navigate to="/monitor" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter></AuthGate>
|
||||
</PlatformErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,19 @@ test('renders only explicitly assigned customer menus', () => {
|
||||
expect(screen.queryByRole('link', { name: '运维质量' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('groups desktop workflows with distinct Semi navigation icons and the current brand name', () => {
|
||||
const view = render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/statistics']}>
|
||||
<Routes><Route element={<AppShell />}><Route path="*" element={<span>页面内容</span>} /></Route></Routes>
|
||||
</MemoryRouter>);
|
||||
|
||||
expect(screen.getByLabelText('羚牛智能车辆数据中台')).toContainElement(screen.getByAltText('羚牛智能'));
|
||||
expect(view.container.querySelector('.v2-nav-group-start.is-workspace')).toHaveTextContent('全局监控');
|
||||
expect(view.container.querySelector('.v2-nav-group-start.is-governance')).toHaveTextContent('告警中心');
|
||||
expect(screen.getByRole('link', { name: '轨迹回放' })?.querySelector('.semi-icon-route')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: '历史数据' })?.querySelector('.semi-icon-history')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: '里程查询' })?.querySelector('.semi-icon-bar_chart_v_stroked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('reschedules likely route preloads when the active module changes', async () => {
|
||||
const firstCleanup = vi.fn();
|
||||
const secondCleanup = vi.fn();
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import {
|
||||
IconAlarm,
|
||||
IconBarChartHStroked,
|
||||
IconBox,
|
||||
IconBarChartVStroked,
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconConnectionPoint1,
|
||||
IconHelpCircle,
|
||||
IconHistory,
|
||||
IconHome,
|
||||
IconLock,
|
||||
IconMapPin,
|
||||
IconMore,
|
||||
IconRoute,
|
||||
IconSearch,
|
||||
IconSetting,
|
||||
IconTickCircle,
|
||||
IconUser,
|
||||
IconUserGroup,
|
||||
IconExit
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Avatar, Banner, Button, Card, Dropdown, Layout, Nav, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
@@ -33,14 +35,14 @@ const { Header, Sider, Content } = Layout;
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const navigation = [
|
||||
{ to: '/monitor', menu: 'monitor', label: '全局监控', icon: IconHome },
|
||||
{ to: '/vehicles', menu: 'vehicles', label: '车辆查询', icon: IconSearch },
|
||||
{ to: '/tracks', menu: 'tracks', label: '轨迹回放', icon: IconMapPin },
|
||||
{ to: '/history', menu: 'history', label: '历史数据', icon: IconBarChartHStroked },
|
||||
{ to: '/statistics', menu: 'statistics', label: '里程查询', icon: IconBarChartHStroked },
|
||||
{ to: '/alerts', menu: 'alerts', label: '告警中心', icon: IconAlarm },
|
||||
{ to: '/access', menu: 'access', label: '接入管理', icon: IconBox },
|
||||
{ to: '/users', menu: 'users', label: '账号管理', icon: IconUser }
|
||||
{ to: '/monitor', menu: 'monitor', label: '全局监控', icon: IconHome, group: 'workspace' },
|
||||
{ to: '/vehicles', menu: 'vehicles', label: '车辆查询', icon: IconSearch, group: 'workspace' },
|
||||
{ to: '/tracks', menu: 'tracks', label: '轨迹回放', icon: IconRoute, group: 'workspace' },
|
||||
{ to: '/history', menu: 'history', label: '历史数据', icon: IconHistory, group: 'workspace' },
|
||||
{ to: '/statistics', menu: 'statistics', label: '里程查询', icon: IconBarChartVStroked, group: 'workspace' },
|
||||
{ to: '/alerts', menu: 'alerts', label: '告警中心', icon: IconAlarm, group: 'governance' },
|
||||
{ to: '/access', menu: 'access', label: '接入管理', icon: IconConnectionPoint1, group: 'governance' },
|
||||
{ to: '/users', menu: 'users', label: '账号管理', icon: IconUserGroup, group: 'governance' }
|
||||
];
|
||||
|
||||
const pageNames: Record<string, string> = {
|
||||
@@ -270,7 +272,7 @@ function PasswordDialog({ onClose, onChanged }: { onClose: () => void; onChanged
|
||||
}
|
||||
|
||||
const mobilePrimaryNavigation = [navigation[0], navigation[1], navigation[2], navigation[4]];
|
||||
const operationsNavigation = { to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting };
|
||||
const operationsNavigation = { to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting, group: 'governance' };
|
||||
const mobileMoreGroups = [
|
||||
{
|
||||
key: 'insight',
|
||||
@@ -354,27 +356,33 @@ function Sidebar({ activePath }: { activePath: string }) {
|
||||
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
||||
const visibleNavigation = [
|
||||
...navigation.filter((item) => hasMenu(session, item.menu)),
|
||||
...(hasMenu(session, 'operations') ? [{ to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting }] : [])
|
||||
...(hasMenu(session, 'operations') ? [operationsNavigation] : [])
|
||||
];
|
||||
const items = visibleNavigation.map(({ to, label, icon: Icon }) => ({
|
||||
itemKey: to,
|
||||
text: label,
|
||||
icon: <Icon size="large" />,
|
||||
link: to,
|
||||
linkOptions: {
|
||||
'aria-label': label,
|
||||
title: effectiveCollapsed ? label : undefined,
|
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); navigate(to); },
|
||||
onPointerEnter: () => warmRoute(to),
|
||||
onFocus: () => warmRoute(to),
|
||||
onPointerDown: () => warmRoute(to)
|
||||
}
|
||||
}));
|
||||
let previousGroup = '';
|
||||
const items = visibleNavigation.map(({ to, label, icon: Icon, group }) => {
|
||||
const startsGroup = group !== previousGroup;
|
||||
previousGroup = group;
|
||||
return {
|
||||
itemKey: to,
|
||||
text: label,
|
||||
icon: <Icon size="large" />,
|
||||
className: startsGroup ? `v2-nav-group-start is-${group}` : undefined,
|
||||
link: to,
|
||||
linkOptions: {
|
||||
'aria-label': label,
|
||||
title: effectiveCollapsed ? label : undefined,
|
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); navigate(to); },
|
||||
onPointerEnter: () => warmRoute(to),
|
||||
onFocus: () => warmRoute(to),
|
||||
onPointerDown: () => warmRoute(to)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Sider className={`v2-sidebar${effectiveCollapsed ? ' is-collapsed' : ''}`} aria-label="主导航">
|
||||
<div className="v2-brand" aria-label="灵牛智能车辆数据中台">
|
||||
<img className="v2-brand-logo" src="/brand-logo.svg" alt="灵牛智能" />
|
||||
<div className="v2-brand" aria-label="羚牛智能车辆数据中台">
|
||||
<img className="v2-brand-logo" src="/brand-logo.svg" alt="羚牛智能" />
|
||||
<img className="v2-brand-symbol" src="/brand-mark.svg" alt="" aria-hidden="true" />
|
||||
</div>
|
||||
<Nav className="v2-navigation" aria-label="主导航" items={items} selectedKeys={[activePath]} isCollapsed={effectiveCollapsed} tooltipShowDelay={300} tooltipHideDelay={300} />
|
||||
|
||||
@@ -165,6 +165,97 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Semi UI application navigation convergence.
|
||||
* Group the primary driving workflows separately from governance tools and
|
||||
* give every destination its own semantic icon without changing route order.
|
||||
*/
|
||||
.v2-navigation.semi-navigation {
|
||||
padding: 10px 9px 12px;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item {
|
||||
position: relative;
|
||||
transition: background-color .16s ease, color .16s ease, box-shadow .16s ease;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start {
|
||||
margin-top: 29px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -22px;
|
||||
right: 10px;
|
||||
left: 12px;
|
||||
overflow: hidden;
|
||||
color: #9aa5b4;
|
||||
content: "";
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
line-height: 16px;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start.is-workspace::before {
|
||||
content: "车辆工作台";
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start.is-governance::before {
|
||||
content: "平台治理";
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item-icon {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 30px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #f3f6fa;
|
||||
color: #758397;
|
||||
transition: background-color .16s ease, color .16s ease, box-shadow .16s ease;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item:hover {
|
||||
background: linear-gradient(90deg, #f7f9fc 0%, #fbfcfe 100%);
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item:hover .semi-navigation-item-icon {
|
||||
background: #eaf2fd;
|
||||
color: #3a6fae;
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item-selected {
|
||||
background: linear-gradient(90deg, #eaf3ff 0%, #f4f8ff 100%);
|
||||
box-shadow: inset 3px 0 var(--v2-blue), 0 3px 10px rgba(18, 104, 243, .045);
|
||||
}
|
||||
|
||||
.v2-navigation.semi-navigation .semi-navigation-item-selected .semi-navigation-item-icon {
|
||||
background: #fff;
|
||||
color: var(--v2-blue);
|
||||
box-shadow: 0 2px 8px rgba(18, 104, 243, .09), inset 0 0 0 1px rgba(18, 104, 243, .08);
|
||||
}
|
||||
|
||||
.v2-sidebar.is-collapsed .v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.v2-sidebar.is-collapsed .v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v2-sidebar.is-collapsed .v2-navigation.semi-navigation .semi-navigation-item-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
|
||||
.v2-mobile-filter-sheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||
height: 100dvh !important;
|
||||
|
||||
Reference in New Issue
Block a user