refine Semi UI vehicle flow

This commit is contained in:
lingniu
2026-07-18 21:33:32 +08:00
parent 22f1548f8a
commit 911addd891
6 changed files with 75 additions and 18 deletions

View File

@@ -19,6 +19,7 @@ import { FleetMap } from '../map/FleetMap';
import { InlineError, PageLoading } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { PlatformTime } from '../shared/PlatformTime';
import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
@@ -223,7 +224,7 @@ function TelemetryTimeCell({ item }: { item: LatestTelemetryValue }) {
}
function TelemetrySourceCell({ item }: { item: LatestTelemetryValue }) {
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>;
return <div className="v2-telemetry-source"><ProtocolTag protocol={item.protocol} compact /><small title={item.sourceEndpoint}>{item.sourceEndpoint || '协议默认来源'}</small></div>;
}
function telemetryRowKey(item?: LatestTelemetryValue) {
@@ -431,27 +432,41 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
useLayoutEffect(() => {
const root = recordRootRef.current;
if (!root) return;
let cancelled = false;
const scrollOwner = root.closest<HTMLElement>('.v2-content') ?? document.querySelector<HTMLElement>('.v2-content');
let active = true;
let resetFrame = 0;
const timers: number[] = [];
const revealIdentity = () => {
if (!cancelled) revealVehicleIdentity(root);
if (active) revealVehicleIdentity(root);
};
const cancelEntryReset = () => {
cancelled = true;
const stopEntryGuard = () => {
if (!active) return;
active = false;
window.cancelAnimationFrame(resetFrame);
for (const timer of timers) window.clearTimeout(timer);
resizeObserver?.disconnect();
scrollOwner?.removeEventListener('scroll', restoreEntryPosition);
for (const eventName of interactionEvents) window.removeEventListener(eventName, stopEntryGuard, true);
};
const restoreEntryPosition = () => {
if (!active || !scrollOwner || scrollOwner.scrollTop === 0) return;
window.cancelAnimationFrame(resetFrame);
resetFrame = window.requestAnimationFrame(revealIdentity);
};
const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;
for (const eventName of interactionEvents) window.addEventListener(eventName, cancelEntryReset, { capture: true, passive: true });
for (const eventName of interactionEvents) window.addEventListener(eventName, stopEntryGuard, { capture: true, passive: true });
scrollOwner?.addEventListener('scroll', restoreEntryPosition, { passive: true });
const resizeObserver = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(revealIdentity);
resizeObserver?.observe(root);
revealIdentity();
const frame = window.requestAnimationFrame(revealIdentity);
// The map and live evidence arrive independently. Keep the identity band
// anchored only during initial settlement, then immediately yield to input.
for (const delay of [80, 180, 360, 720, 1200, 1800, 2400]) timers.push(window.setTimeout(revealIdentity, delay));
// Map tiles, source evidence and live data settle independently. Guard the
// first viewport through those late layout changes, but yield immediately
// as soon as the user starts scrolling or interacting.
for (const delay of [80, 180, 360, 720, 1200, 1800, 2400, 3600, 5200]) timers.push(window.setTimeout(revealIdentity, delay));
return () => {
cancelled = true;
window.cancelAnimationFrame(frame);
for (const timer of timers) window.clearTimeout(timer);
for (const eventName of interactionEvents) window.removeEventListener(eventName, cancelEntryReset, true);
stopEntryGuard();
};
}, [detail.vin]);
return <div ref={recordRootRef} className={`v2-vehicle-record-page v2-vehicle-record-v3${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}>
@@ -467,7 +482,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">{protocolDisplayLabel(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) => <ProtocolTag key={source} protocol={source} compact />) : <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 +499,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<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><strong className="is-text" title={realtime?.primaryProtocol}>{protocolDisplayLabel(realtime?.primaryProtocol)}</strong><span title={realtime?.locationSource}>{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}</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>
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />