From aa286b810e8b836fa407e67987a230edc910e84a Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 22:19:20 +0800 Subject: [PATCH] feat(platform): add vehicle service delivery strip --- .../apps/web/src/layout/AppShell.tsx | 32 ++++++++ .../apps/web/src/styles/global.css | 76 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 25 ++++++ 3 files changed, 133 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index 7dfd960c..e3d7d3cf 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -188,6 +188,8 @@ export function AppShell({ const dateFrom = customerTimeWindow?.dateFrom ?? ''; const dateTo = customerTimeWindow?.dateTo ?? ''; const savedViews = customerViews ?? []; + const serviceObjectText = currentVehicleLabel ? `车辆:${currentVehicleLabel}` : '未选择车辆'; + const serviceTimeText = dateFrom || dateTo ? `${dateFrom || '未指定'} 至 ${dateTo || '未指定'}` : '实时范围'; const applyTimeWindowPreset = (days: number) => { onCustomerTimeWindowChange?.({ @@ -214,6 +216,7 @@ export function AppShell({ { label: '导出', ariaLabel: '顶部数据导出', page: 'history-query' as const }, { label: '告警', ariaLabel: '顶部告警事件', page: 'alert-events' as const } ]; + const deliveryActions = quickActions.filter((item) => item.page !== 'realtime'); return (
@@ -299,6 +302,35 @@ export function AppShell({
+
+
+ 车辆服务交付条 + 围绕一辆车或一个车队完成监控、复盘、统计、导出和通知。 +
+
+ 服务对象 + {serviceObjectText} +
+
+ 服务时间 + {serviceTimeText} +
+
+ 服务动作 +
+ {deliveryActions.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 35176b09..71493cb5 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -372,6 +372,81 @@ body { box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); } +.vp-service-delivery-strip { + min-width: min(100%, 560px); + max-width: 100%; + min-height: 44px; + padding: 6px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: 8px; + background: #ffffff; + display: grid; + grid-template-columns: minmax(138px, 0.9fr) minmax(112px, 0.65fr) minmax(150px, 0.85fr) minmax(150px, 1fr); + gap: 6px; + align-items: stretch; +} + +.vp-service-delivery-strip-head, +.vp-service-delivery-strip-fact, +.vp-service-delivery-strip-actions { + min-width: 0; + padding: 6px 8px; + border-radius: 6px; + background: #f8fafc; + display: grid; + align-content: center; + gap: 2px; +} + +.vp-service-delivery-strip-head strong, +.vp-service-delivery-strip-fact strong { + color: var(--vp-text); + font-size: 13px; + line-height: 18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vp-service-delivery-strip-head span, +.vp-service-delivery-strip-fact span, +.vp-service-delivery-strip-actions > span { + color: var(--vp-text-muted); + font-size: 11px; + line-height: 15px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vp-service-delivery-strip-actions > div { + min-width: 0; + display: flex; + align-items: center; + gap: 3px; + overflow: hidden; +} + +.vp-service-delivery-strip-actions button { + height: 22px; + min-width: 34px; + padding: 0 6px; + border: 0; + border-radius: 5px; + background: #ffffff; + color: var(--vp-primary); + cursor: pointer; + font-size: 12px; + font-weight: 700; + line-height: 20px; +} + +.vp-service-delivery-strip-actions button:hover, +.vp-service-delivery-strip-actions button:focus-visible { + background: rgba(22, 100, 255, 0.1); + outline: none; +} + .vp-topbar-quick-actions { height: 32px; display: inline-flex; @@ -10942,6 +11017,7 @@ button.vp-realtime-command-item:focus-visible { .vp-customer-service-package, .vp-customer-service-grid, .vp-vehicle-task-grid, + .vp-service-delivery-strip, .vp-dispatch-operations-home, .vp-dispatch-operations-highlights, .vp-customer-delivery-workbench, 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 a568ac76..dc2639da 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -52,6 +52,31 @@ test('renders vehicle platform shell', () => { expect(screen.getAllByText('运营总览').length).toBeGreaterThanOrEqual(1); }); +test('topbar frames customer work as a vehicle service delivery strip', () => { + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0, linkHealth: [], runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + }) as Response); + + render(); + + expect(screen.getByText('车辆服务交付条')).toBeInTheDocument(); + expect(screen.getByText('服务对象')).toBeInTheDocument(); + expect(screen.getByText('未选择车辆')).toBeInTheDocument(); + expect(screen.getByText('服务时间')).toBeInTheDocument(); + expect(screen.getByText('实时范围')).toBeInTheDocument(); + expect(screen.getByText('服务动作')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆服务交付条 地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆服务交付条 轨迹' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆服务交付条 统计' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆服务交付条 导出' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '车辆服务交付条 告警' })).toBeInTheDocument(); +}); + test('exposes AMap operations shortcuts when map key is configured', async () => { window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {