From 67fe8567efe543f5ac0be0cacda7f973f3731d6b Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 17:42:07 +0800 Subject: [PATCH] feat(platform): add vehicle asset operations console --- .../apps/web/src/pages/Vehicles.tsx | 63 +++++++++++++ .../apps/web/src/styles/global.css | 92 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 8 +- 3 files changed, 162 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 7086c2db..e25bfa69 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -748,6 +748,40 @@ export function Vehicles({ onClick: exportVehicles } ]; + const vehicleAssetOperations = [ + { + title: '资产健康', + value: `${(summary?.totalVehicles ?? pagination.total).toLocaleString()} 车辆`, + detail: '当前筛选车辆池,可继续按车牌、手机号、OEM、在线状态和服务状态缩小范围。', + action: '查看车辆池', + color: 'blue' as const, + onClick: () => applyFilters(filters) + }, + { + title: '在线车辆', + value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 在线`, + detail: '优先进入仍在上报的车辆,查看实时位置、最后时间和来源证据。', + action: '打开实时', + color: (summary?.onlineVehicles ?? 0) > 0 ? 'green' as const : 'orange' as const, + onClick: () => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' }) + }, + { + 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, + onClick: () => applyFilters(primaryAction ? { ...filters, ...primaryAction.filters } : { ...filters, serviceStatus: 'no_data' }) + }, + { + title: '服务交付', + value: `${rows.length.toLocaleString()} 当前页`, + detail: '把当前车辆池打包成客户能用的地图、实时、轨迹、统计、告警和导出入口。', + action: '复制交付包', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + onClick: copyCustomerServicePackage + } + ]; const vehicleDecisionItems = [ { title: '在线车辆', @@ -987,6 +1021,35 @@ export function Vehicles({ return (
+
+
+ + 车辆资产运营台 + {filters.keyword?.trim() ? '单车范围' : '车辆池'} + 0 ? 'orange' : 'green'}> + {((summary?.noDataVehicles ?? 0) + (summary?.unboundVehicles ?? 0)).toLocaleString()} 待处理 + + + 像车队资产系统一样,先判断车辆能不能服务,再进入地图、实时、轨迹、统计、导出和告警。 + 车辆中心面向客户运营:先看资产健康和在线车辆,再处理异常车辆,最后形成可交付的车辆服务包。 +
+
+ {vehicleAssetOperations.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 60b96fc6..ce5337e2 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -2464,6 +2464,91 @@ button.vp-realtime-command-item:focus-visible { line-height: 20px; } +.vp-vehicle-asset-console { + margin-bottom: 16px; + border: 1px solid rgba(22, 100, 255, 0.16); + border-radius: var(--vp-radius); + background: #ffffff; + display: grid; + grid-template-columns: minmax(280px, 0.58fr) minmax(0, 1.42fr); + overflow: hidden; +} + +.vp-vehicle-asset-console-summary { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f8fbff; + display: grid; + gap: 12px; + align-content: center; +} + +.vp-vehicle-asset-console-summary strong { + color: var(--vp-text); + font-size: 20px; + font-weight: 700; + line-height: 28px; +} + +.vp-vehicle-asset-console-summary span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-vehicle-asset-console-grid { + padding: 14px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-vehicle-asset-console-item { + min-height: 142px; + 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-vehicle-asset-console-item:hover, +.vp-vehicle-asset-console-item: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-vehicle-asset-console-item strong { + color: var(--vp-text); + font-size: 20px; + font-weight: 700; + line-height: 26px; + word-break: break-word; +} + +.vp-vehicle-asset-console-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-vehicle-asset-console-item em { + color: var(--vp-primary); + font-size: 13px; + font-style: normal; + font-weight: 700; + line-height: 20px; +} + .vp-vehicle-lookup-strip { display: grid; gap: 14px; @@ -7846,6 +7931,8 @@ button.vp-realtime-command-item:focus-visible { .vp-service-entry-grid, .vp-current-service-board, .vp-current-service-grid, + .vp-vehicle-asset-console, + .vp-vehicle-asset-console-grid, .vp-vehicle-service-desk, .vp-vehicle-service-grid, .vp-vehicle-lookup-head, @@ -7998,6 +8085,11 @@ button.vp-realtime-command-item:focus-visible { border-bottom: 1px solid var(--vp-border); } + .vp-vehicle-asset-console-summary { + border-right: 0; + border-bottom: 1px solid var(--vp-border); + } + .vp-customer-cockpit-workflow { grid-template-columns: 1fr; } 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 2c06a1aa..58d39e5c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2084,6 +2084,12 @@ 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: '车辆资产运营台 待处理 13 待处理 异常优先' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆资产运营台 服务交付 2 当前页 复制交付包' })).toBeInTheDocument(); expect(screen.getByText('车辆服务台')).toBeInTheDocument(); expect(screen.getByText('客户视角')).toBeInTheDocument(); expect(screen.getByText('客户查车路径')).toBeInTheDocument(); @@ -2644,7 +2650,7 @@ test('filters vehicle list from result summary actions', async () => { render(); - fireEvent.click(await screen.findByRole('button', { name: /在线车辆 73/ })); + fireEvent.click(await screen.findByRole('button', { name: /^在线车辆 73$/ })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&online=online'), undefined);