feat(platform): add fleet service journey strip

This commit is contained in:
lingniu
2026-07-06 05:39:57 +08:00
parent 0cb1763326
commit fe005a8296
3 changed files with 184 additions and 0 deletions

View File

@@ -1840,6 +1840,62 @@ export function Realtime({
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '客户车辆地图总控包');
const copyMapFleetJourneyPackage = () => copyText([
'【车队服务旅程交付包】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`实时找车:${onlineCount.toLocaleString()} 在线 / ${rows.length.toLocaleString()} 当前页 / ${pagination.total.toLocaleString()} 总计`,
`轨迹复盘:${selectedMapRow ? `${selectedMapRow.plate || '-'} / ${selectedMapRow.vin || '-'} / ${selectedVehicleProtocol || selectedMapRow.primaryProtocol || '-'}` : '未选择车辆'}`,
`里程核对:${selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '待核对'}`,
`导出通知:${mapAttentionRows.length.toLocaleString()} 关注 / ${staleCount.toLocaleString()} 更新超时`,
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`,
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `历史导出:${appURL(buildAppHash({ page: 'history-query', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : '',
`告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded', ...(filters.protocol ? { protocol: filters.protocol } : {}) } }))}`
].filter(Boolean).join('\n'), '车队服务旅程交付包');
const mapFleetJourneyItems = [
{
step: '1',
title: '实时找车',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${locatedCount.toLocaleString()} 辆有定位,先让客户知道当前能看到哪些车。`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
step: '2',
title: '轨迹复盘',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${dataFreshness(selectedMapRow).detail},进入轨迹复盘位置、速度和里程断点。` : '先从地图或车辆列表选择一辆车。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
step: '3',
title: '里程核对',
value: selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '待核对',
detail: '进入同一车辆的里程统计,核对区间差值、日统计和总里程。',
color: selectedMapRow?.totalMileageKm != null ? 'blue' as const : 'orange' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && (window.location.hash = buildAppHash({
page: 'mileage',
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol
}))
},
{
step: '4',
title: '导出通知',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: `${staleCount.toLocaleString()} 辆更新超时,交付前进入告警通知或复制交付包。`,
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => mapAttentionRows.length > 0 && onOpenQuality ? onOpenQuality({ serviceStatus: 'degraded', protocol: filters.protocol }) : copyMapFleetJourneyPackage()
}
];
const copyMapCustomerDecision = () => copyText(mapCustomerDecisionText({
filters,
rows,
@@ -2412,6 +2468,46 @@ export function Realtime({
</aside>
</section>
<section className="vp-map-fleet-journey" aria-label="车队服务旅程条">
<div className="vp-map-fleet-journey-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 关注` : '态势稳定'}
</Tag>
<Tag color={selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey'}>
{selectedMapRow ? selectedVehicleLabel : '未选车'}
</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
32960808MQTT 沿
</Typography.Text>
<Button size="small" theme="solid" type="primary" icon={<IconCopy />} onClick={copyMapFleetJourneyPackage}>
</Button>
</div>
<div className="vp-map-fleet-journey-steps">
{mapFleetJourneyItems.map((item) => (
<button
key={item.step}
type="button"
className="vp-map-fleet-journey-step"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`车队服务旅程条 ${item.step} ${item.title} ${item.value}`}
>
<Tag color={item.color}>{item.step}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
</button>
))}
</div>
</section>
<div className="vp-map-service-rail" aria-label="客户车辆地图总控">
<div className="vp-map-service-rail-summary">
<Space wrap>

View File

@@ -7011,6 +7011,79 @@ button.vp-realtime-command-item:focus-visible {
word-break: break-word;
}
.vp-map-fleet-journey {
border: 1px solid rgba(22, 100, 255, 0.18);
border-radius: var(--vp-radius);
background: #f8fbff;
display: grid;
grid-template-columns: minmax(280px, 0.68fr) minmax(0, 1.32fr);
gap: 14px;
padding: 16px;
}
.vp-map-fleet-journey-copy {
display: grid;
align-content: start;
gap: 10px;
}
.vp-map-fleet-journey-copy .semi-button {
width: fit-content;
}
.vp-map-fleet-journey-steps {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.vp-map-fleet-journey-step {
min-height: 136px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fff;
color: inherit;
cursor: pointer;
font: inherit;
text-align: left;
display: grid;
align-content: start;
gap: 8px;
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.vp-map-fleet-journey-step:hover,
.vp-map-fleet-journey-step:focus-visible {
border-color: rgba(22, 100, 255, 0.42);
box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08);
outline: none;
transform: translateY(-1px);
}
.vp-map-fleet-journey-step:disabled {
cursor: not-allowed;
opacity: 0.62;
box-shadow: none;
transform: none;
}
.vp-map-fleet-journey-step strong {
color: var(--vp-text);
font-size: 17px;
line-height: 23px;
font-weight: 700;
word-break: break-word;
}
.vp-map-fleet-journey-step span,
.vp-map-fleet-journey-step small {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
word-break: break-word;
}
.vp-map-command-side {
display: grid;
gap: 12px;

View File

@@ -11527,6 +11527,13 @@ test('shows realtime freshness status for recently updated and stale vehicles',
expect(screen.getAllByText('更新超时').length).toBeGreaterThan(0);
expect(screen.getByText('车辆服务闭环')).toBeInTheDocument();
expect(screen.getByText('客户从这里完成看车、选车、查轨迹、查历史、查统计、收告警和导出。')).toBeInTheDocument();
expect(screen.getByText('车队服务旅程条')).toBeInTheDocument();
expect(screen.getByText('把客户常用的四件事固定成一条路径:实时找车、轨迹复盘、里程核对、导出通知。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 1 实时找车 1 在线' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 2 轨迹复盘 粤A超时1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 3 里程核对 1000 km' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车队服务旅程条 4 导出通知 1 关注' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制旅程交付包/ })).toBeInTheDocument();
expect(screen.getByText('Live Map 决策条')).toBeInTheDocument();
expect(screen.getByText('客户进来先判断四件事:哪些车在线、哪些车能定位、哪辆车要先处理、选中车辆下一步做什么。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Live Map 决策条 在线覆盖 1 / 2' })).toBeInTheDocument();
@@ -11568,6 +11575,14 @@ test('shows realtime freshness status for recently updated and stale vehicles',
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务链路:实时找车 -> 轨迹回放 -> 围栏告警 -> 报表导出'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时地图http://localhost:3000/#/map?'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('选中车辆粤A超时1 / VIN-STALE-001 / JT808'));
fireEvent.click(screen.getByRole('button', { name: /复制旅程交付包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车队服务旅程交付包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('实时找车1 在线 / 2 当前页 / 2 总计'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹复盘粤A超时1 / VIN-STALE-001 / JT808'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程核对1000 km'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('导出通知1 关注 / 1 更新超时'));
fireEvent.click(screen.getByRole('button', { name: /复制地图监控包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户地图监控包】'));