From 0beca74bcf0cc5b16a67e0b347c23c150432a916 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 20:12:39 +0800 Subject: [PATCH] feat(platform): center vehicle service around customers --- .../apps/web/src/pages/Vehicles.tsx | 74 +++++++++++++++ .../apps/web/src/styles/global.css | 93 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 9 +- 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index e25bfa69..67d30346 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -913,6 +913,53 @@ export function Vehicles({ onClick: () => copyCustomerServicePackage() } ]; + const customerVehicleServiceItems = [ + { + title: '车辆池', + value: `${(summary?.totalVehicles ?? pagination.total).toLocaleString()} 车辆`, + detail: '按 VIN、车牌、手机号、OEM 和服务状态找到客户要看的车。', + action: '找车', + color: 'blue' as const, + disabled: false, + onClick: () => document.querySelector('input[name="keyword"]')?.focus() + }, + { + title: '实时地图', + value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 在线`, + detail: '直接查看当前在线车辆的位置分布、最后上报和在线状态。', + action: '打开地图', + color: (summary?.onlineVehicles ?? 0) > 0 ? 'green' as const : 'orange' as const, + disabled: !onOpenMap, + onClick: () => onOpenMap?.({ ...serviceScopeFilters, online: 'online' }) + }, + { + title: '轨迹回放', + value: filters.keyword?.trim() ? '单车时间窗' : '先选车辆', + detail: filters.keyword?.trim() ? '围绕当前车辆复盘历史位置和断点。' : '先选中一辆车,再进入时间窗轨迹回放。', + action: '回放', + color: filters.keyword?.trim() ? 'green' as const : 'grey' as const, + disabled: !onOpenHistory || !filters.keyword?.trim(), + onClick: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' }) + }, + { + title: '统计查询', + value: filters.keyword?.trim() ? '区间统计' : '单车后可用', + detail: '按车辆和时间窗核对里程、位置和业务统计口径。', + action: '查询统计', + color: filters.keyword?.trim() ? 'orange' as const : 'grey' as const, + disabled: !onOpenMileage || !filters.keyword?.trim(), + onClick: () => onOpenMileage?.(serviceScopeFilters) + }, + { + title: '告警通知', + value: `${((summary?.noDataVehicles ?? 0) + (summary?.unboundVehicles ?? 0)).toLocaleString()} 待处理`, + detail: '把不可服务车辆、离线断链和身份缺口进入告警处置。', + action: '查看告警', + color: ((summary?.noDataVehicles ?? 0) + (summary?.unboundVehicles ?? 0)) > 0 ? 'orange' as const : 'green' as const, + disabled: !onOpenQuality, + onClick: () => onOpenQuality?.(serviceScopeFilters) + } + ]; useEffect(() => { setFilters(initialFilters); @@ -1021,6 +1068,33 @@ export function Vehicles({ return (
+
+
+ + 客户车辆服务台 + {filters.keyword?.trim() ? '单车服务' : '车辆池服务'} + + {filters.keyword?.trim() || `${(summary?.totalVehicles ?? pagination.total).toLocaleString()} 辆车`} + 三个接入源最终服务同一辆车,客户从这里进入找车、实时、轨迹、统计、导出和告警。 +
+
+ {customerVehicleServiceItems.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 b78389b6..328f3853 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -3108,6 +3108,97 @@ button.vp-realtime-command-item:focus-visible { line-height: 20px; } +.vp-customer-vehicle-service-hub { + margin-bottom: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #ffffff; + display: grid; + grid-template-columns: minmax(280px, 0.52fr) minmax(0, 1.48fr); + overflow: hidden; +} + +.vp-customer-vehicle-service-summary { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 12px; + align-content: center; +} + +.vp-customer-vehicle-service-summary strong { + color: var(--vp-text); + font-size: 24px; + font-weight: 700; + line-height: 30px; + word-break: break-word; +} + +.vp-customer-vehicle-service-summary span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-customer-vehicle-service-grid { + padding: 14px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-customer-vehicle-service-item { + min-height: 148px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; + transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease; +} + +.vp-customer-vehicle-service-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-customer-vehicle-service-item:not(:disabled):hover, +.vp-customer-vehicle-service-item:not(:disabled):focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: 0 0 0 3px rgba(22, 100, 255, 0.08); + outline: none; +} + +.vp-customer-vehicle-service-item strong { + color: var(--vp-text); + font-size: 20px; + font-weight: 700; + line-height: 26px; + word-break: break-word; +} + +.vp-customer-vehicle-service-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-customer-vehicle-service-item em { + color: var(--vp-primary); + font-size: 13px; + font-style: normal; + font-weight: 700; + line-height: 20px; +} + .vp-vehicle-asset-console { margin-bottom: 16px; border: 1px solid rgba(22, 100, 255, 0.16); @@ -9469,6 +9560,8 @@ button.vp-realtime-command-item:focus-visible { .vp-service-entry-grid, .vp-current-service-board, .vp-current-service-grid, + .vp-customer-vehicle-service-hub, + .vp-customer-vehicle-service-grid, .vp-vehicle-asset-console, .vp-vehicle-asset-console-grid, .vp-vehicle-service-desk, 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 9f2997e2..f05d8791 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2205,6 +2205,13 @@ test('shows vehicle service result summary on vehicle list filters', async () => render(); expect(await screen.findByText('当前车辆结果')).toBeInTheDocument(); + expect(screen.getByText('客户车辆服务台')).toBeInTheDocument(); + expect(screen.getByText('三个接入源最终服务同一辆车,客户从这里进入找车、实时、轨迹、统计、导出和告警。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆服务台 车辆池 181 车辆 找车' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆服务台 实时地图 73 在线 打开地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆服务台 轨迹回放 先选车辆 回放' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆服务台 统计查询 单车后可用 查询统计' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户车辆服务台 告警通知 13 待处理 查看告警' })).toBeInTheDocument(); expect(screen.getByText('车辆资产运营台')).toBeInTheDocument(); expect(screen.getByText('像车队资产系统一样,先判断车辆能不能服务,再进入地图、实时、轨迹、统计、导出和告警。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆资产运营台 资产健康 181 车辆 查看车辆池' })).toBeInTheDocument(); @@ -2214,7 +2221,7 @@ test('shows vehicle service result summary on vehicle list filters', async () => expect(screen.getByText('车辆服务台')).toBeInTheDocument(); expect(screen.getByText('客户视角')).toBeInTheDocument(); expect(screen.getByText('客户查车路径')).toBeInTheDocument(); - expect(screen.getByText('车辆池服务')).toBeInTheDocument(); + expect(screen.getAllByText('车辆池服务').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('从车辆池筛出客户要看的车,再进入单车服务')).toBeInTheDocument(); expect(screen.getByText('客户侧只关心车辆服务结果,接入来源只作为证据参与筛选和可信度判断。')).toBeInTheDocument(); expect(screen.getByText('车辆身份')).toBeInTheDocument();