feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -2,6 +2,7 @@ import {
IconAlarm,
IconBarChartHStroked,
IconBox,
IconChevronDown,
IconChevronLeft,
IconHelpCircle,
IconHome,
@@ -12,13 +13,19 @@ import {
IconUser,
IconExit
} from '@douyinfe/semi-icons';
import { FormEvent, useEffect, useState } from 'react';
import { NavLink, Outlet, useLocation } from 'react-router-dom';
import { Avatar, Button, Dropdown, Input, Layout, Modal, Nav, SideSheet, Tag, Typography } from '@douyinfe/semi-ui';
import { FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { api } from '../../api/client';
import { usePlatformSession } from '../auth/AuthGate';
import { hasMenu } from '../auth/session';
import { useMobileLayout } from '../hooks/useMobileLayout';
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
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 },
@@ -54,21 +61,17 @@ const pageHelp: Record<string, { summary: string; tips: string[] }> = {
users: { summary: '创建客户账号并分配菜单和车辆数据范围。', tips: ['客户只能使用四个对外菜单中的已分配项。', '停用账号或重置密码会立即撤销其旧会话。', '车辆权限修改最多在 30 秒内对活跃会话生效。'] }
};
function ContextHelp({ section, onClose }: { section: string; onClose: () => void }) {
function ContextHelp({ section, visible, onClose }: { section: string; visible: boolean; onClose: () => void }) {
const content = pageHelp[section] ?? { summary: '查看当前模块的操作说明。', tips: ['通过左侧导航切换模块,页面状态会在当前任务中保留。'] };
useEffect(() => {
const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); };
window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape);
}, [onClose]);
return <div className="v2-help-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) onClose(); }}>
<aside className="v2-help-panel" role="dialog" aria-modal="true" aria-labelledby="v2-help-title">
<header><div><small></small><h2 id="v2-help-title">{pageNames[section] ?? '车辆数据中台'}</h2></div><button type="button" autoFocus aria-label="关闭帮助" onClick={onClose}><span aria-hidden="true">×</span></button></header>
<p>{content.summary}</p>
const title = pageNames[section] ?? '车辆数据中台';
useSideSheetA11y(visible, '.v2-help-sheet', 'v2-context-help', title, '关闭帮助');
return <SideSheet className="v2-help-sheet" visible={visible} aria-label={title} width={410} title={<div><Text type="tertiary" size="small"></Text><Title heading={4}>{title}</Title></div>} onCancel={onClose} footer={<Button block onClick={onClose}></Button>}>
<div className="v2-help-panel">
<Text type="secondary">{content.summary}</Text>
<ol>{content.tips.map((tip, index) => <li key={tip}><span>{index + 1}</span>{tip}</li>)}</ol>
<footer><kbd>Esc</kbd><span></span></footer>
</aside>
</div>;
</div>
</SideSheet>;
}
export function AppShell() {
@@ -77,36 +80,78 @@ export function AppShell() {
const activeRoutePath = `/${section}`;
const { session, logout } = usePlatformSession();
const [helpOpen, setHelpOpen] = useState(false);
const [accountOpen, setAccountOpen] = useState(false);
const [passwordOpen, setPasswordOpen] = useState(false);
const [mobileLayout, setMobileLayout] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 680px)').matches);
const mobileLayout = useMobileLayout();
const roleLabel = { viewer: '只读', operator: '处置员', admin: '管理员', customer: '客户' }[session.role];
useEffect(() => scheduleIdleRoutePreloads({ activePathname: activeRoutePath }), [activeRoutePath]);
useEffect(() => {
const media = window.matchMedia('(max-width: 680px)');
const update = () => setMobileLayout(media.matches);
media.addEventListener('change', update);
update();
return () => media.removeEventListener('change', update);
}, []);
useLayoutEffect(() => {
const content = document.querySelector<HTMLElement>('.v2-content');
if (!content) return;
const reset = () => {
content.scrollTop = 0;
content.scrollLeft = 0;
content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
};
reset();
let trailingFrame = 0;
const frame = window.requestAnimationFrame(() => {
reset();
trailingFrame = window.requestAnimationFrame(reset);
});
const timer = window.setTimeout(reset, 120);
return () => {
window.cancelAnimationFrame(frame);
window.cancelAnimationFrame(trailingFrame);
window.clearTimeout(timer);
};
}, [location.pathname]);
return (
<div className="v2-shell">
{mobileLayout ? <MobileNavigation /> : <Sidebar />}
<div className="v2-main">
<header className="v2-topbar">
<h1>{pageNames[section] ?? '车辆数据中台'}</h1>
<Layout className="v2-shell">
{mobileLayout ? <MobileNavigation /> : <Sidebar activePath={activeRoutePath} />}
<Layout className="v2-main">
<Header className="v2-topbar">
<div className="v2-topbar-title"><Text type="tertiary" size="small"></Text><Title heading={4}>{pageNames[section] ?? '车辆数据中台'}</Title></div>
<div className="v2-topbar-actions">
<button type="button" aria-label="帮助" aria-expanded={helpOpen} aria-controls="v2-context-help" onClick={() => setHelpOpen(true)}><IconHelpCircle /></button>
<button type="button" className="v2-current-user" title="修改密码" onClick={() => setPasswordOpen(true)}><IconUser /><b>{session.name}</b><small>{roleLabel}</small></button>
<button type="button" className="v2-password-mobile" aria-label="修改密码" onClick={() => setPasswordOpen(true)}><IconUser /></button>
<button type="button" aria-label="退出登录" title="退出登录" onClick={logout}><IconExit /></button>
<Button theme="borderless" icon={<IconHelpCircle />} aria-label="帮助" aria-expanded={helpOpen} aria-controls="v2-context-help" onClick={() => setHelpOpen(true)} />
<Dropdown
trigger="click"
position="bottomRight"
visible={accountOpen}
onVisibleChange={setAccountOpen}
contentClassName="v2-account-dropdown"
render={<Dropdown.Menu>
<Dropdown.Title className="v2-account-dropdown-profile">
<Avatar size="small" color="blue">{session.name.slice(0, 1)}</Avatar>
<span><strong>{session.name}</strong><small>{session.username ? `@${session.username}` : session.authProvider === 'legacy-token' ? '运维令牌账号' : '平台账号'}</small></span>
<Tag color="blue" size="small">{roleLabel}</Tag>
</Dropdown.Title>
<Dropdown.Divider />
<Dropdown.Item icon={<IconUser />} onClick={() => { setAccountOpen(false); setPasswordOpen(true); }}></Dropdown.Item>
<Dropdown.Item type="danger" icon={<IconExit />} onClick={() => { setAccountOpen(false); logout(); }}>退</Dropdown.Item>
</Dropdown.Menu>}
>
<Button
theme="borderless"
className="v2-current-user"
aria-label={`账号菜单,${session.name}`}
aria-expanded={accountOpen}
aria-haspopup="menu"
>
<Avatar size="extra-small" color="blue">{session.name.slice(0, 1)}</Avatar>
<b>{session.name}</b>
<Tag color="blue" size="small">{roleLabel}</Tag>
<IconChevronDown className="v2-account-chevron" />
</Button>
</Dropdown>
</div>
</header>
<main className="v2-content"><Outlet /></main>
</div>
{helpOpen ? <div id="v2-context-help"><ContextHelp section={section} onClose={() => setHelpOpen(false)} /></div> : null}
</Header>
<Content className={`v2-content${section === 'tracks' ? ' is-track-workspace' : ''}`}><Outlet /></Content>
</Layout>
<ContextHelp section={section} visible={helpOpen} onClose={() => setHelpOpen(false)} />
{passwordOpen ? <PasswordDialog onClose={() => setPasswordOpen(false)} onChanged={logout} /> : null}
</div>
</Layout>
);
}
@@ -127,19 +172,16 @@ function PasswordDialog({ onClose, onChanged }: { onClose: () => void; onChanged
setError(reason instanceof Error ? reason.message : '密码修改失败');
} finally { setPending(false); }
};
useEffect(() => {
const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && !pending) onClose(); };
window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape);
}, [onClose, pending]);
return <div className="v2-password-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget && !pending) onClose(); }}><form className="v2-password-dialog" role="dialog" aria-modal="true" aria-labelledby="v2-password-title" onSubmit={submit}>
<header><div><h2 id="v2-password-title"></h2><p></p></div><button type="button" aria-label="关闭修改密码" onClick={onClose}>×</button></header>
<label><span></span><input autoFocus required type="password" autoComplete="current-password" value={currentPassword} onChange={(event) => setCurrentPassword(event.target.value)} /></label>
<label><span></span><input required minLength={10} type="password" autoComplete="new-password" value={newPassword} onChange={(event) => setNewPassword(event.target.value)} placeholder="至少 10 位,包含三类字符" /></label>
<label><span></span><input required minLength={10} type="password" autoComplete="new-password" value={confirmPassword} onChange={(event) => setConfirmPassword(event.target.value)} /></label>
{error ? <em role="alert">{error}</em> : null}
<footer><button type="button" onClick={onClose} disabled={pending}></button><button type="submit" disabled={pending || !currentPassword || !newPassword || !confirmPassword}>{pending ? '正在修改…' : '确认修改'}</button></footer>
</form></div>;
return <Modal className="v2-password-modal" visible title="修改登录密码" aria-label="修改登录密码" onCancel={onClose} closeOnEsc={!pending} maskClosable={!pending} footer={null}>
<form className="v2-password-dialog" onSubmit={submit}>
<Text type="secondary"></Text>
<label><span></span><Input autoFocus required type="password" autoComplete="current-password" value={currentPassword} onChange={setCurrentPassword} size="large" /></label>
<label><span></span><Input required minLength={10} type="password" autoComplete="new-password" value={newPassword} onChange={setNewPassword} placeholder="至少 10 位,包含三类字符" size="large" /></label>
<label><span></span><Input required minLength={10} type="password" autoComplete="new-password" value={confirmPassword} onChange={setConfirmPassword} size="large" /></label>
{error ? <Text type="danger" role="alert">{error}</Text> : null}
<footer><Button type="tertiary" onClick={onClose} disabled={pending}></Button><Button htmlType="submit" theme="solid" type="primary" loading={pending} disabled={pending || !currentPassword || !newPassword || !confirmPassword}>{pending ? '正在修改…' : '确认修改'}</Button></footer>
</form>
</Modal>;
}
const mobilePrimaryNavigation = [navigation[0], navigation[1], navigation[2], navigation[4]];
@@ -154,51 +196,55 @@ function MobileNavigation() {
const moreActive = more.some((item) => location.pathname.startsWith(item.to));
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
useEffect(() => setMoreOpen(false), [location.pathname]);
useEffect(() => {
if (!moreOpen) return;
const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setMoreOpen(false); };
window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape);
}, [moreOpen]);
useSideSheetA11y(moreOpen, '.v2-mobile-more-sidesheet', 'v2-mobile-more', '更多功能', '关闭更多功能');
const link = ({ to, label, icon: Icon }: (typeof navigation)[number]) => <NavLink key={to} to={to} aria-label={label} onPointerDown={() => warmRoute(to)} className={({ isActive }) => `v2-mobile-nav-item ${isActive ? 'is-active' : ''}`}><Icon size="large" /><span>{label}</span></NavLink>;
return <>
{moreOpen && more.length ? <div className="v2-mobile-more-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) setMoreOpen(false); }}><section className="v2-mobile-more-sheet" role="dialog" aria-modal="true" aria-label="更多功能"><header><div><strong></strong><span></span></div><button type="button" aria-label="关闭更多功能" onClick={() => setMoreOpen(false)}>×</button></header><nav>{more.map(link)}</nav></section></div> : null}
{more.length ? <SideSheet className="v2-mobile-more-sidesheet" visible={moreOpen} placement="bottom" height="auto" title={<div><strong></strong><span></span></div>} aria-label="更多功能" footer={null} onCancel={() => setMoreOpen(false)}><nav>{more.map(link)}</nav></SideSheet> : null}
<nav className="v2-mobile-navigation" aria-label="主导航">
{primary.map(link)}
{more.length ? <button type="button" className={`v2-mobile-nav-item${moreActive ? ' is-active' : ''}`} aria-label="更多功能" aria-expanded={moreOpen} onClick={() => setMoreOpen((value) => !value)}><IconMore size="large" /><span></span></button> : null}
{more.length ? <Button theme="borderless" className={`v2-mobile-nav-item${moreActive ? ' is-active' : ''}`} aria-label="更多功能" aria-expanded={moreOpen} aria-controls="v2-mobile-more" icon={<IconMore size="large" />} onClick={() => setMoreOpen((value) => !value)}><span></span></Button> : null}
</nav>
</>;
}
function Sidebar() {
function Sidebar({ activePath }: { activePath: string }) {
const { session } = usePlatformSession();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
const compactLayout = useMobileLayout(900);
const effectiveCollapsed = collapsed || compactLayout;
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
const visibleNavigation = navigation.filter((item) => hasMenu(session, item.menu));
const visibleNavigation = [
...navigation.filter((item) => hasMenu(session, item.menu)),
...(hasMenu(session, 'operations') ? [{ to: '/operations', menu: 'operations', label: '运维质量', icon: IconSetting }] : [])
];
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)
}
}));
return (
<aside className={`v2-sidebar${collapsed ? ' is-collapsed' : ''}`}>
<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="灵牛智能" />
<img className="v2-brand-symbol" src="/brand-mark.svg" alt="" aria-hidden="true" />
</div>
<nav className="v2-navigation" aria-label="主导航">
{visibleNavigation.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>
{hasMenu(session, 'operations') ? <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> : null}
<button className="v2-collapse" type="button" onClick={() => setCollapsed((value) => !value)} aria-label={collapsed ? '展开侧栏' : '收起侧栏'}>
<Nav className="v2-navigation" aria-label="主导航" items={items} selectedKeys={[activePath]} isCollapsed={effectiveCollapsed} tooltipShowDelay={300} tooltipHideDelay={300} />
{!compactLayout ? <Button className="v2-collapse" theme="borderless" onClick={() => setCollapsed((value) => !value)} aria-label={collapsed ? '展开侧栏' : '收起侧栏'}>
<IconChevronLeft />
<span></span>
</button>
</aside>
</Button> : null}
</Sider>
);
}