fix(web): activate contextual help and filter status

This commit is contained in:
lingniu
2026-07-16 07:33:55 +08:00
parent d1764b3d66
commit 25e8427882
5 changed files with 66 additions and 3 deletions

View File

@@ -41,3 +41,19 @@ test('reschedules likely route preloads when the active module changes', async (
cleanup();
expect(secondCleanup).toHaveBeenCalledTimes(1);
});
test('opens contextual help for the active module and closes it without navigating', () => {
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
const help = screen.getByRole('button', { name: '帮助' });
expect(help).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(help);
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('筛选会自动生效');
expect(help).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByRole('button', { name: '关闭帮助' }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.getByText('页面内容')).toBeInTheDocument();
});

View File

@@ -37,11 +37,40 @@ const pageNames: Record<string, string> = {
operations: '运维质量'
};
const pageHelp: Record<string, { summary: string; tips: string[] }> = {
monitor: { summary: '查看全车队实时位置、状态和最新上报。', tips: ['车牌、协议和状态筛选会自动生效,无需再次提交。', '拖动地图会暂停选中车辆跟随,点击“跟随车辆”可恢复。', '文字地址按需解析,避免实时刷新持续消耗地图 API。'] },
vehicles: { summary: '按车牌或 VIN 查询车辆档案和实时遥测。', tips: ['先搜索车辆,再进入详情查看接入来源与最新数据。', '实时遥测按固定周期更新,离开页面后会自动停止。'] },
tracks: { summary: '按车辆和时间范围回放历史轨迹。', tips: ['先提交查询条件,再使用播放轴定位具体时刻。', '手动拖动地图会暂停跟随,避免操作与播放动画冲突。'] },
history: { summary: '查询车辆原始历史数据并导出证据。', tips: ['最多同时查询 5 台车辆,缩小时间范围可提高响应速度。', '导出任务在后台执行,完成前可继续使用其他页面。'] },
statistics: { summary: '按日期区间比较车辆每日里程与总里程。', tips: ['未选择车牌时按车队分页展示。', '可配置 JT808、GB32960、YUTONG_MQTT 的启用状态与优先级。'] },
alerts: { summary: '查看、确认和关闭车辆业务告警。', tips: ['筛选条件会限定列表和统计口径。', '处置前请核对证据与版本,避免覆盖其他人员的操作。'] },
access: { summary: '核对车辆接入覆盖、身份差异和协议质量。', tips: ['差异列表是主要工作区,可从统计卡片快速下钻。', '阈值配置仅对有权限的账号开放。'] },
operations: { summary: '查看数据源、查询链路和服务健康状态。', tips: ['优先处理红色异常,再检查数据新鲜度和来源就绪状态。', '页面会自动刷新,也可以手动触发即时检查。'] }
};
function ContextHelp({ section, onClose }: { section: string; 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>
<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>;
}
export function AppShell() {
const location = useLocation();
const section = location.pathname.split('/')[1] || 'monitor';
const activeRoutePath = `/${section}`;
const { session, logout } = usePlatformSession();
const [helpOpen, setHelpOpen] = useState(false);
const roleLabel = { viewer: '只读', operator: '处置员', admin: '管理员' }[session.role];
useEffect(() => scheduleIdleRoutePreloads({ activePathname: activeRoutePath }), [activeRoutePath]);
@@ -52,13 +81,14 @@ export function AppShell() {
<header className="v2-topbar">
<h1>{pageNames[section] ?? '车辆数据中台'}</h1>
<div className="v2-topbar-actions">
<button type="button" aria-label="帮助"><IconHelpCircle /></button>
<button type="button" aria-label="帮助" aria-expanded={helpOpen} aria-controls="v2-context-help" onClick={() => setHelpOpen(true)}><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>
{helpOpen ? <div id="v2-context-help"><ContextHelp section={section} onClose={() => setHelpOpen(false)} /></div> : null}
</div>
);
}