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 { SourceStatusTags } from '../components/SourceStatusTags';
|
||||||
import { StatusTag } from '../components/StatusTag';
|
import { StatusTag } from '../components/StatusTag';
|
||||||
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
|
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
|
||||||
import { summarizeVehicleService } from '../domain/vehicleService';
|
import { summarizeVehicleService, type VehicleServiceVerdict } from '../domain/vehicleService';
|
||||||
|
|
||||||
function vehicleServiceStatus(row: VehicleCoverageRow) {
|
function vehicleServiceStatus(row: VehicleCoverageRow) {
|
||||||
if (row.serviceStatus) {
|
if (row.serviceStatus) {
|
||||||
@@ -27,6 +27,16 @@ function vehicleServiceStatus(row: VehicleCoverageRow) {
|
|||||||
return { label: verdict.status, color: verdict.color };
|
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) {
|
function sourceEvidenceText(row: VehicleCoverageRow) {
|
||||||
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
|
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
|
||||||
}
|
}
|
||||||
@@ -188,6 +198,14 @@ type VehicleActionRecommendation = {
|
|||||||
filters: Record<string, string> | null;
|
filters: Record<string, string> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PageVerdictStat = {
|
||||||
|
status: string;
|
||||||
|
count: number;
|
||||||
|
color: 'green' | 'orange' | 'red';
|
||||||
|
risk: string;
|
||||||
|
nextStep: string;
|
||||||
|
};
|
||||||
|
|
||||||
function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionRecommendation {
|
function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionRecommendation {
|
||||||
const verdict = summarizeVehicleService({
|
const verdict = summarizeVehicleService({
|
||||||
bindingStatus: row.bindingStatus,
|
bindingStatus: row.bindingStatus,
|
||||||
@@ -211,6 +229,41 @@ function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionReco
|
|||||||
return { label: '持续观察', color: verdict.color, filters: null };
|
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) {
|
function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record<string, string>) => void) {
|
||||||
const consistency = row.sourceConsistency;
|
const consistency = row.sourceConsistency;
|
||||||
if (!consistency) {
|
if (!consistency) {
|
||||||
@@ -354,6 +407,8 @@ export function Vehicles({
|
|||||||
return items;
|
return items;
|
||||||
}, [summary]);
|
}, [summary]);
|
||||||
const filterSummary = vehicleFilterSummary(filters);
|
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) => {
|
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -577,6 +632,45 @@ export function Vehicles({
|
|||||||
)}
|
)}
|
||||||
style={{ marginTop: 16 }}
|
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">
|
<div className="vp-result-summary-grid">
|
||||||
{resultSummary.map((item) => (
|
{resultSummary.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -241,6 +241,61 @@ body {
|
|||||||
gap: 12px;
|
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 {
|
.vp-result-summary-item {
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -1071,6 +1126,8 @@ body {
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.vp-evidence-grid,
|
.vp-evidence-grid,
|
||||||
.vp-action-grid,
|
.vp-action-grid,
|
||||||
|
.vp-current-service-board,
|
||||||
|
.vp-current-service-grid,
|
||||||
.vp-operation-flow,
|
.vp-operation-flow,
|
||||||
.vp-service-model-grid,
|
.vp-service-model-grid,
|
||||||
.vp-capability-grid,
|
.vp-capability-grid,
|
||||||
|
|||||||
@@ -1457,6 +1457,11 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
|
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.getAllByText('181').length).toBeGreaterThanOrEqual(2);
|
||||||
expect(screen.getByText('过滤车辆')).toBeInTheDocument();
|
expect(screen.getByText('过滤车辆')).toBeInTheDocument();
|
||||||
expect(screen.getByText('73')).toBeInTheDocument();
|
expect(screen.getByText('73')).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user