feat(web): refine Semi UI application shell
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const readyEvent = 'vehicle-platform:ready';
|
const readyEvent = 'vehicle-platform:ready';
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
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 { useEffect, type ReactNode } from 'react';
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { AppShell } from './layout/AppShell';
|
import { AppShell } from './layout/AppShell';
|
||||||
@@ -50,6 +52,7 @@ function ProtectedRoute({ menu, children }: { menu: string; children: ReactNode
|
|||||||
|
|
||||||
export function AppV2() {
|
export function AppV2() {
|
||||||
return (
|
return (
|
||||||
|
<ConfigProvider locale={zhCN} timeZone="Asia/Shanghai" direction="ltr">
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<PlatformReadySignal />
|
<PlatformReadySignal />
|
||||||
<PlatformErrorBoundary>
|
<PlatformErrorBoundary>
|
||||||
@@ -72,6 +75,7 @@ export function AppV2() {
|
|||||||
</BrowserRouter></AuthGate>
|
</BrowserRouter></AuthGate>
|
||||||
</PlatformErrorBoundary>
|
</PlatformErrorBoundary>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ test('renders only explicitly assigned customer menus', () => {
|
|||||||
expect(screen.queryByRole('link', { name: '运维质量' })).not.toBeInTheDocument();
|
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 () => {
|
test('reschedules likely route preloads when the active module changes', async () => {
|
||||||
const firstCleanup = vi.fn();
|
const firstCleanup = vi.fn();
|
||||||
const secondCleanup = vi.fn();
|
const secondCleanup = vi.fn();
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
IconAlarm,
|
IconAlarm,
|
||||||
IconBarChartHStroked,
|
IconBarChartVStroked,
|
||||||
IconBox,
|
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronLeft,
|
IconChevronLeft,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
|
IconConnectionPoint1,
|
||||||
IconHelpCircle,
|
IconHelpCircle,
|
||||||
|
IconHistory,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconMapPin,
|
|
||||||
IconMore,
|
IconMore,
|
||||||
|
IconRoute,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSetting,
|
IconSetting,
|
||||||
IconTickCircle,
|
IconTickCircle,
|
||||||
IconUser,
|
IconUser,
|
||||||
|
IconUserGroup,
|
||||||
IconExit
|
IconExit
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Avatar, Banner, Button, Card, Dropdown, Layout, Nav, Tag, Typography } from '@douyinfe/semi-ui';
|
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 { Text, Title } = Typography;
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ to: '/monitor', menu: 'monitor', label: '全局监控', icon: IconHome },
|
{ to: '/monitor', menu: 'monitor', label: '全局监控', icon: IconHome, group: 'workspace' },
|
||||||
{ to: '/vehicles', menu: 'vehicles', label: '车辆查询', icon: IconSearch },
|
{ to: '/vehicles', menu: 'vehicles', label: '车辆查询', icon: IconSearch, group: 'workspace' },
|
||||||
{ to: '/tracks', menu: 'tracks', label: '轨迹回放', icon: IconMapPin },
|
{ to: '/tracks', menu: 'tracks', label: '轨迹回放', icon: IconRoute, group: 'workspace' },
|
||||||
{ to: '/history', menu: 'history', label: '历史数据', icon: IconBarChartHStroked },
|
{ to: '/history', menu: 'history', label: '历史数据', icon: IconHistory, group: 'workspace' },
|
||||||
{ to: '/statistics', menu: 'statistics', label: '里程查询', icon: IconBarChartHStroked },
|
{ to: '/statistics', menu: 'statistics', label: '里程查询', icon: IconBarChartVStroked, group: 'workspace' },
|
||||||
{ to: '/alerts', menu: 'alerts', label: '告警中心', icon: IconAlarm },
|
{ to: '/alerts', menu: 'alerts', label: '告警中心', icon: IconAlarm, group: 'governance' },
|
||||||
{ to: '/access', menu: 'access', label: '接入管理', icon: IconBox },
|
{ to: '/access', menu: 'access', label: '接入管理', icon: IconConnectionPoint1, group: 'governance' },
|
||||||
{ to: '/users', menu: 'users', label: '账号管理', icon: IconUser }
|
{ to: '/users', menu: 'users', label: '账号管理', icon: IconUserGroup, group: 'governance' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const pageNames: Record<string, string> = {
|
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 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 = [
|
const mobileMoreGroups = [
|
||||||
{
|
{
|
||||||
key: 'insight',
|
key: 'insight',
|
||||||
@@ -354,12 +356,17 @@ function Sidebar({ activePath }: { activePath: string }) {
|
|||||||
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
||||||
const visibleNavigation = [
|
const visibleNavigation = [
|
||||||
...navigation.filter((item) => hasMenu(session, item.menu)),
|
...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,
|
itemKey: to,
|
||||||
text: label,
|
text: label,
|
||||||
icon: <Icon size="large" />,
|
icon: <Icon size="large" />,
|
||||||
|
className: startsGroup ? `v2-nav-group-start is-${group}` : undefined,
|
||||||
link: to,
|
link: to,
|
||||||
linkOptions: {
|
linkOptions: {
|
||||||
'aria-label': label,
|
'aria-label': label,
|
||||||
@@ -369,12 +376,13 @@ function Sidebar({ activePath }: { activePath: string }) {
|
|||||||
onFocus: () => warmRoute(to),
|
onFocus: () => warmRoute(to),
|
||||||
onPointerDown: () => warmRoute(to)
|
onPointerDown: () => warmRoute(to)
|
||||||
}
|
}
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider className={`v2-sidebar${effectiveCollapsed ? ' is-collapsed' : ''}`} aria-label="主导航">
|
<Sider className={`v2-sidebar${effectiveCollapsed ? ' is-collapsed' : ''}`} aria-label="主导航">
|
||||||
<div className="v2-brand" aria-label="灵牛智能车辆数据中台">
|
<div className="v2-brand" aria-label="羚牛智能车辆数据中台">
|
||||||
<img className="v2-brand-logo" src="/brand-logo.svg" alt="灵牛智能" />
|
<img className="v2-brand-logo" src="/brand-logo.svg" alt="羚牛智能" />
|
||||||
<img className="v2-brand-symbol" src="/brand-mark.svg" alt="" aria-hidden="true" />
|
<img className="v2-brand-symbol" src="/brand-mark.svg" alt="" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<Nav className="v2-navigation" aria-label="主导航" items={items} selectedKeys={[activePath]} isCollapsed={effectiveCollapsed} tooltipShowDelay={300} tooltipHideDelay={300} />
|
<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) {
|
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
|
||||||
.v2-mobile-filter-sheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
.v2-mobile-filter-sheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
height: 100dvh !important;
|
height: 100dvh !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user