feat: unify mobile filter sheets

This commit is contained in:
lingniu
2026-07-19 04:44:21 +08:00
parent d3eb77e2c7
commit 9a87d14d6d
11 changed files with 417 additions and 64 deletions

View File

@@ -29,7 +29,7 @@ import { useMobileLayout } from '../hooks/useMobileLayout';
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
function fmt(value?: string) { return value?.trim() || '—'; }
function metric(value: number | undefined, fallback = '—') { return typeof value === 'number' && Number.isFinite(value) ? formatZhNumber(value, 1) : fallback; }
function metric(value: number | undefined) { return Number.isFinite(value as number) ? formatZhNumber(value!, 1) : '—'; }
function availableMetric(value: number | undefined, available?: boolean) { return available === false ? '—' : metric(value); }
function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ? 'error' : 'warning'; }
function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; }
@@ -246,7 +246,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
const [copiedValue, setCopiedValue] = useState(false);
const inspectedPresentation = useMemo(() => inspectedValue ? telemetryValuePresentation(inspectedValue) : undefined, [inspectedValue]);
const inspectedDetail = useMemo(() => inspectedValue ? telemetryValueDetail(inspectedValue) : '', [inspectedValue]);
useSideSheetA11y(Boolean(inspectedValue), '.v2-telemetry-value-sidesheet', 'v2-telemetry-value-detail', '遥测字段详情', '关闭遥测字段详情');
useSideSheetA11y(!!inspectedValue, '.v2-telemetry-value-sidesheet', 'v2-telemetry-value-detail', '遥测字段详情', '关闭遥测字段详情');
const indexed = useMemo(() => {
const valuesByProtocolCategory = new Map<string, LatestTelemetryResponse['values']>();
const sources = new Map<string, { protocol: string; endpoint?: string }>();
@@ -314,7 +314,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
<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)}
visible={!!inspectedValue}
placement={mobileLayout ? 'bottom' : 'right'}
width={mobileLayout ? undefined : 520}
height={mobileLayout ? 'min(88dvh, 780px)' : undefined}
@@ -373,7 +373,7 @@ function CurrentVehicleAddress({ vehicle, fallback }: { vehicle?: VehicleRealtim
refetchOnWindowFocus: false,
retry: 1
});
const moved = Boolean(coordinateKey && requestedKey && coordinateKey !== requestedKey);
const moved = !!(coordinateKey && requestedKey && coordinateKey !== requestedKey);
return <div className="v2-current-location">
<span><IconMapPin /><small></small><strong>{coordinate ? `${vehicle!.longitude.toFixed(6)}, ${vehicle!.latitude.toFixed(6)}` : '—'}</strong></span>
@@ -433,7 +433,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
const [sourceEvidenceOpen, setSourceEvidenceOpen] = useState(false);
const realtime = liveRealtime ?? detail.realtimeSummary;
const identity = detail.identity;
const hasLocation = Boolean(realtime && realtime.locationAvailable !== false && isValidAMapCoordinate(realtime.longitude, realtime.latitude));
const hasLocation = !!(realtime && realtime.locationAvailable !== false && isValidAMapCoordinate(realtime.longitude, realtime.latitude));
const mapVehicles = hasLocation && realtime ? [realtime] : [];
const actions = [
{ key: 'switch', label: '切换车辆', icon: <IconSearch />, to: '/vehicles', type: 'primary' as const },
@@ -543,18 +543,18 @@ export default function VehiclePage() {
const { vin } = useParams();
const [searchParams] = useSearchParams();
const monitorReturn = monitorReturnFromParams(searchParams);
const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime });
const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: !!vin, queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime });
const resolvedVin = query.data?.lookupResolved ? query.data.vin : '';
const realtime = useQuery({
queryKey: ['vehicle-detail-realtime', resolvedVin],
queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keywords: resolvedVin, limit: '1', offset: '0' }), signal),
enabled: Boolean(resolvedVin),
enabled: !!resolvedVin,
staleTime: 5_000,
gcTime: QUERY_MEMORY.highVolumeGcTime,
refetchInterval: resolvedVin ? SINGLE_VEHICLE_REFRESH_MS : false,
...LIVE_QUERY_POLICY
});
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY });
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: !!vin, queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY });
useEffect(() => {
resetWorkspaceScroll();
let trailingFrame = 0;