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

View File

@@ -241,6 +241,61 @@ body {
gap: 12px;
}
.vp-current-service-board {
margin-bottom: 16px;
padding: 14px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 16px;
}
.vp-current-service-title {
margin-bottom: 10px;
color: var(--vp-text);
font-size: 14px;
font-weight: 700;
line-height: 20px;
}
.vp-current-service-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.vp-current-service-item {
min-height: 112px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: var(--vp-surface);
display: grid;
gap: 8px;
align-content: start;
}
.vp-current-service-risk {
color: var(--vp-text);
font-size: 13px;
font-weight: 600;
line-height: 20px;
}
.vp-current-service-next {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
}
.vp-current-action-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.vp-result-summary-item {
min-height: 72px;
padding: 12px;
@@ -1071,6 +1126,8 @@ body {
@media (max-width: 900px) {
.vp-evidence-grid,
.vp-action-grid,
.vp-current-service-board,
.vp-current-service-grid,
.vp-operation-flow,
.vp-service-model-grid,
.vp-capability-grid,

View File

@@ -1457,6 +1457,11 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
render(<App />);
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
expect(screen.getByText('当前页服务判读')).toBeInTheDocument();
expect(screen.getByText('建议下一步')).toBeInTheDocument();
expect(screen.getAllByText('降级可服务').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('2 辆').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('车辆仍可查询,但需要补齐缺失来源后再做跨来源校验。')).toBeInTheDocument();
expect(screen.getAllByText('181').length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('过滤车辆')).toBeInTheDocument();
expect(screen.getByText('73')).toBeInTheDocument();