From fe005a8296fa43f8cb34be5312055e1569091697 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 05:39:57 +0800 Subject: [PATCH] feat(platform): add fleet service journey strip --- .../apps/web/src/pages/Realtime.tsx | 96 +++++++++++++++++++ .../apps/web/src/styles/global.css | 73 ++++++++++++++ .../apps/web/src/test/App.test.tsx | 15 +++ 3 files changed, 184 insertions(+) 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 的协议差异,只要沿着这条路径完成看车、复盘、核对和交付。 + + +
+
+ {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('【客户地图监控包】'));