refine Semi UI vehicle workspace

This commit is contained in:
lingniu
2026-07-18 10:00:15 +08:00
parent 04d5992252
commit c1c35f8962
5 changed files with 222 additions and 75 deletions

View File

@@ -4,7 +4,7 @@ import {
IconChevronRight, IconMapPin, IconSearch, IconTickCircle
} from '@douyinfe/semi-icons';
import { Button, Card, Descriptions, Empty, Input, List, Select, SideSheet, Table, Tag } from '@douyinfe/semi-ui';
import { FormEvent, lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { FormEvent, lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { LatestTelemetryResponse, LatestTelemetryValue, QualityIssueRow, VehicleDetail, VehicleRealtimeRow } from '../../api/types';
@@ -35,11 +35,28 @@ const SINGLE_VEHICLE_REFRESH_MS = 10_000;
const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace'));
function resetWorkspaceScroll() {
const content = document.querySelector<HTMLElement>('.v2-content');
if (!content) return;
content.scrollTop = 0;
content.scrollLeft = 0;
content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
const targets = [
document.scrollingElement,
document.documentElement,
document.body,
document.querySelector<HTMLElement>('.v2-shell'),
document.querySelector<HTMLElement>('.v2-main'),
document.querySelector<HTMLElement>('.v2-content')
];
for (const target of targets) {
if (!(target instanceof HTMLElement)) continue;
target.scrollTop = 0;
target.scrollLeft = 0;
target.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
}
}
function revealVehicleIdentity(root: HTMLElement | null) {
resetWorkspaceScroll();
if (!root) return;
root.scrollTop = 0;
root.scrollLeft = 0;
root.scrollIntoView?.({ behavior: 'auto', block: 'start', inline: 'nearest' });
}
function eventTimeLabel(value?: string) {
@@ -394,6 +411,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
const { session } = usePlatformSession();
const navigate = useNavigate();
const mobileLayout = useMobileLayout();
const recordRootRef = useRef<HTMLDivElement>(null);
const [sourceEvidenceOpen, setSourceEvidenceOpen] = useState(false);
const realtime = liveRealtime ?? detail.realtimeSummary;
const identity = detail.identity;
@@ -406,7 +424,33 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
...(hasMenu(session, 'statistics') ? [{ key: 'statistics', label: '里程查询', icon: <IconClock />, to: withMonitorReturn(`/statistics?vins=${encodeURIComponent(detail.vin)}`, monitorReturn), type: 'tertiary' as const }] : []),
...(hasMenu(session, 'alerts') ? [{ key: 'alerts', label: '告警事件', icon: <IconAlarm />, to: `/alerts?vin=${encodeURIComponent(detail.vin)}`, type: 'tertiary' as const }] : [])
];
return <div className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`}>
useLayoutEffect(() => {
const root = recordRootRef.current;
if (!root) return;
let cancelled = false;
const timers: number[] = [];
const revealIdentity = () => {
if (!cancelled) revealVehicleIdentity(root);
};
const cancelEntryReset = () => {
cancelled = true;
for (const timer of timers) window.clearTimeout(timer);
};
const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;
for (const eventName of interactionEvents) window.addEventListener(eventName, cancelEntryReset, { capture: true, passive: true });
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));
return () => {
cancelled = true;
window.cancelAnimationFrame(frame);
for (const timer of timers) window.clearTimeout(timer);
for (const eventName of interactionEvents) window.removeEventListener(eventName, cancelEntryReset, true);
};
}, [detail.vin]);
return <div ref={recordRootRef} className={`v2-vehicle-record-page${mobileLayout ? ' is-mobile-layout' : ''}`}>
<MonitorReturnBar />
<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">