diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index dbb084c5..21dd2b19 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -1,8 +1,7 @@ import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; -import { IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useMemo, useState } from 'react'; import { api } from '../api/client'; -import type { DashboardSummary, LinkHealth, OpsHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types'; +import type { DashboardSummary, OpsHealth, QualityIssueRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { SourceStatusTags } from '../components/SourceStatusTags'; import { StatusTag } from '../components/StatusTag'; @@ -13,20 +12,6 @@ import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { qualityIssueLabel, qualityProtocolLabel } from '../domain/qualityIssue'; import { qualityIssueVehicleLookup } from '../domain/vehicleLookup'; -const statusColor: Record = { - ok: 'green', - warning: 'orange', - error: 'red' -}; - -const serviceStatusColor: Record = { - healthy: 'green', - degraded: 'orange', - offline: 'red', - no_data: 'orange', - identity_required: 'orange' -}; - const serviceStatusTitle: Record = { healthy: '服务正常', degraded: '来源不完整', @@ -86,13 +71,6 @@ function vehicleServiceOnlineText(serviceSummary: VehicleServiceSummary | null, return `${onlineVehicles.toLocaleString()} / ${totalVehicles.toLocaleString()} 在线`; } -function formatProtocolRate(row: ProtocolStat) { - if (!Number.isFinite(row.total) || row.total <= 0) { - return '0%'; - } - return `${Math.round((row.online / row.total) * 100)}%`; -} - function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) { if (row.serviceStatus) { return { @@ -311,16 +289,6 @@ export function Dashboard({ .finally(() => setLoading(false)); }, []); - const kpis: Array<{ label: string; value: string; filters: Record }> = [ - { label: '总车辆', value: formatCount(serviceSummary?.totalVehicles), filters: {} }, - { label: '在线车辆', value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles), filters: { online: 'online' } }, - { label: '单源车辆', value: formatCount(serviceSummary?.singleSourceVehicles), filters: { coverage: 'single' } }, - { label: '多源车辆', value: formatCount(serviceSummary?.multiSourceVehicles), filters: { coverage: 'multi' } }, - { label: '暂无来源车辆', value: formatCount(serviceSummary?.noDataVehicles), filters: { serviceStatus: 'no_data' } }, - { label: '身份未绑定', value: formatCount(serviceSummary?.identityRequiredVehicles), filters: { serviceStatus: 'identity_required' } }, - { label: '档案不完整', value: formatCount(serviceSummary?.archiveIncompleteVehicles), filters: { archiveStatus: 'incomplete' } } - ]; - const missingSourceCounts = new Map((serviceSummary?.missingSources ?? []).map((item) => [item.protocol, item.count])); const serviceActionQueue = useMemo; detail: string }>>(() => { const items: Array<{ label: string; count: number; filters: Record; detail: string }> = []; if ((serviceSummary?.noDataVehicles ?? 0) > 0) { @@ -370,6 +338,15 @@ export function Dashboard({ const commandOnlineCount = locations.filter((row) => row.online).length; const commandLocatedCount = locations.filter(hasValidCoordinate).length; const commandDegradedCount = locations.filter((row) => row.onlineSourceCount <= 0 || row.onlineSourceCount < row.sourceCount).length; + const kpis: Array<{ label: string; value: string; filters: Record }> = [ + { label: '总车辆', value: formatCount(serviceSummary?.totalVehicles), filters: {} }, + { label: '在线车辆', value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles), filters: { online: 'online' } }, + { label: '今日活跃', value: formatCount(summary?.activeToday), filters: { online: 'online' } }, + { label: '有效定位', value: commandLocatedCount.toLocaleString(), filters: { online: 'online' } }, + { label: '告警车辆', value: formatCount(summary?.issueVehicles), filters: { serviceStatus: 'degraded' } }, + { label: '今日数据', value: formatCount(summary?.frameToday), filters: {} }, + { label: '档案不完整', value: formatCount(serviceSummary?.archiveIncompleteVehicles), filters: { archiveStatus: 'incomplete' } } + ]; const highPriorityIssue = qualityIssues.find((item) => item.severity === 'error') ?? qualityIssues[0]; const highPriorityLookup = highPriorityIssue ? qualityIssueVehicleLookup(highPriorityIssue) : undefined; const highPriorityEvidenceFilters = highPriorityIssue ? priorityIssueEvidenceFilters(highPriorityIssue) : undefined; @@ -647,8 +624,8 @@ export function Dashboard({ { title: '历史数据查询', value: `${formatCount(summary?.frameToday)} 今日帧`, - meta: `Kafka Lag ${formatLag(summary?.kafkaLag)}`, - color: (summary?.kafkaLag ?? 0) > 0 ? 'orange' as const : 'blue' as const, + meta: '字段裁剪与导出', + color: 'blue' as const, detail: '查询位置历史、RAW 帧和解析字段,为车辆服务提供可追溯证据。', actions: [ { label: '查询历史数据', onClick: () => onOpenHistory({ tab: 'location' }) }, @@ -758,7 +735,7 @@ export function Dashboard({ { title: '历史数据查询', objective: '围绕车辆查询位置、RAW、解析字段,给 BI、运维和业务复盘提供证据。', - evidence: `TDengine / 今日帧 ${formatCount(summary?.frameToday)} / Kafka Lag ${formatLag(summary?.kafkaLag)}`, + evidence: `今日数据 ${formatCount(summary?.frameToday)} / 支持字段裁剪与导出`, sla: '优先返回必要字段,避免大 JSON 拖慢查询', primaryAction: '历史查询', secondaryAction: '字段证据', @@ -778,7 +755,7 @@ export function Dashboard({ { title: '统计查询', objective: '按车辆口径查询里程等指标,保证区间统计和日统计可闭合。', - evidence: `车辆口径 ${formatCount(serviceSummary?.totalVehicles)} / 多源覆盖 ${formatCount(serviceSummary?.multiSourceVehicles)}`, + evidence: `车辆口径 ${formatCount(serviceSummary?.totalVehicles)} / 区间统计可追溯轨迹证据`, sla: '统计值必须能追溯轨迹和 RAW 证据', primaryAction: '统计查询', secondaryAction: '车辆中心', @@ -798,9 +775,7 @@ export function Dashboard({ `今日帧量:${formatCount(summary?.frameToday)}`, `实时地图有效定位:${commandLocatedCount.toLocaleString()}`, `告警车辆:${formatCount(summary?.issueVehicles)}`, - `Kafka Lag:${formatLag(summary?.kafkaLag)}`, `高德地图:Web JS ${amapConfigured ? '已配置' : '待配置'} / 服务端 API ${amapApiConfigured ? '已配置' : '待配置'} / 安全代理 ${amapSecurityProxyEnabled ? '已启用' : '未启用'}`, - `链路健康:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常'}`, `优先动作:${priorityAction ? `${priorityAction.label} ${priorityAction.count.toLocaleString()}辆 - ${priorityAction.detail}` : '暂无待办'}`, highPriorityIssue ? `最高告警:${highPriorityIssue.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)} / ${highPriorityIssue.lastSeen || '-'}` : '最高告警:暂无', highPriorityIssue ? `告警详情:${highPriorityIssue.detail || '-'}` : '', @@ -890,21 +865,9 @@ export function Dashboard({ }, { section: '车辆服务', - item: '多源覆盖车辆', - value: formatCount(serviceSummary?.multiSourceVehicles), - detail: '同一车辆可由多个协议交叉验证' - }, - { - section: '车辆服务', - item: '单源车辆', - value: formatCount(serviceSummary?.singleSourceVehicles), - detail: '仅有一个协议来源,需要持续补齐' - }, - { - section: '车辆服务', - item: '暂无来源车辆', - value: formatCount(serviceSummary?.noDataVehicles), - detail: '车辆档案存在但暂无实时来源' + item: '今日活跃', + value: formatCount(summary?.activeToday), + detail: '当天有有效上报或历史记录的车辆服务对象' }, { section: '实时监控', @@ -922,7 +885,7 @@ export function Dashboard({ section: '历史数据', item: '今日帧量', value: formatCount(summary?.frameToday), - detail: `Kafka Lag ${formatLag(summary?.kafkaLag)}` + detail: '历史查询和导出可按车辆、时间、字段裁剪' }, { section: '告警事件', @@ -930,12 +893,6 @@ export function Dashboard({ value: formatCount(summary?.issueVehicles), detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无最高优先级告警' }, - { - section: '链路健康', - item: '异常链路', - value: unhealthyLinks.length.toLocaleString(), - detail: unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常' - }, { section: '地图能力', item: '高德地图', @@ -949,14 +906,6 @@ export function Dashboard({ detail: priorityAction?.detail ?? '当前没有必须立即处理的车辆服务事项' } ]; - (serviceSummary?.protocols ?? summary?.protocols ?? []).forEach((item) => { - rows.push({ - section: '协议来源', - item: item.protocol, - value: `${item.online.toLocaleString()} / ${item.total.toLocaleString()}`, - detail: `在线率 ${formatProtocolRate(item)}` - }); - }); (serviceSummary?.serviceStatuses ?? summary?.serviceStatuses ?? []).forEach((item) => { rows.push({ section: '服务状态', @@ -965,14 +914,6 @@ export function Dashboard({ detail: serviceStatusTitle[item.status] || item.status }); }); - (summary?.linkHealth ?? []).forEach((item) => { - rows.push({ - section: '链路健康', - item: item.name, - value: item.status, - detail: item.detail ?? '' - }); - }); return rows; }; const exportDashboardSnapshot = () => { @@ -992,9 +933,9 @@ export function Dashboard({
- 先看车,再看数据来源 + 先看车辆服务 - 车辆是主对象。32960、808 和宇通 MQTT 只是证据来源,客户每天需要先确认车辆在哪里、是否在线、今天跑了多少、异常能否追溯和导出。 + 车辆是主对象。32960、808 和宇通 MQTT 只是数据来源,客户每天需要先确认车辆在哪里、是否在线、今天跑了多少、异常能否追溯和导出。 @@ -1050,21 +991,42 @@ export function Dashboard({ ))}
- + {vehicleServiceOnlineText(serviceSummary, summary)} - {formatCount(serviceSummary?.multiSourceVehicles)} 多源覆盖 - + 有效定位 {commandLocatedCount.toLocaleString()} + + 今日活跃 {formatCount(summary?.activeToday)} + + 今日数据 {formatCount(summary?.frameToday)} + 0 ? 'orange' : 'green'}> {formatCount(summary?.issueVehicles)} 告警事件 - 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)} + +
+ {[ + { title: '今天车辆轨迹', detail: '查看当天活跃车辆的轨迹、速度和里程断点。', action: '查今天', onClick: () => onOpenHistory() }, + { title: '历史时间窗', detail: '按任意起止时间查询位置历史、RAW 字段和证据。', action: '自定义查询', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) }, + { title: '区间里程', detail: '按车辆和时间范围核对区间里程、日统计和异常点。', action: '统计查询', onClick: () => onOpenMileage() }, + { title: '告警复盘', detail: '按车辆或事件回放告警发生前后的轨迹和数据证据。', action: '告警事件', onClick: () => onOpenQuality() } + ].map((item) => ( + + ))} +
+
@@ -1100,7 +1062,7 @@ export function Dashboard({
{focusVehicle.reason} {focusVehicle.label} - 把协议来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。 + 把数据来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。
{[ @@ -1165,26 +1127,6 @@ export function Dashboard({ ))}
- 数据流转作业链} - style={{ marginBottom: 16 }} - > -
- {dataFlowStages.map((item) => ( - - ))} -
-
车联网场景导航} @@ -1211,31 +1153,6 @@ export function Dashboard({ ))}
- -
- {workflowSteps.map((item, index) => ( -
-
{index + 1}
-
- - {item.title} - {item.value} - - {item.detail} - -
-
- ))} -
-
{highPriorityIssue ? (
@@ -1377,89 +1294,6 @@ export function Dashboard({
) : null} - - - - {row.title} - }, - { title: '车辆数', dataIndex: 'count' }, - { - title: '操作', - width: 90, - render: (_: unknown, row: ServiceStatusStat) => ( - - -
formatProtocolRate(row) - }, - { - title: '缺失车辆', - render: (_: unknown, row: ProtocolStat) => { - const missingCount = missingSourceCounts.get(row.protocol); - if (missingCount == null) { - return '-'; - } - return ( - - ); - } - } - ]} - /> - - - - -
{row.status} - }, - { title: '说明', dataIndex: 'detail' } - ]} - /> - - - - - Kafka 当前消费积压:{formatLag(summary?.kafkaLag)} - 质量问题预览} bordered diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index e632092e..b9da688d 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -365,6 +365,34 @@ body { line-height: 26px; } +.vp-time-monitor-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-time-monitor-item { + min-height: 132px; + padding: 16px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #ffffff; + cursor: pointer; + text-align: left; + font: inherit; + display: grid; + align-content: space-between; + gap: 14px; +} + +.vp-time-monitor-item:hover, +.vp-time-monitor-item:focus-visible { + border-color: rgba(22, 100, 255, 0.45); + background: #f7fbff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + .vp-result-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -2486,6 +2514,7 @@ button.vp-realtime-command-item:focus-visible { .vp-map-ops-board, .vp-map-ops-readiness, .vp-map-ops-work, + .vp-time-monitor-grid, .vp-current-service-board, .vp-current-service-grid, .vp-realtime-command-board, diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 35523d2e..68c44a05 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -424,9 +424,9 @@ test('shows vehicle service status distribution on dashboard', async () => { render(); - expect(await screen.findByText('车辆服务状态')).toBeInTheDocument(); - expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); - expect(screen.getByText('39')).toBeInTheDocument(); + expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); + expect(screen.getAllByText('服务状态').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('质量问题预览')).toBeInTheDocument(); }); test('dashboard renders vehicle service summary metrics', async () => { @@ -495,18 +495,12 @@ test('dashboard renders vehicle service summary metrics', async () => { expect(await screen.findByText('总车辆')).toBeInTheDocument(); expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('单源车辆')).toBeInTheDocument(); - expect(screen.getByText('391')).toBeInTheDocument(); - expect(screen.getByText('多源车辆')).toBeInTheDocument(); - expect(screen.getByText('181')).toBeInTheDocument(); - expect(screen.getByText('暂无来源车辆')).toBeInTheDocument(); - expect(screen.getByText('461')).toBeInTheDocument(); - expect(screen.getByText('身份未绑定')).toBeInTheDocument(); + expect(screen.getByText('今日活跃')).toBeInTheDocument(); + expect(screen.getAllByText('有效定位').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('告警车辆')).toBeInTheDocument(); + expect(screen.getByText('今日数据')).toBeInTheDocument(); expect(screen.getByText('档案不完整')).toBeInTheDocument(); expect(screen.getByText('366')).toBeInTheDocument(); - expect(screen.getByText('来源证据在线分布')).toBeInTheDocument(); - expect(screen.getByText('YUTONG_MQTT')).toBeInTheDocument(); - expect(screen.getByText('0%')).toBeInTheDocument(); expect(screen.queryByText('NaN%')).not.toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', undefined); }); @@ -593,9 +587,9 @@ test('dashboard presents one vehicle service operating posture', async () => { render(); - expect(await screen.findByText('统一车辆服务入口')).toBeInTheDocument(); + expect(await screen.findByText('车辆服务入口')).toBeInTheDocument(); expect(screen.getAllByText('208 / 1,033 在线').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('181 多源覆盖')).toBeInTheDocument(); + expect(screen.getByText('今日活跃 4')).toBeInTheDocument(); expect(screen.getByText('7 告警事件')).toBeInTheDocument(); }); @@ -792,24 +786,24 @@ test('dashboard exposes vehicle data center capability matrix', async () => { await renderDashboard(); expect(screen.getByText('车辆运营监控台')).toBeInTheDocument(); - expect(screen.getByText('先看车,再看数据来源')).toBeInTheDocument(); + expect(screen.getByText('先看车辆服务')).toBeInTheDocument(); expect(screen.getByText('客户常用工作流')).toBeInTheDocument(); expect(screen.getByText('实时车辆地图')).toBeInTheDocument(); expect(screen.getByText('历史数据导出')).toBeInTheDocument(); + expect(screen.getByText('车辆服务入口')).toBeInTheDocument(); + expect(screen.getByText('自定义时间监控')).toBeInTheDocument(); + expect(screen.getByText('今天车辆轨迹')).toBeInTheDocument(); + expect(screen.getByText('历史时间窗')).toBeInTheDocument(); + expect(screen.getByText('区间里程')).toBeInTheDocument(); + expect(screen.getByText('告警复盘')).toBeInTheDocument(); expect(screen.getByText('车辆服务作业台')).toBeInTheDocument(); expect(screen.getByText('地图运营能力')).toBeInTheDocument(); - expect(screen.getByText('数据流转作业链')).toBeInTheDocument(); expect(screen.getByText('车联网场景导航')).toBeInTheDocument(); expect(screen.getByText('高德 Web JS')).toBeInTheDocument(); expect(screen.getByText('服务端 API')).toBeInTheDocument(); expect(screen.getByText('安全代理')).toBeInTheDocument(); expect(screen.getByText('安全码暴露')).toBeInTheDocument(); - expect(screen.getByText('协议接入')).toBeInTheDocument(); - expect(screen.getByText('统一解析')).toBeInTheDocument(); - expect(screen.getByText('实时投影')).toBeInTheDocument(); - expect(screen.getByText('车辆归并')).toBeInTheDocument(); expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('运营闭环')).toBeInTheDocument(); expect(screen.getAllByText('已配置').length).toBeGreaterThanOrEqual(2); expect(screen.getByText('已启用')).toBeInTheDocument(); expect(screen.getByText('未暴露')).toBeInTheDocument(); @@ -822,7 +816,6 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getByText('告警事件与通知')).toBeInTheDocument(); expect(screen.getByText('区间闭合复核')).toBeInTheDocument(); expect(screen.getByText('高德轨迹证据')).toBeInTheDocument(); - expect(screen.getAllByText('Kafka Lag 0').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('1,033 车辆口径')).toBeInTheDocument(); expect(screen.getAllByText('在线 208').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线态势 208').length).toBeGreaterThanOrEqual(1); @@ -840,9 +833,6 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('高德地图:Web JS 已配置;服务端 API 已配置;安全代理 已启用;安全码未暴露')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-1 秒内进入实时视图')); - fireEvent.click(screen.getByRole('button', { name: '复制流转图' })); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台数据流转图】')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('03. 实时投影 / Redis KV')); fireEvent.click(screen.getByRole('button', { name: '客户工作流 实时车辆地图' })); expect(window.location.hash).toBe('#/map?online=online'); @@ -867,14 +857,14 @@ test('dashboard exposes vehicle data center capability matrix', async () => { cleanup(); await renderDashboard(); - fireEvent.click(screen.getByRole('button', { name: '数据流转 实时投影' })); - expect(window.location.hash).toBe('#/realtime?online=online'); + fireEvent.click(screen.getByRole('button', { name: '时间监控 历史时间窗' })); + expect(window.location.hash.startsWith('#/history-query')).toBe(true); + expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); cleanup(); await renderDashboard(); - fireEvent.click(screen.getByRole('button', { name: '数据流转 统一解析' })); - expect(window.location.hash.startsWith('#/history-query')).toBe(true); - expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true'); + fireEvent.click(screen.getByRole('button', { name: '时间监控 区间里程' })); + expect(window.location.hash.startsWith('#/mileage')).toBe(true); cleanup(); await renderDashboard(); @@ -1172,7 +1162,7 @@ test('dashboard shows vehicle service action queue', async () => { test.each([ { buttonName: '查看在线车辆', expectedHash: '#/vehicles?online=online' }, - { buttonName: '查看多源车辆', expectedHash: '#/vehicles?coverage=multi' }, + { buttonName: '实时地图', expectedHash: '#/map?online=online' }, { buttonName: '查看告警事件', expectedHash: '#/alert-events' } ])('dashboard posture opens %s', async ({ buttonName, expectedHash }) => { window.history.replaceState(null, '', '/#/dashboard'); @@ -1335,18 +1325,16 @@ test('dashboard exposes end-to-end operations workflow entries', async () => { render(); - expect(await screen.findByText('全链路值班闭环')).toBeInTheDocument(); - expect(screen.getByText('接入巡检')).toBeInTheDocument(); - expect(screen.getByText('实时态势')).toBeInTheDocument(); - expect(screen.getByText('轨迹复盘')).toBeInTheDocument(); - expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); - expect(screen.getAllByText('告警事件').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('统计复核')).toBeInTheDocument(); + expect(await screen.findByText('自定义时间监控')).toBeInTheDocument(); + expect(screen.getByText('今天车辆轨迹')).toBeInTheDocument(); + expect(screen.getByText('历史时间窗')).toBeInTheDocument(); + expect(screen.getByText('区间里程')).toBeInTheDocument(); + expect(screen.getByText('告警复盘')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: '闭环入口 轨迹复盘' })); + fireEvent.click(screen.getByRole('button', { name: '时间监控 今天车辆轨迹' })); await waitFor(() => { - expect(window.location.hash).toBe('#/history?keyword=LB9A32A24R0LS1426'); + expect(window.location.hash.startsWith('#/history')).toBe(true); }); }); @@ -1459,7 +1447,7 @@ test('dashboard surfaces highest priority quality issue with evidence shortcuts' render(); expect(await screen.findByText('重点车辆服务')).toBeInTheDocument(); - expect(screen.getByText('把协议来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。')).toBeInTheDocument(); + expect(screen.getByText('把数据来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。')).toBeInTheDocument(); expect(screen.getByText('实时证据')).toBeInTheDocument(); expect(screen.getAllByText('轨迹证据').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('告警证据')).toBeInTheDocument(); @@ -1611,7 +1599,6 @@ test('dashboard copies highest priority quality issue notification text', async expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:88 / 1,033')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日活跃:96')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日帧量:1,286,320')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Kafka Lag:0')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最高告警:P0 VIN 缺失 / 粤A告警1 / 13307795425 / 2026-07-03 20:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?online=online')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时地图:http://localhost:3000/#/map?online=online')); @@ -1695,11 +1682,11 @@ test('dashboard keeps core vehicle service metrics when preview requests fail', expect(await screen.findByText('总车辆')).toBeInTheDocument(); expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('单源车辆')).toBeInTheDocument(); - expect(screen.getByText('391')).toBeInTheDocument(); + expect(screen.getByText('今日活跃')).toBeInTheDocument(); + expect(screen.getByText('告警车辆')).toBeInTheDocument(); }); -test('opens dashboard coverage from missing source distribution', async () => { +test('opens dashboard coverage from service action queue', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); @@ -1755,14 +1742,11 @@ test('opens dashboard coverage from missing source distribution', async () => { render(); - expect(await screen.findByText('缺失车辆')).toBeInTheDocument(); - expect(screen.getByText('983')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: '查看缺 YUTONG_MQTT' })); + fireEvent.click(await screen.findByRole('button', { name: '补齐 YUTONG_MQTT 来源 983' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); }); - expect(screen.getByText('当前筛选:缺 YUTONG_MQTT')).toBeInTheDocument(); }); test('opens vehicle list filtered by service summary KPI', async () => { @@ -1811,12 +1795,12 @@ test('opens vehicle list filtered by service summary KPI', async () => { render(); - fireEvent.click(await screen.findByRole('button', { name: /暂无来源车辆/ })); + fireEvent.click(await screen.findByRole('button', { name: /告警车辆/ })); await waitFor(() => { - expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&serviceStatus=no_data'), undefined); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&serviceStatus=degraded'), undefined); }); - expect(window.location.hash).toBe('#/vehicles?serviceStatus=no_data'); + expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded'); }); test('shows vehicle service result summary on vehicle list filters', async () => { @@ -3362,7 +3346,10 @@ test('opens dashboard coverage from service status distribution', async () => { render(); - fireEvent.click(await screen.findByRole('button', { name: '查看来源不完整' })); + expect(await screen.findByText('车辆服务覆盖')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('dashboard-service-status-filter')); + fireEvent.click(await screen.findByText('来源不完整')); + fireEvent.click(screen.getByRole('button', { name: '筛选' })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined);