refine Semi UI vehicle evidence

This commit is contained in:
lingniu
2026-07-18 18:07:15 +08:00
parent 801a9851ea
commit 5bfdc7f637
13 changed files with 110 additions and 47 deletions

View File

@@ -13,6 +13,7 @@ import { canAdminister, hasMenu } from '../auth/session';
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityLabel } from '../domain/telemetry';
import { formatZhNumber } from '../domain/formatters';
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
import { isValidAMapCoordinate } from '../../integrations/amap';
import { FleetMap } from '../map/FleetMap';
import { InlineError, PageLoading } from '../shared/AsyncState';
@@ -118,7 +119,7 @@ function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; edita
function Events({ detail }: { detail: VehicleDetail }) {
const events = [
...detail.quality.items.slice(0, 3).map((item) => ({ tone: issueTone(item), title: item.severity === 'error' ? '质量异常' : '质量提醒', detail: item.detail, time: item.lastSeen })),
...detail.sourceStatus.slice(0, 3).map((item) => ({ tone: item.online ? 'success' : 'muted', title: item.online ? '数据上报' : '来源离线', detail: `${item.protocol} · ${item.online ? '当前在线' : '暂无在线数据'}`, time: item.lastSeen }))
...detail.sourceStatus.slice(0, 3).map((item) => ({ tone: item.online ? 'success' : 'muted', title: item.online ? '数据上报' : '来源离线', detail: `${protocolDisplayLabel(item.protocol)} · ${item.online ? '当前在线' : '暂无在线数据'}`, time: item.lastSeen }))
].slice(0, 5);
return <Card className="v2-record-card v2-events-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
@@ -222,7 +223,7 @@ function TelemetryTimeCell({ item }: { item: LatestTelemetryValue }) {
}
function TelemetrySourceCell({ item }: { item: LatestTelemetryValue }) {
return <div className="v2-telemetry-source"><Tag color="blue" type="light" size="small">{item.protocol}</Tag><small title={item.sourceEndpoint}>{item.sourceEndpoint || '协议默认来源'}</small></div>;
return <div className="v2-telemetry-source"><Tag color="blue" type="light" size="small">{protocolDisplayLabel(item.protocol)}</Tag><small title={item.sourceEndpoint}>{item.sourceEndpoint || '协议默认来源'}</small></div>;
}
function telemetryRowKey(item?: LatestTelemetryValue) {
@@ -260,7 +261,6 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
.map((key) => ({ key, label: categoryLabels.get(key) ?? key, count: indexed.valuesByProtocolCategory.get(`${activeProtocol}\u0000${key}`)?.length ?? 0 }));
const activeCategory = indexed.valuesByProtocolCategory.has(`${activeProtocol}\u0000${selectedCategory}`) ? selectedCategory : categories[0]?.key ?? '';
const visibleMetrics = indexed.valuesByProtocolCategory.get(`${activeProtocol}\u0000${activeCategory}`) ?? [];
const protocolLabel = (protocol: string) => protocol === 'GB32960' ? 'GB/T 32960' : protocol === 'JT808' ? 'JT/T 808' : protocol === 'YUTONG_MQTT' ? '宇通 MQTT' : protocol;
const columns = [
{ title: '字段 / 协议映射', dataIndex: 'label', width: 260, render: (_: unknown, item: LatestTelemetryValue) => <TelemetryFieldCell item={item} /> },
{ title: '当前值', dataIndex: 'value', width: 170, render: (_: unknown, item: LatestTelemetryValue) => <TelemetryValueCell item={item} onInspect={setInspectedValue} /> },
@@ -296,11 +296,11 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
meta={`${data?.values.length ?? 0}`}
/>
<div className="v2-telemetry-tabs">
<SegmentedTabs className="v2-telemetry-protocols" variant="filled" ariaLabel="数据协议" value={activeProtocol} onChange={(protocol) => { setSelectedProtocol(protocol); setSelectedCategory('vehicle'); }} items={indexed.protocols.map((protocol) => ({ key: protocol, label: protocolLabel(protocol), count: (data?.values ?? []).filter((value) => value.protocol === protocol).length }))} />
<SegmentedTabs className="v2-telemetry-protocols" variant="filled" ariaLabel="数据协议" value={activeProtocol} onChange={(protocol) => { setSelectedProtocol(protocol); setSelectedCategory('vehicle'); }} items={indexed.protocols.map((protocol) => ({ key: protocol, label: protocolDisplayLabel(protocol), count: (data?.values ?? []).filter((value) => value.protocol === protocol).length }))} />
<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}>{protocolDisplayLabel(source.protocol)}</span>)}<small> {data?.scannedFrames ?? 0} · {formatTelemetryTime(data?.asOf)}</small></footer>
<SideSheet
className="v2-telemetry-value-sidesheet"
visible={Boolean(inspectedValue)}
@@ -315,7 +315,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
size="small"
row
data={[
{ key: '协议', value: inspectedValue.protocol },
{ key: '协议', value: protocolDisplayLabel(inspectedValue.protocol) },
{ key: '数据类型', value: inspectedValue.valueType || '—' },
{ key: '单位', value: inspectedValue.unit || '—' },
{ key: '质量', value: telemetryQualityLabel(inspectedValue.quality) },
@@ -467,7 +467,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
description={<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>}
meta={<span className="v2-identity-meta">
<span className="v2-identity-model"><small> / </small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span>
<span className="v2-identity-source-context"><small></small><span 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>}</span></span>
<span className="v2-identity-source-context"><small></small><span className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <Tag key={source} color="blue" type="light" size="small">{protocolDisplayLabel(source)}</Tag>) : <Tag color="grey" type="light" size="small"></Tag>}</span></span>
</span>}
actions={<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>)}
@@ -484,7 +484,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<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 className="is-text" title={realtime?.primaryProtocol}>{protocolDisplayLabel(realtime?.primaryProtocol)}</strong><span title={realtime?.locationSource}>{protocolSourceLabel(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} />