From 99261997dbefb8fee7709037683a6a7fe9808ede Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 19:17:16 +0800 Subject: [PATCH] feat(platform): summarize vehicle service verdicts --- .../apps/web/src/pages/Vehicles.tsx | 96 ++++++++++++++++++- .../apps/web/src/styles/global.css | 57 +++++++++++ .../apps/web/src/test/App.test.tsx | 5 + 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index c1dc1e28..e0675c1e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -8,7 +8,7 @@ import { PageHeader } from '../components/PageHeader'; import { SourceStatusTags } from '../components/SourceStatusTags'; import { StatusTag } from '../components/StatusTag'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; -import { summarizeVehicleService } from '../domain/vehicleService'; +import { summarizeVehicleService, type VehicleServiceVerdict } from '../domain/vehicleService'; function vehicleServiceStatus(row: VehicleCoverageRow) { if (row.serviceStatus) { @@ -27,6 +27,16 @@ function vehicleServiceStatus(row: VehicleCoverageRow) { return { label: verdict.status, color: verdict.color }; } +function rowServiceVerdict(row: VehicleCoverageRow): VehicleServiceVerdict { + return summarizeVehicleService({ + bindingStatus: row.bindingStatus, + sourceCount: row.sourceCount, + onlineSourceCount: row.onlineSourceCount, + missingProtocols: row.missingProtocols, + mileageDeltaKm: row.sourceConsistency?.mileageDeltaKm + }); +} + function sourceEvidenceText(row: VehicleCoverageRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } @@ -188,6 +198,14 @@ type VehicleActionRecommendation = { filters: Record | null; }; +type PageVerdictStat = { + status: string; + count: number; + color: 'green' | 'orange' | 'red'; + risk: string; + nextStep: string; +}; + function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionRecommendation { const verdict = summarizeVehicleService({ bindingStatus: row.bindingStatus, @@ -211,6 +229,41 @@ function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionReco return { label: '持续观察', color: verdict.color, filters: null }; } +function currentPageVerdictStats(rows: VehicleCoverageRow[]): PageVerdictStat[] { + const stats = new Map(); + for (const row of rows) { + const verdict = rowServiceVerdict(row); + const current = stats.get(verdict.status); + if (current) { + current.count += 1; + continue; + } + stats.set(verdict.status, { + status: verdict.status, + count: 1, + color: verdict.color, + risk: verdict.risk, + nextStep: verdict.nextStep + }); + } + const order = ['不可服务', '降级可服务', '可服务']; + return [...stats.values()].sort((left, right) => order.indexOf(left.status) - order.indexOf(right.status)); +} + +function currentPageActionStats(rows: VehicleCoverageRow[]) { + const stats = new Map | null }>(); + for (const row of rows) { + const action = vehicleActionRecommendation(row); + const current = stats.get(action.label); + if (current) { + current.count += 1; + continue; + } + stats.set(action.label, { label: action.label, count: 1, color: action.color, filters: action.filters }); + } + return [...stats.values()].sort((left, right) => right.count - left.count); +} + function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record) => void) { const consistency = row.sourceConsistency; if (!consistency) { @@ -354,6 +407,8 @@ export function Vehicles({ return items; }, [summary]); const filterSummary = vehicleFilterSummary(filters); + const pageVerdictStats = useMemo(() => currentPageVerdictStats(rows), [rows]); + const pageActionStats = useMemo(() => currentPageActionStats(rows), [rows]); const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); @@ -577,6 +632,45 @@ export function Vehicles({ )} style={{ marginTop: 16 }} > + {rows.length > 0 ? ( +
+
+
当前页服务判读
+
+ {pageVerdictStats.map((item) => ( +
+ + {item.status} + {item.count.toLocaleString()} 辆 + +
{item.risk}
+
{item.nextStep}
+
+ ))} +
+
+
+
建议下一步
+
+ {pageActionStats.map((item) => ( + item.filters ? ( + + ) : ( + {item.label} {item.count.toLocaleString()} + ) + ))} +
+
+
+ ) : null}
{resultSummary.map((item) => (