feat(web): refine Semi UI application shell

This commit is contained in:
lingniu
2026-07-20 01:49:16 +08:00
parent 275d453954
commit 6e9a170fbf
5 changed files with 168 additions and 52 deletions

View File

@@ -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';

View File

@@ -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,6 +52,7 @@ function ProtectedRoute({ menu, children }: { menu: string; children: ReactNode
export function AppV2() {
return (
<ConfigProvider locale={zhCN} timeZone="Asia/Shanghai" direction="ltr">
<QueryClientProvider client={queryClient}>
<PlatformReadySignal />
<PlatformErrorBoundary>
@@ -72,6 +75,7 @@ export function AppV2() {
</BrowserRouter></AuthGate>
</PlatformErrorBoundary>
</QueryClientProvider>
</ConfigProvider>
);
}

View File

@@ -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();

View File

@@ -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,12 +356,17 @@ 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 }) => ({
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,
@@ -369,12 +376,13 @@ function Sidebar({ activePath }: { activePath: string }) {
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} />

View File

@@ -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;