feat: unify vehicle live metric rail

This commit is contained in:
lingniu
2026-07-19 10:10:28 +08:00
parent a6f6ce260f
commit add47b7efc
4 changed files with 241 additions and 25 deletions

View File

@@ -83,12 +83,13 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
expect(screen.getByLabelText('单车任务栏')).toHaveClass('v2-workspace-command-bar', 'v2-vehicle-record-command');
expect(screen.getByLabelText('单车任务栏')).toHaveTextContent('粤A12345');
expect(screen.getByLabelText('单车任务栏')).toHaveTextContent('LTEST000000000001');
expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-live-grid');
expect(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6);
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(commandCard?.querySelector('.v2-live-grid > div:nth-child(5)')).toHaveTextContent('推荐来源JT/T 808');
expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-workspace-metric-list');
expect(commandCard?.querySelector('.v2-vehicle-live-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue');
expect(commandCard?.querySelectorAll('.v2-workspace-metric-list > [role="listitem"]')).toHaveLength(5);
expect(commandCard?.querySelector('.v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度');
expect(commandCard?.querySelector('.v2-workspace-metric-list > span:nth-child(2)')).toHaveTextContent('SOC');
expect(commandCard?.querySelector('.v2-workspace-metric-list > span:nth-child(4)')).toHaveTextContent('当日里程');
expect(commandCard?.querySelector('.v2-vehicle-live-source-context')).toHaveTextContent('推荐来源JT/T 808');
expect(commandCard?.querySelector('.v2-live-primary-protocol')).toHaveClass('v2-protocol-tag', 'semi-tag-cyan-light');
expect(commandCard?.querySelector('.v2-identity-sources .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(screen.getByLabelText('粤A12345 车辆档案')).toHaveFocus();
@@ -112,9 +113,9 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
expect(locationSource).toHaveClass('semi-button', 'v2-map-source-link');
expect(locationSource.closest('.semi-card')).toHaveClass('v2-single-map-card');
const totalMileageSource = screen.getByRole('button', { name: '查看总里程全部来源' });
expect(totalMileageSource).toHaveClass('semi-button', 'v2-live-source-link');
expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-live-overview');
expect(totalMileageSource.closest('.semi-card')).toBe(commandCard);
expect(totalMileageSource).toHaveClass('semi-button', 'v2-workspace-metric-action');
expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-vehicle-live-metric-rail');
expect(totalMileageSource.closest('.v2-live-overview')).toBe(commandCard);
fireEvent.click(locationSource);
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
});
@@ -445,7 +446,7 @@ test('shows unavailable live fields as dashes instead of fabricated zeroes', asy
const liveOverview = (await screen.findByText('最新上报')).closest('.semi-card')!;
for (const label of ['速度', 'SOC', '总里程', '当日里程']) {
const metric = Array.from(liveOverview.querySelectorAll('.v2-live-grid > div')).find((item) => item.querySelector('small')?.textContent === label);
const metric = Array.from(liveOverview.querySelectorAll('.v2-workspace-metric-list > [role="listitem"]')).find((item) => item.querySelector('small')?.textContent === label);
expect(metric).toHaveTextContent('—');
expect(metric).not.toHaveTextContent('88.8');
expect(metric).not.toHaveTextContent(/^0/);
@@ -562,11 +563,11 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
expect(document.querySelector('.v2-identity-sources .semi-tag-blue-light')).toHaveTextContent('GB/T 32960');
expect(document.querySelector('.v2-identity-sources .semi-tag-cyan-light')).toHaveTextContent('JT/T 808');
expect(document.querySelector('.v2-identity-sources .semi-tag-violet-light')).toHaveTextContent('宇通 MQTT');
expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标');
expect(document.querySelectorAll('.v2-vehicle-command-card .v2-live-grid > [role="listitem"]')).toHaveLength(6);
expect(document.querySelector('.v2-live-metric.is-speed')).toHaveTextContent('速度');
expect(document.querySelector('.v2-live-metric.is-today-mileage')).toHaveTextContent('当日里程');
expect(document.querySelector('.v2-live-metric.is-source-status')).toHaveTextContent('来源状态');
expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list')).toHaveAttribute('aria-label', '车辆实时指标');
expect(document.querySelectorAll('.v2-vehicle-command-card .v2-workspace-metric-list > [role="listitem"]')).toHaveLength(5);
expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:first-child')).toHaveTextContent('速度');
expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:nth-child(4)')).toHaveTextContent('当日里程');
expect(document.querySelector('.v2-vehicle-command-card .v2-workspace-metric-list > span:nth-child(5)')).toHaveTextContent('来源状态');
fireEvent.click(screen.getByRole('button', { name: '历史数据' }));
expect(await screen.findByTestId('mobile-history-route')).toBeInTheDocument();
});

View File

@@ -23,6 +23,7 @@ import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext';
@@ -466,6 +467,46 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
...(hasMenu(session, 'statistics') ? [{ key: 'statistics', label: '里程查询', icon: <IconClock />, to: withMonitorReturn(`/statistics?vins=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []),
...(hasMenu(session, 'alerts') ? [{ key: 'alerts', label: '告警事件', icon: <IconAlarm />, to: `/alerts?vin=${encodeURIComponent(detail.vin)}`, type: 'tertiary' as const }] : [])
];
const liveMetrics: WorkspaceQueueMetricRailItem[] = [
{
label: '速度',
value: <>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}<em className="v2-vehicle-live-unit">km/h</em></>,
note: '实时车速',
tone: 'primary',
emphasis: 'primary'
},
{
label: 'SOC',
value: <>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em className="v2-vehicle-live-unit">%</em></>,
note: '剩余电量',
tone: 'success',
emphasis: 'secondary'
},
{
label: '总里程',
value: <>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}<em className="v2-vehicle-live-unit">km</em></>,
note: <span className="v2-vehicle-live-action-note"><IconChevronRight /></span>,
emphasis: 'secondary',
ariaLabel: '查看总里程全部来源',
onClick: () => setSourceEvidenceOpen(true)
},
{
label: '当日里程',
value: <>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}<em className="v2-vehicle-live-unit">km</em></>,
note: <span className="v2-vehicle-live-action-note"><IconChevronRight /></span>,
tone: 'primary',
emphasis: 'primary',
ariaLabel: '查看当日里程全部来源',
onClick: () => setSourceEvidenceOpen(true)
},
{
label: '来源状态',
value: <>{realtime?.onlineSourceCount ?? 0}<em className="v2-vehicle-live-unit">/ {detail.sourceStatus.length}</em></>,
note: '在线来源 / 全部',
tone: (realtime?.onlineSourceCount ?? 0) > 0 ? 'success' : 'neutral',
emphasis: 'secondary'
}
];
useLayoutEffect(() => {
const root = recordRootRef.current;
if (!root) return;
@@ -529,14 +570,17 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
description="关键运行指标与推荐数据来源"
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 className="v2-live-metric is-speed" role="listitem"><small></small><strong>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}<em>km/h</em></strong><span></span></div>
<div className="v2-live-metric is-soc" role="listitem"><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong><span></span></div>
<div className="v2-live-metric is-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看总里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div>
<div className="v2-live-metric is-today-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看当日里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div>
<div className="v2-live-metric is-source" role="listitem"><small></small><ProtocolTag protocol={realtime?.primaryProtocol} className="v2-live-primary-protocol" /><span title={realtime?.locationSource}>{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}</span></div>
<div className="v2-live-metric is-source-status" role="listitem"><small></small><strong>{realtime?.onlineSourceCount ?? 0}<em> / {detail.sourceStatus.length}</em></strong><span>线 / </span></div>
</div>
<WorkspaceMetricRail
variant="queue"
className="v2-vehicle-live-metric-rail"
ariaLabel="车辆实时指标"
items={liveMetrics}
context={<span className="v2-vehicle-live-source-context">
<small></small>
<strong><ProtocolTag protocol={realtime?.primaryProtocol} className="v2-live-primary-protocol" compact /></strong>
<em title={realtime?.locationSource}>{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}</em>
</span>}
/>
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />
</section>
</Card>