feat(platform): frame history replay as customer delivery

This commit is contained in:
lingniu
2026-07-06 05:04:06 +08:00
parent e314f8330f
commit 97c2db01f8
2 changed files with 28 additions and 28 deletions

View File

@@ -442,7 +442,7 @@ function scheduledHistoryReportPlanText({
`车辆范围:${vehicle || '全部车辆'}`, `车辆范围:${vehicle || '全部车辆'}`,
`数据通道:${protocol || '全部数据通道'}`, `数据通道:${protocol || '全部数据通道'}`,
`时间范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`, `时间范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`,
'推荐频率:日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。', '推荐频率:日报用于客户复盘,周报用于客户对账,临时导出用于问题解释。',
'交付内容:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 统计查询链接 / 质量提示。', '交付内容:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 统计查询链接 / 质量提示。',
`当前数据量:位置 ${locationCount.toLocaleString()} 条,有效定位 ${validPointCount.toLocaleString()},明细证据 ${rawCount.toLocaleString()} 帧,字段裁剪 ${fieldCount.toLocaleString()} 个。`, `当前数据量:位置 ${locationCount.toLocaleString()} 条,有效定位 ${validPointCount.toLocaleString()},明细证据 ${rawCount.toLocaleString()} 帧,字段裁剪 ${fieldCount.toLocaleString()} 个。`,
`质量提示:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`, `质量提示:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`,
@@ -486,7 +486,7 @@ function savedHistoryReportViewText({
'【保存报表视图】', '【保存报表视图】',
`视图范围:${vehicle || '全部车辆'} / ${protocol || '全部数据通道'} / ${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`, `视图范围:${vehicle || '全部车辆'} / ${protocol || '全部数据通道'} / ${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`,
`字段模板:${fieldTemplate}`, `字段模板:${fieldTemplate}`,
'交付节奏:日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。', '交付节奏:日报用于客户复盘,周报用于客户对账,临时导出用于问题解释。',
`当前证据:位置 ${locationCount.toLocaleString()} 条,有效定位 ${validPointCount.toLocaleString()},明细 ${rawCount.toLocaleString()} 帧,字段 ${fieldCount.toLocaleString()} 个。`, `当前证据:位置 ${locationCount.toLocaleString()} 条,有效定位 ${validPointCount.toLocaleString()},明细 ${rawCount.toLocaleString()} 帧,字段 ${fieldCount.toLocaleString()} 个。`,
`质量提示:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`, `质量提示:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`,
`复用链接:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: evidenceFilters }))}` `复用链接:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: evidenceFilters }))}`
@@ -540,7 +540,7 @@ function trajectoryReviewPackageText({
? '建议核对明细证据、字段裁剪、当日统计查询,并确认平台是否存在断链或补发。' ? '建议核对明细证据、字段裁剪、当日统计查询,并确认平台是否存在断链或补发。'
: '当前页未发现明显断点、里程回退或速度异常,可作为轨迹复盘依据使用。'; : '当前页未发现明显断点、里程回退或速度异常,可作为轨迹复盘依据使用。';
return [ return [
'【轨迹复盘交接包】', '【客户轨迹复盘包】',
`车辆:${vehicle || '全部车辆'}`, `车辆:${vehicle || '全部车辆'}`,
`数据通道:${protocol || '全部数据通道'}`, `数据通道:${protocol || '全部数据通道'}`,
`查询范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`, `查询范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`,
@@ -601,7 +601,7 @@ function trajectoryImpactPackageText({
const anomalyTotal = anomalySummary.gapCount + anomalySummary.mileageRollbackCount + anomalySummary.overspeedCount; const anomalyTotal = anomalySummary.gapCount + anomalySummary.mileageRollbackCount + anomalySummary.overspeedCount;
const operationState = anomalyTotal > 0 || (coverageRate ?? 100) < 80 ? '需要复核' : '可用于业务回放'; const operationState = anomalyTotal > 0 || (coverageRate ?? 100) < 80 ? '需要复核' : '可用于业务回放';
return [ return [
'【轨迹运营影响】', '【客户轨迹影响】',
`车辆:${vehicle || '全部车辆'}`, `车辆:${vehicle || '全部车辆'}`,
`数据通道:${protocol || '全部数据通道'}`, `数据通道:${protocol || '全部数据通道'}`,
`查询范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`, `查询范围:${filters.dateFrom?.trim() || '-'}${filters.dateTo?.trim() || '-'}`,
@@ -1365,9 +1365,9 @@ export function History({
]; ];
const customerReportCadenceItems = [ const customerReportCadenceItems = [
{ {
title: '每日运营复盘', title: '每日客户复盘',
value: `${locations.total.toLocaleString()} 位置`, value: `${locations.total.toLocaleString()} 位置`,
detail: '每天交付位置历史、轨迹覆盖、里程统计和质量提示,便于运营早会复盘。', detail: '每天交付位置历史、轨迹覆盖、里程统计和质量提示,便于客户日报复盘。',
action: '日报导出', action: '日报导出',
color: locations.total > 0 ? 'green' as const : 'orange' as const, color: locations.total > 0 ? 'green' as const : 'orange' as const,
disabled: locations.items.length === 0, disabled: locations.items.length === 0,
@@ -1403,9 +1403,9 @@ export function History({
]; ];
const customerExportTemplateLibraryItems = [ const customerExportTemplateLibraryItems = [
{ {
title: '日报运营包', title: '日报复盘包',
value: '运营早会', value: '客户日报',
detail: '每天交付车辆位置覆盖、轨迹断点、字段证据和质量提示,适合早会复盘。', detail: '每天交付车辆位置覆盖、轨迹断点、字段证据和质量提示,适合客户日报复盘。',
action: '导出日报', action: '导出日报',
color: locations.total > 0 ? 'green' as const : 'orange' as const, color: locations.total > 0 ? 'green' as const : 'orange' as const,
disabled: locations.items.length === 0, disabled: locations.items.length === 0,
@@ -1442,7 +1442,7 @@ export function History({
const customerReportPurposeItems = [ const customerReportPurposeItems = [
{ {
title: '日报复盘', title: '日报复盘',
value: '运营早会', value: '客户日报',
detail: '每天快速交付车辆位置、轨迹覆盖、区间里程和质量提示。', detail: '每天快速交付车辆位置、轨迹覆盖、区间里程和质量提示。',
action: '生成日报', action: '生成日报',
color: locations.total > 0 ? 'green' as const : 'orange' as const, color: locations.total > 0 ? 'green' as const : 'orange' as const,
@@ -1499,7 +1499,7 @@ export function History({
{ {
title: '交付节奏', title: '交付节奏',
value: '日报/周报', value: '日报/周报',
detail: '日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。', detail: '日报用于客户复盘,周报用于客户对账,临时导出用于问题解释。',
action: '复制计划', action: '复制计划',
color: 'blue' as const, color: 'blue' as const,
disabled: false, disabled: false,
@@ -1508,7 +1508,7 @@ export function History({
{ {
title: '快速分享', title: '快速分享',
value: '可复制', value: '可复制',
detail: '复制当前视图链接和字段模板,交给客户或内部运营复用。', detail: '复制当前视图链接和字段模板,交给客户或内部服务团队复用。',
action: '复制链接', action: '复制链接',
color: 'blue' as const, color: 'blue' as const,
disabled: false, disabled: false,
@@ -1683,7 +1683,7 @@ export function History({
currentAddress: currentPlaybackAddress, currentAddress: currentPlaybackAddress,
anomalySummary: trajectoryAnomalies anomalySummary: trajectoryAnomalies
}), }),
'轨迹复盘交接包' '客户轨迹复盘包'
); );
}; };
const copyTrajectoryImpactPackage = () => { const copyTrajectoryImpactPackage = () => {
@@ -1702,7 +1702,7 @@ export function History({
anomalySummary: trajectoryAnomalies, anomalySummary: trajectoryAnomalies,
amapConfigured amapConfigured
}), }),
'轨迹运营影响' '客户轨迹影响'
); );
}; };
const copyTrajectoryDecision = () => { const copyTrajectoryDecision = () => {
@@ -2735,7 +2735,7 @@ export function History({
</Space> </Space>
<Typography.Title heading={5} style={{ margin: 0 }}> FleetioSamsaraGeotab </Typography.Title> <Typography.Title heading={5} style={{ margin: 0 }}> FleetioSamsaraGeotab </Typography.Title>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
</Typography.Text> </Typography.Text>
</div> </div>
<div className="vp-history-report-cadence-grid"> <div className="vp-history-report-cadence-grid">
@@ -2765,7 +2765,7 @@ export function History({
{fieldEvidenceCount.toLocaleString()} {fieldEvidenceCount.toLocaleString()}
</Tag> </Tag>
</Space> </Space>
<Typography.Title heading={5} style={{ margin: 0 }}> CSV </Typography.Title> <Typography.Title heading={5} style={{ margin: 0 }}> CSV </Typography.Title>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
</Typography.Text> </Typography.Text>
@@ -3302,7 +3302,7 @@ export function History({
))} ))}
</div> </div>
</Card> </Card>
<Card bordered title={mode === 'query' ? '历史数据质量' : '轨迹运营影响'} style={{ marginTop: 16 }}> <Card bordered title={mode === 'query' ? '历史数据质量' : '客户轨迹影响'} style={{ marginTop: 16 }}>
<div className="vp-trajectory-impact-board"> <div className="vp-trajectory-impact-board">
<div className="vp-trajectory-impact-summary"> <div className="vp-trajectory-impact-summary">
<Space wrap> <Space wrap>
@@ -3313,10 +3313,10 @@ export function History({
</Space> </Space>
<Typography.Text strong>{currentVehicleKeyword || '全部车辆'}</Typography.Text> <Typography.Text strong>{currentVehicleKeyword || '全部车辆'}</Typography.Text>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
</Typography.Text> </Typography.Text>
<Space wrap> <Space wrap>
<Button size="small" icon={<IconCopy />} onClick={copyTrajectoryImpactPackage}></Button> <Button size="small" icon={<IconCopy />} onClick={copyTrajectoryImpactPackage}></Button>
<Button size="small" disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}></Button> <Button size="small" disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}></Button>
<Button size="small" disabled={!onOpenMileage} onClick={() => onOpenMileage?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) })}></Button> <Button size="small" disabled={!onOpenMileage} onClick={() => onOpenMileage?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) })}></Button>
</Space> </Space>

View File

@@ -6887,9 +6887,9 @@ test('copies trajectory playback summary from history page', async () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计查询http://localhost:3000/#/mileage?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计查询http://localhost:3000/#/mileage?keyword=VIN-HISTORY-SUMMARY&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务http://localhost:3000/#/detail?keyword=VIN-HISTORY-SUMMARY&protocol=JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务http://localhost:3000/#/detail?keyword=VIN-HISTORY-SUMMARY&protocol=JT808'));
fireEvent.click(screen.getByRole('button', { name: /复制轨迹影响/ })); fireEvent.click(screen.getByRole('button', { name: /复制客户影响/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹运营影响】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户轨迹影响】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆VIN-HISTORY-SUMMARY'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务状态:需要复核')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务状态:需要复核'));
@@ -6903,7 +6903,7 @@ test('copies trajectory playback summary from history page', async () => {
fireEvent.click(screen.getByRole('button', { name: /复制轨迹复盘包/ })); fireEvent.click(screen.getByRole('button', { name: /复制轨迹复盘包/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹复盘交接包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户轨迹复盘包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆VIN-HISTORY-SUMMARY')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆VIN-HISTORY-SUMMARY'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹规模:位置 2 / 有效定位 2 / 明细证据 1 / 字段裁剪 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹规模:位置 2 / 有效定位 2 / 明细证据 1 / 字段裁剪 2'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹质量:定位覆盖率 100% / 回放跨度 60 分钟 / 采样间隔 60 分钟/点')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹质量:定位覆盖率 100% / 回放跨度 60 分钟 / 采样间隔 60 分钟/点'));
@@ -8272,17 +8272,17 @@ test('shows parsed field history as a flattened evidence table', async () => {
expect(screen.getByText('参考 Fleetio、Samsara、Geotab 的报表能力,把导出从一次性下载升级为可复用的交付节奏。')).toBeInTheDocument(); expect(screen.getByText('参考 Fleetio、Samsara、Geotab 的报表能力,把导出从一次性下载升级为可复用的交付节奏。')).toBeInTheDocument();
expect(screen.getByText('客户报表用途导航')).toBeInTheDocument(); expect(screen.getByText('客户报表用途导航')).toBeInTheDocument();
expect(screen.getByText('先选择客户要解决的问题,再决定导出位置、字段、统计或说明。')).toBeInTheDocument(); expect(screen.getByText('先选择客户要解决的问题,再决定导出位置、字段、统计或说明。')).toBeInTheDocument();
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: '客户报表用途导航 临时解释 质量说明 复制说明' })).toBeInTheDocument(); 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: '客户报表交付节奏 每日客户复盘 日报导出' })).toBeInTheDocument();
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: '客户报表交付节奏 字段模板复用 字段配置' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户报表交付节奏 字段模板复用 字段配置' })).toBeInTheDocument();
expect(screen.getByText('客户导出包模板库')).toBeInTheDocument(); expect(screen.getByText('客户导出包模板库')).toBeInTheDocument();
expect(screen.getByText('把一次性 CSV 下载变成客户可复用的报表模板:日报看运营、周报做对账、临时解释处理质疑、字段模板保持口径稳定。')).toBeInTheDocument(); expect(screen.getByText('把一次性 CSV 下载变成客户可复用的报表模板:日报看车辆、周报做对账、临时解释处理质疑、字段模板保持口径稳定。')).toBeInTheDocument();
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: '客户导出包模板库 问题解释包 客户问询 复制说明' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户导出包模板库 问题解释包 客户问询 复制说明' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出包模板库 字段稳定包 数据对接 字段配置' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户导出包模板库 字段稳定包 数据对接 字段配置' })).toBeInTheDocument();
@@ -8315,7 +8315,7 @@ test('shows parsed field history as a flattened evidence table', async () => {
await waitFor(() => { await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【历史数据周期交付计划】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【历史数据周期交付计划】'));
}); });
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('推荐频率:日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('推荐频率:日报用于客户复盘,周报用于客户对账,临时导出用于问题解释。'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付内容:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 统计查询链接 / 质量提示。')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付内容:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 统计查询链接 / 质量提示。'));
fireEvent.click(screen.getByRole('button', { name: '保存报表视图 当前筛选 VIN-FIELDS-001 保存视图' })); fireEvent.click(screen.getByRole('button', { name: '保存报表视图 当前筛选 VIN-FIELDS-001 保存视图' }));
await waitFor(() => { await waitFor(() => {
@@ -8488,7 +8488,7 @@ test('shows trajectory playback workspace from history locations', async () => {
expect(screen.getByText('位置导出')).toBeInTheDocument(); expect(screen.getByText('位置导出')).toBeInTheDocument();
expect(screen.getAllByText('历史明细').length).toBeGreaterThan(0); expect(screen.getAllByText('历史明细').length).toBeGreaterThan(0);
expect(screen.getAllByText('字段裁剪').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('字段裁剪').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('轨迹运营影响')).toBeInTheDocument(); expect(screen.getByText('客户轨迹影响')).toBeInTheDocument();
expect(screen.getByText('轨迹范围')).toBeInTheDocument(); expect(screen.getByText('轨迹范围')).toBeInTheDocument();
expect(screen.getAllByText('定位覆盖').length).toBeGreaterThan(0); expect(screen.getAllByText('定位覆盖').length).toBeGreaterThan(0);
expect(screen.getByText('异常影响')).toBeInTheDocument(); expect(screen.getByText('异常影响')).toBeInTheDocument();