feat(platform): add mileage impact scope board

This commit is contained in:
lingniu
2026-07-04 22:35:05 +08:00
parent 5dea611dd9
commit 6803d51b78
3 changed files with 197 additions and 2 deletions

View File

@@ -228,6 +228,54 @@ function mileageBIPublishText({
].join('\n'); ].join('\n');
} }
function statisticsImpactText({
filters,
summary,
onlineSummary,
closureStatus,
confidenceStatus,
anomalyCount,
pageCoverageRate,
publishAuditConclusion,
statisticsURL,
vehicleURL
}: {
filters: Record<string, string>;
summary: MileageSummary;
onlineSummary: OnlineStatisticsSummary;
closureStatus: string;
confidenceStatus: string;
anomalyCount: number;
pageCoverageRate: number;
publishAuditConclusion: string;
statisticsURL: string;
vehicleURL?: string;
}) {
const keyword = filters.keyword?.trim() || '全部车辆';
const protocol = filters.protocol?.trim() || '全部来源';
const dateFrom = filters.dateFrom?.trim();
const dateTo = filters.dateTo?.trim();
const dateRange = dateFrom || dateTo ? `${dateFrom || '-'}${dateTo || '-'}` : '全部日期';
return [
'【统计影响范围】',
`车辆范围:${keyword}`,
`数据来源:${protocol}`,
`日期范围:${dateRange}`,
`影响车辆:${summary.vehicleCount.toLocaleString()}`,
`统计记录:${summary.recordCount.toLocaleString()}`,
`累计里程:${formatKm(summary.totalMileageKm)} km`,
`在线状态:${onlineSummary.onlineVehicleCount.toLocaleString()} 在线 / ${onlineSummary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineSummary.onlineRatePercent)}`,
`闭合状态:${closureStatus}`,
`发布判断:${publishAuditConclusion}`,
`可信度:${confidenceStatus}`,
`异常记录:${anomalyCount.toLocaleString()}`,
`当前页覆盖:${formatPercent(pageCoverageRate)}`,
`业务影响:${publishAuditConclusion === '可发布' ? '当前统计范围可进入运营和BI口径' : '当前统计范围需要先复核轨迹、RAW和离线车辆影响'}`,
`统计查询:${statisticsURL}`,
...(vehicleURL ? [`车辆服务:${vehicleURL}`] : [])
].join('\n');
}
type MileagePublishAuditItem = { type MileagePublishAuditItem = {
key: string; key: string;
label: string; label: string;
@@ -472,6 +520,40 @@ export function Mileage({
const publishReviewCount = publishAuditItems.filter((item) => item.status === 'review').length; const publishReviewCount = publishAuditItems.filter((item) => item.status === 'review').length;
const publishAuditConclusion = publishBlockedCount > 0 ? '阻断发布' : publishReviewCount > 0 ? '复核后发布' : '可发布'; const publishAuditConclusion = publishBlockedCount > 0 ? '阻断发布' : publishReviewCount > 0 ? '复核后发布' : '可发布';
const publishAuditColor = publishBlockedCount > 0 ? 'red' as const : publishReviewCount > 0 ? 'orange' as const : 'green' as const; const publishAuditColor = publishBlockedCount > 0 ? 'red' as const : publishReviewCount > 0 ? 'orange' as const : 'green' as const;
const statisticsImpactColor = publishAuditColor;
const statisticsImpactLevel = publishAuditConclusion === '可发布' ? '可运营发布' : publishAuditConclusion === '阻断发布' ? '发布阻断' : '影响待复核';
const statisticsImpactItems = [
{
label: '影响车辆',
value: `${summary.vehicleCount.toLocaleString()}`,
detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线`,
color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const
},
{
label: '统计规模',
value: `${summary.recordCount.toLocaleString()}`,
detail: `${summary.sourceCount.toLocaleString()} 个来源,累计 ${formatKm(summary.totalMileageKm)} km`,
color: summary.recordCount > 0 ? 'blue' as const : 'grey' as const
},
{
label: '闭合影响',
value: closureStatus,
detail: closureActionText,
color: closureStatusColor
},
{
label: '异常影响',
value: `${anomalyRows.length.toLocaleString()}`,
detail: anomalyRows.length > 0 ? '异常会影响BI与运营日报需要回到证据复核。' : '当前明细未命中异常标记。',
color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const
},
{
label: '发布判断',
value: publishAuditConclusion,
detail: publishAuditConclusion === '可发布' ? '统计范围闭合且审计项通过。' : '发布前需要补齐审计项证据。',
color: publishAuditColor
}
];
const maxDateMileage = Math.max(...dateSeries.map((item) => item.value), 0); const maxDateMileage = Math.max(...dateSeries.map((item) => item.value), 0);
const maxSourceMileage = Math.max(...sourceSeries.map((item) => item.value), 0); const maxSourceMileage = Math.max(...sourceSeries.map((item) => item.value), 0);
const filterSummary = [ const filterSummary = [
@@ -610,6 +692,26 @@ export function Mileage({
'统计发布审计' '统计发布审计'
); );
}; };
const copyStatisticsImpact = () => {
const vehicleURL = currentVehicleKeyword
? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol }))
: undefined;
copyText(
statisticsImpactText({
filters,
summary,
onlineSummary,
closureStatus,
confidenceStatus,
anomalyCount: anomalyRows.length,
pageCoverageRate,
publishAuditConclusion,
statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })),
vehicleURL
}),
'统计影响范围'
);
};
const copyOnlineStatusHandoff = () => { const copyOnlineStatusHandoff = () => {
copyText( copyText(
onlineStatusHandoffText({ onlineStatusHandoffText({
@@ -689,6 +791,35 @@ export function Mileage({
))} ))}
</div> </div>
</Card> </Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyStatisticsImpact}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-stat-impact-board">
<div className="vp-stat-impact-summary">
<Tag color={statisticsImpactColor}>{statisticsImpactLevel}</Tag>
<div className="vp-monitor-metric-value">{summary.vehicleCount.toLocaleString()} </div>
<Typography.Text type="secondary">
{summary.recordCount.toLocaleString()} {formatKm(summary.totalMileageKm)} km {offlineVehicleCount.toLocaleString()} 线
</Typography.Text>
<Space wrap>
<Tag color={confidenceColor}>{confidenceStatus}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
</div>
<div className="vp-stat-impact-grid">
{statisticsImpactItems.map((item) => (
<div key={item.label} className="vp-stat-impact-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</div>
</Card>
<Card bordered> <Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => { <Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>; const nextFilters = values as Record<string, string>;

View File

@@ -1070,6 +1070,48 @@ button.vp-realtime-command-item:focus-visible {
gap: 12px; gap: 12px;
} }
.vp-stat-impact-board {
display: grid;
grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr);
gap: 16px;
}
.vp-stat-impact-summary {
min-height: 156px;
padding: 12px;
border: 1px solid rgba(22, 100, 255, 0.28);
border-radius: var(--vp-radius);
background: #f5f9ff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-stat-impact-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-stat-impact-item {
min-height: 156px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-stat-impact-item strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
font-weight: 700;
word-break: break-word;
}
.vp-stat-closure-item { .vp-stat-closure-item {
min-height: 112px; min-height: 112px;
padding: 12px; padding: 12px;
@@ -2038,6 +2080,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-stat-workspace, .vp-stat-workspace,
.vp-stat-insight-grid, .vp-stat-insight-grid,
.vp-stat-definition-grid, .vp-stat-definition-grid,
.vp-stat-impact-board,
.vp-stat-impact-grid,
.vp-stat-closure-board, .vp-stat-closure-board,
.vp-bi-publish-board, .vp-bi-publish-board,
.vp-bi-publish-grid, .vp-bi-publish-grid,

View File

@@ -7250,12 +7250,12 @@ test('shows mileage statistics workspace with trend source and definition', asyn
expect(screen.getByText('闭合差值')).toBeInTheDocument(); expect(screen.getByText('闭合差值')).toBeInTheDocument();
expect(screen.getByText('记录覆盖率')).toBeInTheDocument(); expect(screen.getByText('记录覆盖率')).toBeInTheDocument();
expect(screen.getAllByText('闭合通过').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('闭合通过').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('当前筛选范围汇总总里程与明细合计一致,可作为统计证据。')).toBeInTheDocument(); expect(screen.getAllByText('当前筛选范围汇总总里程与明细合计一致,可作为统计证据。').length).toBeGreaterThanOrEqual(1);
expect(screen.getByRole('button', { name: '复制闭合摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制闭合摘要' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '闭合复核轨迹' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '闭合复核轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '闭合复核 RAW' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '闭合复核 RAW' })).toBeInTheDocument();
expect(screen.getByText('统计发布审计')).toBeInTheDocument(); expect(screen.getByText('统计发布审计')).toBeInTheDocument();
expect(screen.getByText('复核后发布')).toBeInTheDocument(); expect(screen.getAllByText('复核后发布').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('区间闭合')).toBeInTheDocument(); expect(screen.getByText('区间闭合')).toBeInTheDocument();
expect(screen.getAllByText('记录覆盖').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('记录覆盖').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('在线证据')).toBeInTheDocument(); expect(screen.getByText('在线证据')).toBeInTheDocument();
@@ -7398,10 +7398,30 @@ test('shows vehicle-first statistics domains for one vehicle service', async ()
expect(screen.getByText('三类数据源最终汇总为一个车辆服务统计视图')).toBeInTheDocument(); expect(screen.getByText('三类数据源最终汇总为一个车辆服务统计视图')).toBeInTheDocument();
expect(screen.getByText('当前统计证据')).toBeInTheDocument(); expect(screen.getByText('当前统计证据')).toBeInTheDocument();
expect(screen.getByText('3 车 / 2 来源 / 3 条明细')).toBeInTheDocument(); expect(screen.getByText('3 车 / 2 来源 / 3 条明细')).toBeInTheDocument();
expect(screen.getByText('统计影响范围')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '复制影响范围' })).toBeInTheDocument();
expect(screen.getByText('影响待复核')).toBeInTheDocument();
expect(screen.getByText('3 辆车')).toBeInTheDocument();
expect(screen.getByText('当前筛选会影响 6 条里程记录、210 km 统计口径和 1 辆离线车辆判断。')).toBeInTheDocument();
expect(screen.getByText('影响车辆')).toBeInTheDocument();
expect(screen.getByText('统计规模')).toBeInTheDocument();
expect(screen.getByText('闭合影响')).toBeInTheDocument();
expect(screen.getByText('异常影响')).toBeInTheDocument();
expect(screen.getAllByText('发布判断').length).toBeGreaterThanOrEqual(1);
expect(await screen.findByText('75%')).toBeInTheDocument(); expect(await screen.findByText('75%')).toBeInTheDocument();
expect(screen.getAllByText((content) => content.includes('离线 1 车')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('离线 1 车')).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('Redis 在线 92 key')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('Redis 在线 92 key')).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText((content) => content.includes('由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算')).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText((content) => content.includes('由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算')).length).toBeGreaterThanOrEqual(1);
fireEvent.click(screen.getByRole('button', { name: '复制影响范围' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【统计影响范围】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围VIN-STATS-SERVICE'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据来源JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆3 辆'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计记录6 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程210 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线状态3 在线 / 1 离线 / 在线率 75%'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布判断:复核后发布'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务影响当前统计范围需要先复核轨迹、RAW和离线车辆影响'));
expect(await screen.findByText('在线状态明细')).toBeInTheDocument(); expect(await screen.findByText('在线状态明细')).toBeInTheDocument();
expect(await screen.findByText('VIN-STATS-SERVICE-OFFLINE')).toBeInTheDocument(); expect(await screen.findByText('VIN-STATS-SERVICE-OFFLINE')).toBeInTheDocument();
expect(screen.getByText('粤A离线1')).toBeInTheDocument(); expect(screen.getByText('粤A离线1')).toBeInTheDocument();