diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
index 8aae261c..804c4a67 100644
--- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
@@ -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({
+
+
+
+ 车队服务旅程条
+ 0 ? 'orange' : 'green'}>
+ {mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 关注` : '态势稳定'}
+
+
+ {selectedMapRow ? selectedVehicleLabel : '未选车'}
+
+
+
+ 把客户常用的四件事固定成一条路径:实时找车、轨迹复盘、里程核对、导出通知。
+
+
+ 客户不需要理解 32960、808、MQTT 的协议差异,只要沿着这条路径完成看车、复盘、核对和交付。
+
+ } onClick={copyMapFleetJourneyPackage}>
+ 复制旅程交付包
+
+
+
+ {mapFleetJourneyItems.map((item) => (
+
+ ))}
+
+
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index 64ca776d..85cf5b2f 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -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;
diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx
index 6599eef2..898f5a96 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -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('【客户地图监控包】'));