refine Semi UI vehicle detail workspace
This commit is contained in:
@@ -77,7 +77,10 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
|
||||
await waitFor(() => expect(fleetMapVehicles).toHaveBeenLastCalledWith([
|
||||
expect.objectContaining({ longitude: 113.28, latitude: 23.15, reportIntervalMs: 30_000 })
|
||||
]));
|
||||
expect(view.container.querySelector('.v2-identity-band')).toHaveClass('semi-card');
|
||||
const commandCard = view.container.querySelector('.v2-identity-band');
|
||||
expect(commandCard).toHaveClass('semi-card', 'v2-vehicle-command-card', 'v2-live-overview');
|
||||
expect(screen.getByRole('list', { name: '车辆实时指标' })).toHaveClass('v2-live-grid');
|
||||
expect(commandCard?.querySelectorAll('.v2-live-grid > [role="listitem"]')).toHaveLength(6);
|
||||
expect(screen.getByRole('heading', { name: '最新上报', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '实时位置', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '最近事件', level: 5 })).toBeInTheDocument();
|
||||
@@ -99,6 +102,7 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
|
||||
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);
|
||||
fireEvent.click(locationSource);
|
||||
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
@@ -347,4 +351,5 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
|
||||
expect(document.querySelector('.v2-telemetry-table')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-telemetry-mobile-list.semi-list')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-telemetry-mobile-item.semi-list-item')).toHaveTextContent('36km/h正常');
|
||||
expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标');
|
||||
});
|
||||
|
||||
@@ -51,6 +51,12 @@ function vehicleLastSeenLabel(value?: string) {
|
||||
return normalized.length >= 16 ? `${normalized.slice(5, 16)} 更新` : normalized;
|
||||
}
|
||||
|
||||
function eventTimeLabel(value?: string) {
|
||||
if (!value) return '—';
|
||||
const normalized = value.replace('T', ' ');
|
||||
return normalized.length >= 16 ? normalized.slice(5, 16) : normalized;
|
||||
}
|
||||
|
||||
function VehicleSearch() {
|
||||
const navigate = useNavigate();
|
||||
const { session } = usePlatformSession();
|
||||
@@ -255,7 +261,7 @@ function Events({ detail }: { detail: VehicleDetail }) {
|
||||
emptyContent={<Empty className="v2-event-empty" title="暂无可用事件证据" description="车辆产生质量提醒或来源状态变化后会显示在这里。" />}
|
||||
renderItem={(event, index) => <List.Item className={`v2-event-row is-${event.tone}`} key={`${event.title}-${event.time}-${index}`}>
|
||||
<span className="v2-event-icon">{event.tone === 'success' ? <IconTickCircle /> : <IconAlarm />}</span>
|
||||
<div><strong>{event.title}</strong><p>{event.detail}</p></div><time>{fmt(event.time)}</time>
|
||||
<div><strong>{event.title}</strong><p>{event.detail}</p></div><time title={fmt(event.time)}>{eventTimeLabel(event.time)}</time>
|
||||
</List.Item>}
|
||||
/>
|
||||
</Card>;
|
||||
@@ -409,16 +415,19 @@ function VehicleRecordNavigation() {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
target.focus({ preventScroll: true });
|
||||
};
|
||||
const navigationItems = [
|
||||
['location', '实时位置', <IconMapPin />],
|
||||
['events', '最近事件', <IconAlarm />],
|
||||
['telemetry', '实时遥测', <IconClock />],
|
||||
['archive', '车辆主档', <IconBox />]
|
||||
] as const;
|
||||
return <Card className="v2-record-card v2-vehicle-record-nav" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
variant="compact"
|
||||
title="详情导航"
|
||||
description="快速定位当前车辆的数据区域"
|
||||
title="车辆视图"
|
||||
description="位置、事件、遥测与主档"
|
||||
actions={<nav className="v2-vehicle-section-nav" aria-label="单车详情导航">
|
||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconMapPin />} aria-label="跳转到实时位置" onClick={() => jumpToSection('location')}>实时位置</Button>
|
||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconAlarm />} aria-label="跳转到最近事件" onClick={() => jumpToSection('events')}>最近事件</Button>
|
||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconClock />} aria-label="跳转到实时遥测" onClick={() => jumpToSection('telemetry')}>实时遥测</Button>
|
||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconBox />} aria-label="跳转到车辆主档" onClick={() => jumpToSection('archive')}>车辆主档</Button>
|
||||
{navigationItems.map(([key, label, icon]) => <Button key={key} size="small" theme="borderless" type="tertiary" icon={icon} aria-label={`跳转到${label}`} onClick={() => jumpToSection(key)}>{label}</Button>)}
|
||||
</nav>}
|
||||
/>
|
||||
</Card>;
|
||||
@@ -441,28 +450,37 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
||||
];
|
||||
return <div className="v2-vehicle-record-page">
|
||||
<MonitorReturnBar />
|
||||
<Card className="v2-identity-band" bodyStyle={{ padding: 0 }}>
|
||||
<div className="v2-identity-primary"><span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span><Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></div>
|
||||
<div className="v2-identity-meta"><div><small>车辆品牌 / 车型</small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></div><div><small>可用数据来源</small><p>{detail.sources.length ? detail.sources.map((source) => <span key={source}>{source}</span>) : <span>暂无来源</span>}</p></div></div>
|
||||
<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)}
|
||||
<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">
|
||||
<div className="v2-identity-primary">
|
||||
<span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span>
|
||||
<Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag>
|
||||
<span className="v2-identity-vin"><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></span>
|
||||
</div>
|
||||
<div className="v2-identity-meta">
|
||||
<div><small>车辆品牌 / 车型</small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></div>
|
||||
<div><small>可用数据来源</small><div className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <Tag key={source} color="blue" type="light" size="small">{source}</Tag>) : <Tag color="grey" type="light" size="small">暂无来源</Tag>}</div></div>
|
||||
</div>
|
||||
<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="v2-record-card v2-live-card v2-live-overview" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title="最新上报"
|
||||
meta={<span className="v2-live-report-meta"><i className={realtime?.online ? 'is-online' : ''} />{realtime?.online ? '实时在线' : '当前离线'} · <IconClock />{fmt(realtime?.lastSeen || identity?.lastSeen)}</span>}
|
||||
/>
|
||||
<div className="v2-live-grid">
|
||||
<div><small>速度</small><strong>{availableMetric(realtime?.speedKmh, realtime?.speedAvailable)}<em>km/h</em></strong></div>
|
||||
<div><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong></div>
|
||||
<div><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></div>
|
||||
<div><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></div>
|
||||
<div><small>推荐来源</small><strong className="is-text">{fmt(realtime?.primaryProtocol)}<em>{realtime?.locationSource ? ` · ${realtime.locationSource}` : ''}</em></strong></div>
|
||||
<div><small>来源状态</small><strong>{realtime?.onlineSourceCount ?? 0}<em> 在线 / {detail.sourceStatus.length} 个</em></strong></div>
|
||||
</div>
|
||||
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />
|
||||
<section className="v2-vehicle-live-section" aria-labelledby="vehicle-live-heading">
|
||||
<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>}
|
||||
/>
|
||||
<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>
|
||||
<div role="listitem"><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong><span>剩余电量</span></div>
|
||||
<div 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 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 role="listitem"><small>推荐来源</small><strong className="is-text">{fmt(realtime?.primaryProtocol)}</strong><span title={realtime?.locationSource}>{realtime?.locationSource || '当前最优来源'}</span></div>
|
||||
<div role="listitem"><small>来源状态</small><strong>{realtime?.onlineSourceCount ?? 0}<em> / {detail.sourceStatus.length}</em></strong><span>在线来源 / 全部</span></div>
|
||||
</div>
|
||||
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
<VehicleSourceEvidencePanel vin={detail.vin} open={sourceEvidenceOpen} onOpenChange={setSourceEvidenceOpen} />
|
||||
|
||||
Reference in New Issue
Block a user