refine Semi UI vehicle detail workspace

This commit is contained in:
lingniu
2026-07-18 02:24:21 +08:00
parent 5a8eec16f3
commit c29d41fd86
4 changed files with 626 additions and 31 deletions

View File

@@ -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} />