feat(platform): add fleet service journey strip
This commit is contained in:
@@ -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">
|
||||
客户不需要理解 32960、808、MQTT 的协议差异,只要沿着这条路径完成看车、复盘、核对和交付。
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('【客户地图监控包】'));
|
||||
|
||||
Reference in New Issue
Block a user