feat(platform): add mileage impact scope board
This commit is contained in:
@@ -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>;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user