refine Semi UI vehicle flow
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user