From f45cf90b70a16254ebf06c744870e7e9cbf37069 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 01:36:46 +0800 Subject: [PATCH] feat(platform): add vehicle service desk --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/Vehicles.tsx | 89 ++++++++++++++++++- .../apps/web/src/styles/global.css | 77 ++++++++++++++++ .../apps/web/src/test/App.test.tsx | 5 ++ 4 files changed, 171 insertions(+), 2 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 5a9b28e1..ea756a71 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -454,7 +454,7 @@ export default function App() { const pages: Record = { dashboard: , - vehicles: , + vehicles: , map: , realtime: , detail: , diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index b3fc4aff..ecb7fa84 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -383,15 +383,19 @@ async function copyVehicleShareURL() { export function Vehicles({ onOpenVehicle, onOpenQuality, + onOpenMap, onOpenRealtime, onOpenHistory, + onOpenMileage, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality?: (filters: Record) => void; + onOpenMap?: (filters: Record) => void; onOpenRealtime?: (filters: Record) => void; onOpenHistory?: (filters: Record) => void; + onOpenMileage?: (filters: Record) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { @@ -490,6 +494,12 @@ export function Vehicles({ const filterSummary = vehicleFilterSummary(filters); const pageVerdictStats = useMemo(() => currentPageVerdictStats(rows), [rows]); const pageActionStats = useMemo(() => currentPageActionStats(rows), [rows]); + const serviceScopeFilters = useMemo(() => { + const nextFilters: Record = {}; + if (filters.keyword?.trim()) nextFilters.keyword = filters.keyword.trim(); + if (filters.protocol?.trim()) nextFilters.protocol = filters.protocol.trim(); + return nextFilters; + }, [filters.keyword, filters.protocol]); const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); @@ -547,6 +557,48 @@ export function Vehicles({ } copyText(vehicleIdentityMaintenanceText({ filters, rows, total: pagination.total }), '车辆身份维护清单'); }; + const serviceDeskActions = useMemo(() => [ + { + label: '实时监控', + value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 在线`, + detail: '查看车辆在线、速度、最后上报和来源状态。', + color: 'green' as const, + disabled: !onOpenRealtime, + onClick: () => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' }) + }, + { + label: '车辆地图', + value: '地图态势', + detail: '把当前筛选车辆带入实时地图,优先看位置分布。', + color: 'blue' as const, + disabled: !onOpenMap, + onClick: () => onOpenMap?.({ ...serviceScopeFilters, online: 'online' }) + }, + { + label: '轨迹回放', + value: filters.keyword?.trim() ? '单车轨迹' : '选择车辆', + detail: filters.keyword?.trim() ? '按当前车辆进入历史轨迹复盘。' : '先选择车辆后进入精确轨迹回放。', + color: filters.keyword?.trim() ? 'blue' as const : 'grey' as const, + disabled: !onOpenHistory || !filters.keyword?.trim(), + onClick: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' }) + }, + { + label: '里程统计', + value: filters.keyword?.trim() ? '区间核对' : '车辆口径', + detail: filters.keyword?.trim() ? '按当前车辆核对区间里程和日里程。' : '输入车辆后查看里程闭合统计。', + color: filters.keyword?.trim() ? 'green' as const : 'grey' as const, + disabled: !onOpenMileage || !filters.keyword?.trim(), + onClick: () => onOpenMileage?.(serviceScopeFilters) + }, + { + label: '数据导出', + value: `${rows.length.toLocaleString()} 当前页`, + detail: '导出当前页车辆清单,后续可继续导出轨迹和原始记录。', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: exportVehicles + } + ], [exportVehicles, filters.keyword, onOpenHistory, onOpenMap, onOpenMileage, onOpenRealtime, rows.length, serviceScopeFilters, summary?.onlineVehicles]); useEffect(() => { setFilters(initialFilters); @@ -654,7 +706,42 @@ export function Vehicles({ return (
- + + +
+
+ + 客户视角 + {filters.keyword?.trim() ? '单车服务' : '车辆池'} + + {filters.keyword?.trim() || '全部车辆'} + + 当前筛选覆盖 {(summary?.totalVehicles ?? pagination.total).toLocaleString()} 辆车,在线 {(summary?.onlineVehicles ?? 0).toLocaleString()} 辆。 + 客户先看车辆能不能服务,再进入地图、轨迹、里程和导出。 + + + + + + +
+
+ {serviceDeskActions.map((item) => ( + + ))} +
+
+
{ const nextFilters = values as Record; diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 0bd0f947..702a3d74 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -654,6 +654,81 @@ button.vp-realtime-command-item:focus-visible { line-height: 18px; } +.vp-vehicle-service-desk { + display: grid; + grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr); + gap: 16px; +} + +.vp-vehicle-service-summary { + min-height: 168px; + padding: 12px; + border: 1px solid rgba(10, 168, 107, 0.28); + border-radius: var(--vp-radius); + background: #f4fbf8; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-vehicle-service-summary strong { + color: var(--vp-text); + font-size: 20px; + line-height: 26px; + word-break: break-word; +} + +.vp-vehicle-service-summary span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + +.vp-vehicle-service-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-vehicle-service-action { + min-height: 168px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; + text-align: left; + font: inherit; + cursor: pointer; +} + +.vp-vehicle-service-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-vehicle-service-action:not(:disabled):hover, +.vp-vehicle-service-action:not(:disabled):focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + +.vp-vehicle-service-action strong { + color: var(--vp-text); + font-size: 20px; + line-height: 26px; + word-break: break-word; +} + +.vp-vehicle-service-action span { + color: var(--vp-text-muted); + font-size: 13px; + line-height: 20px; +} + .vp-result-summary-item { min-height: 72px; padding: 12px; @@ -2957,6 +3032,8 @@ button.vp-realtime-command-item:focus-visible { .vp-time-monitor-grid, .vp-current-service-board, .vp-current-service-grid, + .vp-vehicle-service-desk, + .vp-vehicle-service-grid, .vp-realtime-command-board, .vp-realtime-command-grid, .vp-realtime-impact-board, 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 d736bc7c..3e2338ef 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1937,6 +1937,11 @@ 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.getByText('车辆地图')).toBeInTheDocument(); + expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('数据导出').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('当前页服务判读')).toBeInTheDocument(); expect(screen.getByText('建议下一步')).toBeInTheDocument(); expect(screen.getAllByText('降级可服务').length).toBeGreaterThanOrEqual(1);