feat(platform): summarize vehicle service verdicts

This commit is contained in:
lingniu
2026-07-04 19:17:16 +08:00
parent 75d232e773
commit 99261997db
3 changed files with 157 additions and 1 deletions

View File

@@ -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<string, string> | 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<string, PageVerdictStat>();
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<string, { label: string; count: number; color: 'green' | 'orange' | 'red'; filters: Record<string, string> | 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<string, string>) => 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<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoading(true);
@@ -577,6 +632,45 @@ export function Vehicles({
)}
style={{ marginTop: 16 }}
>
{rows.length > 0 ? (
<div className="vp-current-service-board">
<div>
<div className="vp-current-service-title"></div>
<div className="vp-current-service-grid">
{pageVerdictStats.map((item) => (
<div key={item.status} className="vp-current-service-item">
<Space spacing={6} wrap>
<Tag color={item.color}>{item.status}</Tag>
<Tag color="grey">{item.count.toLocaleString()} </Tag>
</Space>
<div className="vp-current-service-risk">{item.risk}</div>
<div className="vp-current-service-next">{item.nextStep}</div>
</div>
))}
</div>
</div>
<div>
<div className="vp-current-service-title"></div>
<div className="vp-current-action-list">
{pageActionStats.map((item) => (
item.filters ? (
<Button
key={item.label}
size="small"
theme="light"
type={item.color === 'red' ? 'danger' : 'warning'}
onClick={() => applyFilters({ ...filters, ...item.filters })}
>
{item.label} {item.count.toLocaleString()}
</Button>
) : (
<Tag key={item.label} color={item.color}>{item.label} {item.count.toLocaleString()}</Tag>
)
))}
</div>
</div>
</div>
) : null}
<div className="vp-result-summary-grid">
{resultSummary.map((item) => (
<button