polish single vehicle entry workspace
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -77,11 +77,14 @@ test('marks the content as a full-height track workspace after sidebar navigatio
|
||||
<Routes><Route element={<AppShell />}><Route path="*" element={<span>页面内容</span>} /></Route></Routes>
|
||||
</MemoryRouter>);
|
||||
|
||||
expect(document.querySelector('.v2-content')).not.toHaveClass('is-track-workspace');
|
||||
const content = document.querySelector<HTMLElement>('.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<HTMLElement>('.v2-content')?.scrollTop).toBe(0);
|
||||
expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' });
|
||||
expect(screen.getByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -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<HTMLElement>('.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<HTMLElement>('.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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Element | null>([
|
||||
routeRoot ?? null,
|
||||
routeRoot?.closest('.v2-content') ?? null,
|
||||
document.scrollingElement,
|
||||
document.documentElement,
|
||||
document.body,
|
||||
document.querySelector<HTMLElement>('.v2-shell'),
|
||||
document.querySelector<HTMLElement>('.v2-main'),
|
||||
document.querySelector<HTMLElement>('.v2-content')
|
||||
];
|
||||
document.querySelector<HTMLElement>('.v2-main')
|
||||
]);
|
||||
for (const content of document.querySelectorAll<HTMLElement>('.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
|
||||
<SegmentedTabs className="v2-telemetry-categories" ariaLabel="字段分类" value={activeCategory} onChange={setSelectedCategory} items={categories} />
|
||||
</div>
|
||||
{state}
|
||||
<footer><b>来源</b>{indexed.sources.map((source) => <span key={`${source.protocol}-${source.endpoint ?? ''}`} title={source.endpoint}>{source.protocol}</span>)}<small>扫描 {data?.scannedFrames ?? 0} 帧 · 截至 {formatTelemetryTime(data?.asOf)}</small></footer>
|
||||
<footer><b>来源</b>{indexed.sources.map((source) => <span key={`${source.protocol}-${source.endpoint ?? ''}`} title={source.endpoint}>{source.protocol}</span>)}<small>扫描 {data?.scannedFrames ?? 0} 帧 · 上海时间 {formatTelemetryTime(data?.asOf)}</small></footer>
|
||||
<SideSheet
|
||||
className="v2-telemetry-value-sidesheet"
|
||||
visible={Boolean(inspectedValue)}
|
||||
@@ -450,7 +453,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
||||
for (const eventName of interactionEvents) window.removeEventListener(eventName, cancelEntryReset, true);
|
||||
};
|
||||
}, [detail.vin]);
|
||||
return <div ref={recordRootRef} className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`}>
|
||||
return <div ref={recordRootRef} className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}>
|
||||
<MonitorReturnBar />
|
||||
<Card className="v2-identity-band v2-record-card v2-live-card v2-live-overview v2-vehicle-command-card" bodyStyle={{ padding: 0 }}>
|
||||
<div className="v2-vehicle-command-header">
|
||||
@@ -471,7 +474,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
||||
<WorkspacePanelHeader
|
||||
title={<span id="vehicle-live-heading">最新上报</span>}
|
||||
description="关键运行指标与推荐数据来源"
|
||||
meta={<span className="v2-live-report-meta"><i className={realtime?.online ? 'is-online' : ''} />{realtime?.online ? '实时在线' : '当前离线'} · <IconClock />{fmt(realtime?.lastSeen || identity?.lastSeen)}</span>}
|
||||
meta={<span className="v2-live-report-meta"><i className={realtime?.online ? 'is-online' : ''} />{realtime?.online ? '实时在线' : '当前离线'} · <IconClock /><PlatformTime value={realtime?.lastSeen || identity?.lastSeen} /></span>}
|
||||
/>
|
||||
<div className="v2-live-grid" role="list" aria-label="车辆实时指标">
|
||||
<div role="listitem"><small>速度</small><strong>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}<em>km/h</em></strong><span>实时车速</span></div>
|
||||
@@ -497,7 +500,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
||||
meta={realtime?.online ? '实时跟随' : '保留最后位置'}
|
||||
/>
|
||||
<FleetMap vehicles={mapVehicles} selectedVin={hasLocation ? detail.vin : undefined} onSelect={() => undefined} />
|
||||
<footer><Button theme="borderless" type="tertiary" className="v2-map-source-link" title="展开全部位置来源" aria-label="查看全部位置来源" icon={<IconMapPin />} onClick={() => setSourceEvidenceOpen(true)}>{hasLocation && realtime ? `实时坐标 ${realtime.longitude.toFixed(6)}, ${realtime.latitude.toFixed(6)}` : '暂无有效实时坐标'}{identity?.locationText ? ` · 档案区域 ${identity.locationText}` : ''}</Button><time>更新时间:{fmt(realtime?.lastSeen)}</time></footer>
|
||||
<footer><Button theme="borderless" type="tertiary" className="v2-map-source-link" title="展开全部位置来源" aria-label="查看全部位置来源" icon={<IconMapPin />} onClick={() => setSourceEvidenceOpen(true)}>{hasLocation && realtime ? `实时坐标 ${realtime.longitude.toFixed(6)}, ${realtime.latitude.toFixed(6)}` : '暂无有效实时坐标'}{identity?.locationText ? ` · 档案区域 ${identity.locationText}` : ''}</Button><span className="v2-map-update-time">更新时间:<PlatformTime value={realtime?.lastSeen} /></span></footer>
|
||||
</Card>
|
||||
</div>
|
||||
<div id={vehicleSectionIDs.events} tabIndex={-1} className="v2-record-section-anchor"><Events detail={detail} /></div>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user