From f0f7bc546e8ab8d88264072cc04c35d253f2d8a6 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 21:42:04 +0800 Subject: [PATCH] feat(platform): add focus vehicle service card --- .../apps/web/src/pages/Dashboard.tsx | 112 ++++++++++++++++++ .../apps/web/src/styles/global.css | 48 ++++++++ .../apps/web/src/test/App.test.tsx | 22 +++- 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 8cf82cbf..98cd0192 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -48,6 +48,20 @@ type DashboardSnapshotRow = { detail: string; }; +type FocusVehicleService = { + label: string; + lookupKey: string; + protocol?: string; + reason: string; + statusLabel: string; + statusColor: 'green' | 'orange' | 'red' | 'grey' | 'blue'; + realtimeEvidence: string; + historyEvidence: string; + alertEvidence: string; + statisticEvidence: string; + issueType?: string; +}; + const dashboardSnapshotColumns: CsvColumn[] = [ { title: '模块', value: (row) => row.section }, { title: '指标', value: (row) => row.item }, @@ -163,6 +177,43 @@ function priorityIssueNotificationText(row: QualityIssueRow) { ].join('\n'); } +function focusVehicleFromIssue(row: QualityIssueRow): FocusVehicleService { + const lookup = qualityIssueVehicleLookup(row); + const filters = priorityIssueEvidenceFilters(row); + const label = priorityIssueVehicleLabel(row); + return { + label, + lookupKey: lookup.key, + protocol: row.protocol, + reason: `${row.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(row.issueType)}`, + statusLabel: row.severity === 'error' ? '优先处置' : '持续跟踪', + statusColor: row.severity === 'error' ? 'red' : 'orange', + realtimeEvidence: `来源 ${qualityProtocolLabel(row.protocol)} / 最后 ${row.lastSeen || '-'}`, + historyEvidence: filters.dateFrom ? `${filters.dateFrom} 至 ${filters.dateTo}` : '待确认时间窗', + alertEvidence: row.detail || qualityIssueLabel(row.issueType), + statisticEvidence: '统计查询需回溯车辆口径', + issueType: row.issueType + }; +} + +function focusVehicleFromRealtime(row: VehicleRealtimeRow): FocusVehicleService { + const status = rowServiceStatus(row); + const label = [row.plate?.trim(), row.vin?.trim()].filter(Boolean).join(' / ') || row.phone || '-'; + const location = hasValidCoordinate(row) ? `${row.longitude.toFixed(6)}, ${row.latitude.toFixed(6)}` : '无有效坐标'; + return { + label, + lookupKey: row.vin || row.phone || row.plate, + protocol: row.primaryProtocol, + reason: row.online ? '最新在线车辆' : '最新车辆', + statusLabel: status.label, + statusColor: status.color, + realtimeEvidence: `${row.onlineSourceCount}/${row.sourceCount} 来源在线 / ${row.lastSeen || '-'}`, + historyEvidence: location, + alertEvidence: row.onlineSourceCount < row.sourceCount ? '来源不完整,建议检查缺失协议' : '暂无高优先级告警', + statisticEvidence: `速度 ${formatCount(row.speedKmh)} km/h / 里程 ${formatCount(row.totalMileageKm)} km` + }; +} + function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record) => void) { const consistency = row.sourceConsistency; if (!consistency) { @@ -316,6 +367,7 @@ export function Dashboard({ const highPriorityIssue = qualityIssues.find((item) => item.severity === 'error') ?? qualityIssues[0]; const highPriorityLookup = highPriorityIssue ? qualityIssueVehicleLookup(highPriorityIssue) : undefined; const highPriorityEvidenceFilters = highPriorityIssue ? priorityIssueEvidenceFilters(highPriorityIssue) : undefined; + const focusVehicle = highPriorityIssue ? focusVehicleFromIssue(highPriorityIssue) : locations[0] ? focusVehicleFromRealtime(locations[0]) : undefined; const unhealthyLinkCount = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok').length; const commandMapPoints: VehicleMapPoint[] = locations.map((row, index) => ({ id: row.vin || `${row.primaryProtocol || 'source'}-${index}`, @@ -602,6 +654,30 @@ export function Dashboard({ ]; copyText(lines.join('\n'), '功能蓝图'); }; + const copyFocusVehicleService = () => { + if (!focusVehicle) { + Toast.warning('当前没有重点车辆服务可复制'); + return; + } + const commonFilters = { keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' }; + const lines = [ + '【重点车辆服务处置卡】', + `车辆:${focusVehicle.label}`, + `原因:${focusVehicle.reason}`, + `服务状态:${focusVehicle.statusLabel}`, + `实时证据:${focusVehicle.realtimeEvidence}`, + `轨迹证据:${focusVehicle.historyEvidence}`, + `告警证据:${focusVehicle.alertEvidence}`, + `统计证据:${focusVehicle.statisticEvidence}`, + `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`, + `实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`, + `RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: { ...commonFilters, tab: 'raw', includeFields: 'true' } }))}`, + `统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`, + `告警事件:${appURL(buildAppHash({ page: 'alert-events', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: focusVehicle.issueType ? { issueType: focusVehicle.issueType } : {} }))}` + ]; + copyText(lines.join('\n'), '重点车辆服务处置卡'); + }; const buildDashboardSnapshotRows = () => { const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const priorityAction = serviceActionQueue[0]; @@ -738,6 +814,42 @@ export function Dashboard({ + {focusVehicle ? ( + 重点车辆服务{focusVehicle.statusLabel}} + style={{ marginBottom: 16 }} + > +
+
+ {focusVehicle.reason} + {focusVehicle.label} + 把协议来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。 +
+
+ {[ + { label: '实时证据', value: focusVehicle.realtimeEvidence }, + { label: '轨迹证据', value: focusVehicle.historyEvidence }, + { label: '告警证据', value: focusVehicle.alertEvidence }, + { label: '统计证据', value: focusVehicle.statisticEvidence } + ].map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+ + + + + + + + +
+
+ ) : null}
{operationWorkbench.map((item) => ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index f545245a..49647d59 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1354,6 +1354,52 @@ button.vp-realtime-command-item:focus-visible { line-height: 30px; } +.vp-focus-service { + display: grid; + grid-template-columns: minmax(220px, 0.8fr) minmax(420px, 1.6fr) auto; + gap: 16px; + align-items: center; +} + +.vp-focus-service-main { + min-width: 0; +} + +.vp-focus-service-main .semi-typography { + line-height: 20px; +} + +.vp-focus-service-evidence { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-focus-service-evidence-item { + min-height: 78px; + padding: 11px 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 7px; + align-content: start; +} + +.vp-focus-service-evidence-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-focus-service-evidence-item strong { + color: var(--vp-text); + font-size: 13px; + line-height: 19px; + font-weight: 600; + word-break: break-word; +} + .vp-operation-flow { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -1579,6 +1625,8 @@ button.vp-realtime-command-item:focus-visible { .vp-source-coverage-grid, .vp-runbook-grid, .vp-workbench-grid, + .vp-focus-service, + .vp-focus-service-evidence, .vp-scenario-grid, .vp-operation-flow, .vp-service-model-grid, 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 cafc0dab..7b2ebe6b 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1015,6 +1015,11 @@ test('dashboard exposes end-to-end operations workflow entries', async () => { test('dashboard surfaces highest priority quality issue with evidence shortcuts', async () => { window.history.replaceState(null, '', '/#/dashboard'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { @@ -1116,10 +1121,21 @@ test('dashboard surfaces highest priority quality issue with evidence shortcuts' render(); + expect(await screen.findByText('重点车辆服务')).toBeInTheDocument(); + expect(screen.getByText('把协议来源作为证据,把实时、轨迹、RAW、统计和告警集中到同一辆车处理。')).toBeInTheDocument(); + expect(screen.getByText('实时证据')).toBeInTheDocument(); + expect(screen.getAllByText('轨迹证据').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('告警证据')).toBeInTheDocument(); + expect(screen.getByText('统计证据')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制处置卡' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【重点车辆服务处置卡】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A告警1 / 13307795425')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时监控:http://localhost:3000/#/realtime?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('RAW证据:http://localhost:3000/#/history-query?keyword=%E7%B2%A4A%E5%91%8A%E8%AD%A61&protocol=JT808&tab=raw&includeFields=true')); expect(await screen.findByText('最高优先级告警')).toBeInTheDocument(); - expect(screen.getByText('粤A告警1 / 13307795425')).toBeInTheDocument(); + expect(screen.getAllByText('粤A告警1 / 13307795425').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('VIN 缺失')).toBeInTheDocument(); - expect(screen.getByText('JT808 数据缺少 VIN 映射')).toBeInTheDocument(); + expect(screen.getAllByText('JT808 数据缺少 VIN 映射').length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByRole('button', { name: '首页告警轨迹证据' })); @@ -2470,7 +2486,7 @@ test('shows row service status in dashboard vehicle previews', async () => { expect(screen.getAllByText('1/2 来源在线').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('覆盖部分离线')).toBeInTheDocument(); expect(screen.getByText('VIN-REALTIME-OFFLINE')).toBeInTheDocument(); - expect(screen.getByText('实时车辆离线')).toBeInTheDocument(); + expect(screen.getAllByText('实时车辆离线').length).toBeGreaterThanOrEqual(1); }); test('filters dashboard coverage from source consistency diagnosis', async () => {