feat(platform): summarize vehicle service verdicts
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user