diff --git a/vehicle-data-platform/apps/web/src/v2/domain/telemetry.test.ts b/vehicle-data-platform/apps/web/src/v2/domain/telemetry.test.ts index 0f42c51c..be8306a5 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/telemetry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/telemetry.test.ts @@ -18,5 +18,6 @@ describe('latest telemetry presentation', () => { it('keeps telemetry timestamps compact for local and RFC3339 values', () => { expect(formatTelemetryTime('2026-07-14T09:24:34+08:00')).toBe('09:24:34'); expect(formatTelemetryTime('2026-07-14 09:24:34')).toBe('09:24:34'); + expect(formatTelemetryTime('2026-07-18T02:57:02Z')).toBe('10:57:02'); }); }); diff --git a/vehicle-data-platform/apps/web/src/v2/domain/telemetry.ts b/vehicle-data-platform/apps/web/src/v2/domain/telemetry.ts index 18891b51..4504329f 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/telemetry.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/telemetry.ts @@ -1,4 +1,4 @@ -import { formatZhNumber } from './formatters'; +import { formatShanghaiDateTime, formatZhNumber } from './formatters'; export function formatTelemetryValue(value: unknown, displayValue?: string) { if (displayValue) return displayValue; @@ -18,5 +18,6 @@ export function telemetryQualityLabel(quality: string) { export function formatTelemetryTime(value?: string) { if (!value) return '—'; - return value.match(/[T ](\d{2}:\d{2}:\d{2})/)?.[1] ?? value; + const local = formatShanghaiDateTime(value); + return local.match(/[T ](\d{2}:\d{2}:\d{2})/)?.[1] ?? local; } diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx index 165fa6b3..a1c4feb7 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx @@ -77,11 +77,14 @@ test('marks the content as a full-height track workspace after sidebar navigatio }>页面内容} /> ); - expect(document.querySelector('.v2-content')).not.toHaveClass('is-track-workspace'); + const content = document.querySelector('.v2-content')!; + expect(content).not.toHaveClass('is-track-workspace'); + content.scrollTop = 240; scrollTo.mockClear(); fireEvent.click(screen.getByRole('link', { name: '轨迹回放' })); await waitFor(() => expect(document.querySelector('.v2-content')).toHaveClass('is-track-workspace')); + expect(document.querySelector('.v2-content')?.scrollTop).toBe(0); expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' }); expect(screen.getByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); }); diff --git a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx index 27a62cee..9901a670 100644 --- a/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/v2/layout/AppShell.tsx @@ -124,12 +124,17 @@ export function AppShell() { const roleLabel = { viewer: '只读', operator: '处置员', admin: '管理员', customer: '客户' }[session.role]; useEffect(() => scheduleIdleRoutePreloads({ activePathname: activeRoutePath }), [activeRoutePath]); useLayoutEffect(() => { - const content = document.querySelector('.v2-content'); - if (!content) return; const reset = () => { - content.scrollTop = 0; - content.scrollLeft = 0; - content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' }); + for (const content of document.querySelectorAll('.v2-content')) { + content.scrollTop = 0; + content.scrollLeft = 0; + content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' }); + const routeRoot = content.firstElementChild; + if (routeRoot instanceof HTMLElement) { + routeRoot.scrollTop = 0; + routeRoot.scrollLeft = 0; + } + } }; reset(); let trailingFrame = 0; diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx index bd893cba..516801f5 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx @@ -55,7 +55,7 @@ test('polls lightweight single-vehicle realtime data and passes its report inter const movedRealtime = { ...initialRealtime, longitude: 113.28, latitude: 23.15, lastSeen: '2026-07-16 10:00:30' }; vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail); const realtimeSpy = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [movedRealtime], total: 1, limit: 1, offset: 0 }); - vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never); + vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0, asOf: '2026-07-16T02:00:00Z' } as never); const sourceEvidence = vi.spyOn(api, 'vehicleSourceEvidence').mockResolvedValue({ vin: initialRealtime.vin, plate: initialRealtime.plate, mileageDate: '2026-07-16', recommendedLocationProtocol: 'JT808', recommendedLocationLabel: 'JT808', locationConflict: false, @@ -84,6 +84,8 @@ test('polls lightweight single-vehicle realtime data and passes its report inter expect(commandCard?.querySelector('.v2-live-grid > div:first-child')).toHaveTextContent('速度'); expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(2)')).toHaveTextContent('SOC'); expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(4)')).toHaveTextContent('当日里程'); + expect(screen.getByLabelText('粤A12345 车辆档案')).toHaveFocus(); + expect(screen.getByText('扫描 0 帧 · 上海时间 10:00:00')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '最新上报', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '实时位置', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '最近事件', level: 5 })).toBeInTheDocument(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 7c2b5ff0..f685e44e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -17,6 +17,7 @@ import { isValidAMapCoordinate } from '../../integrations/amap'; import { FleetMap } from '../map/FleetMap'; import { InlineError, PageLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; +import { PlatformTime } from '../shared/PlatformTime'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; @@ -34,15 +35,17 @@ const operationStatusLabels = { unknown: '待维护', active: '运营中', inact const SINGLE_VEHICLE_REFRESH_MS = 10_000; const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace')); -function resetWorkspaceScroll() { - const targets = [ +function resetWorkspaceScroll(routeRoot?: HTMLElement | null) { + const targets = new Set([ + routeRoot ?? null, + routeRoot?.closest('.v2-content') ?? null, document.scrollingElement, document.documentElement, document.body, document.querySelector('.v2-shell'), - document.querySelector('.v2-main'), - document.querySelector('.v2-content') - ]; + document.querySelector('.v2-main') + ]); + for (const content of document.querySelectorAll('.v2-content')) targets.add(content); for (const target of targets) { if (!(target instanceof HTMLElement)) continue; target.scrollTop = 0; @@ -52,11 +55,11 @@ function resetWorkspaceScroll() { } function revealVehicleIdentity(root: HTMLElement | null) { - resetWorkspaceScroll(); + resetWorkspaceScroll(root); if (!root) return; root.scrollTop = 0; root.scrollLeft = 0; - root.scrollIntoView?.({ behavior: 'auto', block: 'start', inline: 'nearest' }); + root.focus({ preventScroll: true }); } function eventTimeLabel(value?: string) { @@ -296,7 +299,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon {state} - + + return @@ -471,7 +474,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele 最新上报} description="关键运行指标与推荐数据来源" - meta={{realtime?.online ? '实时在线' : '当前离线'} · {fmt(realtime?.lastSeen || identity?.lastSeen)}} + meta={{realtime?.online ? '实时在线' : '当前离线'} · } /> 速度{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}km/h实时车速 @@ -497,7 +500,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele meta={realtime?.online ? '实时跟随' : '保留最后位置'} /> undefined} /> - + @@ -528,7 +531,7 @@ export default function VehiclePage() { let trailingFrame = 0; const frame = window.requestAnimationFrame(() => { resetWorkspaceScroll(); - trailingFrame = window.requestAnimationFrame(resetWorkspaceScroll); + trailingFrame = window.requestAnimationFrame(() => resetWorkspaceScroll()); }); const shortTimer = window.setTimeout(resetWorkspaceScroll, 120); const settledTimer = window.setTimeout(resetWorkspaceScroll, 360); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 176f91aa..ce5efd35 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -2199,17 +2199,19 @@ } } +.v2-shell, +.v2-main, +.v2-content, +.v2-vehicle-record-page { + overflow-anchor: none; +} + .v2-vehicle-record-page { overflow: visible !important; - overflow-anchor: none; } -.v2-content { - overflow-anchor: none; -} - -.v2-content:has(> .v2-vehicle-record-page) { - overflow-anchor: none; +.v2-vehicle-record-page:focus { + outline: 0; } .v2-track-current-card.semi-card {