diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx index 6a12d492..4bdfcd04 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx @@ -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( + }>页面内容} /> + ); + + 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(); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx index 6e52a789..7c5d8ea6 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx @@ -37,11 +37,40 @@ const pageNames: Record = { operations: '运维质量' }; +const pageHelp: Record = { + 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
{ if (event.target === event.currentTarget) onClose(); }}> + +
; +} + 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() {

{pageNames[section] ?? '车辆数据中台'}

- + {session.name}{roleLabel}
+ {helpOpen ?
setHelpOpen(false)} />
: null} ); } diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index e1f2ddbc..41696da2 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -93,6 +93,8 @@ test('starts without a selection and supports expand, collapse, reselection, and expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument(); expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', ''); expect(screen.getByText('有实时位置')).toBeInTheDocument(); + expect(screen.getByRole('status', { name: '搜索和筛选条件修改后自动生效' })).toHaveTextContent('实时筛选'); + expect(screen.queryByRole('button', { name: '筛选' })).not.toBeInTheDocument(); expect(screen.queryByText('接入车辆')).not.toBeInTheDocument(); fireEvent.click(firstVehicle); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index 68e11d24..37dcca7e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -310,7 +310,7 @@ export default function MonitorPage() { {statuses.map((item) => )} - + 实时筛选
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index 2a67af4a..d3096295 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -44,6 +44,18 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-topbar-actions { display: flex; align-items: center; gap: 4px; } .v2-topbar-actions button { display: grid; width: 34px; height: 34px; place-items: center; border: 0; border-radius: 8px; background: transparent; color: #64748b; cursor: pointer; } .v2-topbar-actions button:hover { background: #f1f5f9; } +.v2-help-backdrop { position: fixed; z-index: 1200; inset: 0; background: rgba(15,23,42,.12); } +.v2-help-panel { position: absolute; top: 58px; right: 18px; width: min(390px, calc(100vw - 28px)); border: 1px solid rgba(255,255,255,.9); border-radius: 14px; background: #fff; padding: 18px; box-shadow: 0 24px 70px rgba(15,23,42,.22); } +.v2-help-panel > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; } +.v2-help-panel > header small { color: var(--v2-blue); font-size: 10px; font-weight: 800; letter-spacing: .08em; } +.v2-help-panel h2 { margin: 4px 0 0; color: #17253a; font-size: 19px; } +.v2-help-panel > header button { display: grid; width: 32px; height: 32px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 8px; background: #f3f6fa; color: #627086; cursor: pointer; } +.v2-help-panel > p { margin: 15px 0; color: #59677c; font-size: 12px; line-height: 1.7; } +.v2-help-panel ol { display: grid; gap: 9px; margin: 0; padding: 0; list-style: none; } +.v2-help-panel li { display: grid; grid-template-columns: 24px minmax(0,1fr); align-items: start; gap: 9px; border-radius: 8px; background: #f7f9fc; padding: 10px; color: #435168; font-size: 11px; line-height: 1.65; } +.v2-help-panel li > span { display: grid; width: 22px; height: 22px; place-items: center; border-radius: 50%; background: var(--v2-blue-soft); color: var(--v2-blue); font-size: 10px; font-weight: 800; } +.v2-help-panel > footer { display: flex; align-items: center; gap: 6px; margin-top: 14px; color: #8996a8; font-size: 10px; } +.v2-help-panel kbd { border: 1px solid #dce4ef; border-radius: 4px; background: #fff; padding: 2px 5px; color: #59677c; font-family: inherit; } .v2-current-user { display: flex; align-items: center; gap: 6px; margin: 0 5px; color: #59677c; font-size: 11px; }.v2-current-user b { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.v2-current-user small, .v2-role-badge { border-radius: 10px; background: var(--v2-blue-soft); padding: 2px 6px; color: var(--v2-blue); font-size: 9px; } .v2-role-notice { margin: 6px 0; border-radius: 6px; background: #f6f8fb; padding: 8px; color: var(--v2-muted); font-size: 8px; line-height: 1.5; } .v2-content { min-width: 0; min-height: 0; flex: 1; overflow: auto; } @@ -81,6 +93,8 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-batch-search-dialog > footer button { min-width: 92px; } .v2-batch-search-dialog > footer button:disabled { opacity: .45; cursor: not-allowed; } .v2-filterbar select { height: 36px; border: 1px solid #dce4ef; border-radius: 7px; background: #fff; padding: 0 30px 0 11px; color: #4d5c70; outline: 0; font-size: 12px; } +.v2-filter-live-status { display: flex; height: 36px; align-items: center; justify-content: center; gap: 6px; border: 1px solid #d9e8fb; border-radius: 7px; background: #f5f9ff; padding: 0 11px; color: #3568a9; font-size: 11px; font-weight: 700; white-space: nowrap; } +.v2-filter-live-status svg { color: var(--v2-blue); } .v2-primary-button, .v2-secondary-button { display: flex; height: 36px; align-items: center; justify-content: center; gap: 7px; border-radius: 7px; padding: 0 14px; cursor: pointer; font-size: 12px; font-weight: 700; } .v2-primary-button { border: 1px solid var(--v2-blue); background: var(--v2-blue); color: #fff; box-shadow: 0 5px 12px rgba(18,104,243,.18); } .v2-secondary-button { border: 1px solid #dce4ef; background: #fff; color: #59677c; } @@ -1107,13 +1121,14 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-topbar h1 { font-size: 17px; } .v2-topbar-actions { gap: 0; } .v2-topbar-actions button { width: 40px; height: 40px; } + .v2-help-panel { top: 48px; right: 7px; padding: 16px; } .v2-current-user { display: none; } .v2-content { overscroll-behavior: contain; } .v2-monitor-page { height: auto; min-height: 100%; gap: 8px; overflow: visible; padding: 8px; } .v2-filterbar { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; padding: 9px; box-shadow: 0 4px 16px rgba(21,32,51,.04); } .v2-search-field { grid-column: 1 / -1; height: 44px; } .v2-search-field input, .v2-filterbar select { font-size: 16px; } - .v2-filterbar select, .v2-primary-button, .v2-secondary-button { height: 44px; } + .v2-filterbar select, .v2-primary-button, .v2-secondary-button, .v2-filter-live-status { height: 44px; } .v2-primary-button, .v2-secondary-button { padding: 0 10px; font-size: 12px; } .v2-monitor-mode { height: 44px; } .v2-monitor-mode button { flex: 1; font-size: 12px; }