Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/hooks/useMonitorData.ts
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

230 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useEffect, useRef, useState } from 'react';
import { api } from '../../api/client';
import type { MonitorMapResponse, MonitorSummary, MonitorWorkspaceResponse, Page, VehicleRealtimeRow } from '../../api/types';
import { normalizeMonitorBounds } from '../domain/monitor';
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
export type MonitorFilters = {
keyword: string;
protocol: string;
status: string;
};
export type MonitorViewport = {
zoom: number;
bounds: string;
};
export const MONITOR_REFRESH = {
summary: 30_000,
fleet: 15_000,
selected: 10_000,
alerts: 15_000
} as const;
export const MONITOR_CACHE = { mapGcTime: 0 } as const;
export const MAX_MONITOR_SEARCH_TERMS = 100;
export const MONITOR_ADDRESS_REFRESH_MS = 60_000;
type MonitorAddressPoint = {
vin: string;
key: string;
longitude: number;
latitude: number;
};
function monitorAddressPoint(vin: string, longitude?: number, latitude?: number): MonitorAddressPoint | undefined {
if (!vin || !Number.isFinite(longitude) || !Number.isFinite(latitude)
|| Math.abs(longitude ?? 0) > 180 || Math.abs(latitude ?? 0) > 90
|| (longitude === 0 && latitude === 0)) return undefined;
const roundedLongitude = Number(longitude!.toFixed(4));
const roundedLatitude = Number(latitude!.toFixed(4));
return {
vin,
key: `${roundedLongitude.toFixed(4)},${roundedLatitude.toFixed(4)}`,
longitude: roundedLongitude,
latitude: roundedLatitude
};
}
function useMonitorAddressPoint(vin: string, longitude?: number, latitude?: number) {
const next = monitorAddressPoint(vin, longitude, latitude);
const [published, setPublished] = useState<MonitorAddressPoint>();
const pendingRef = useRef<MonitorAddressPoint>();
const lastPublishedAtRef = useRef(0);
useEffect(() => {
if (!next) {
pendingRef.current = undefined;
lastPublishedAtRef.current = 0;
setPublished((current) => current === undefined ? current : undefined);
return;
}
if (!published || published.vin !== next.vin) {
pendingRef.current = undefined;
lastPublishedAtRef.current = Date.now();
setPublished(next);
return;
}
if (published.key === next.key) {
pendingRef.current = undefined;
return;
}
pendingRef.current = next;
const elapsed = Date.now() - lastPublishedAtRef.current;
const timer = window.setTimeout(() => {
const pending = pendingRef.current;
if (!pending) return;
pendingRef.current = undefined;
lastPublishedAtRef.current = Date.now();
setPublished(pending);
}, Math.max(0, MONITOR_ADDRESS_REFRESH_MS - elapsed));
return () => window.clearTimeout(timer);
}, [next?.key, next?.vin, published?.key, published?.vin]);
return published;
}
export function parseMonitorSearchTerms(value: string) {
const terms: string[] = [];
const seen = new Set<string>();
for (const raw of value.split(/[\s,,、;;]+/)) {
const term = raw.trim();
if (!term) continue;
const key = term.toLocaleUpperCase();
if (seen.has(key)) continue;
seen.add(key);
terms.push(term.toLocaleUpperCase());
if (terms.length === MAX_MONITOR_SEARCH_TERMS) break;
}
return terms;
}
export function monitorQueryParams(filters: MonitorFilters, limit: number) {
const params = new URLSearchParams({ limit: String(limit) });
const terms = parseMonitorSearchTerms(filters.keyword);
if (terms.length === 1) params.set('keyword', terms[0]);
if (terms.length > 1) params.set('keywords', terms.join(','));
if (filters.protocol) params.set('protocol', filters.protocol);
if (filters.status) params.set('status', filters.status);
if (filters.status === 'online' || filters.status === 'offline') params.set('online', filters.status);
return params;
}
export function monitorMapQueryParams(filters: MonitorFilters, viewport: MonitorViewport) {
const params = monitorQueryParams(filters, 10_000);
params.set('zoom', String(viewport.zoom));
const bounds = normalizeMonitorBounds(viewport.bounds);
if (bounds && parseMonitorSearchTerms(filters.keyword).length === 0) params.set('bounds', bounds);
return params;
}
export function monitorFilterScope(filters: MonitorFilters) {
const params = monitorQueryParams(filters, 0);
params.delete('limit');
return params.toString();
}
function projectWorkspaceQuery<T>(query: UseQueryResult<MonitorWorkspaceResponse>, select: (workspace: MonitorWorkspaceResponse) => T): UseQueryResult<T> {
return { ...query, data: query.data ? select(query.data) : undefined } as UseQueryResult<T>;
}
export function useMonitorData(filters: MonitorFilters, viewport: MonitorViewport, selectedVin: string, mapEnabled = true, vehicleEnabled = true) {
const params = monitorQueryParams(filters, 200);
const mapParams = monitorMapQueryParams(filters, viewport);
const filterScope = monitorFilterScope(filters);
mapParams.set('railLimit', '200');
const workspace = useQuery<MonitorWorkspaceResponse>({
queryKey: ['monitor', 'workspace', filterScope, mapParams.toString()],
queryFn: ({ signal }) => api.monitorWorkspace(mapParams, signal),
enabled: mapEnabled,
placeholderData: retainPreviousPageWithinScope<MonitorWorkspaceResponse>(filterScope, 2),
staleTime: 5_000,
gcTime: MONITOR_CACHE.mapGcTime,
refetchInterval: mapEnabled ? MONITOR_REFRESH.fleet : false,
...LIVE_QUERY_POLICY
});
const summaryQuery = useQuery({
queryKey: ['monitor', 'summary', params.toString()],
queryFn: ({ signal }) => api.monitorSummary(params, signal),
enabled: !mapEnabled,
placeholderData: workspace.data?.summary,
refetchInterval: !mapEnabled ? MONITOR_REFRESH.summary : false,
gcTime: QUERY_MEMORY.summaryGcTime,
...LIVE_QUERY_POLICY
});
const vehiclesQuery = useQuery({
queryKey: ['monitor', 'vehicles', params.toString()],
queryFn: ({ signal }) => api.vehicleRealtime(params, signal),
enabled: vehicleEnabled && !mapEnabled,
placeholderData: workspace.data?.vehicles,
refetchInterval: vehicleEnabled && !mapEnabled ? MONITOR_REFRESH.fleet : false,
gcTime: QUERY_MEMORY.highVolumeGcTime,
...LIVE_QUERY_POLICY
});
const summary = mapEnabled ? projectWorkspaceQuery<MonitorSummary>(workspace, (data) => data.summary) : summaryQuery;
const vehicles = mapEnabled ? projectWorkspaceQuery<Page<VehicleRealtimeRow>>(workspace, (data) => data.vehicles) : vehiclesQuery;
const map = projectWorkspaceQuery<MonitorMapResponse>(workspace, (data) => data.map);
const selectedVehicle = useQuery({
queryKey: ['monitor', 'selected-vehicle', selectedVin],
queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keyword: selectedVin, limit: '1', offset: '0' }), signal),
enabled: Boolean(selectedVin),
staleTime: 5_000,
refetchInterval: selectedVin ? MONITOR_REFRESH.selected : false,
gcTime: QUERY_MEMORY.highVolumeGcTime,
...LIVE_QUERY_POLICY
});
return { summary, vehicles, map, selectedVehicle };
}
export function useMonitorVehicleCard(vin: string, vehicle?: VehicleRealtimeRow, activelyTracked = false, alertsAllowed = true) {
const enabled = Boolean(vin);
const longitude = vehicle?.longitude;
const latitude = vehicle?.latitude;
const addressPoint = useMonitorAddressPoint(vin, longitude, latitude);
const detail = useQuery({
queryKey: ['monitor', 'vehicle-card', 'detail', vin],
queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin }), signal),
enabled,
staleTime: 30_000,
gcTime: QUERY_MEMORY.highVolumeGcTime
});
const activeAlerts = useQuery({
queryKey: ['monitor', 'vehicle-card', 'active-alerts', vin],
queryFn: ({ signal }) => api.alertEventsV2({ keyword: vin, status: 'active', limit: 20, offset: 0 }, signal),
enabled: enabled && alertsAllowed,
staleTime: 10_000,
refetchInterval: enabled && alertsAllowed && activelyTracked ? MONITOR_REFRESH.alerts : false,
gcTime: QUERY_MEMORY.highVolumeGcTime,
...LIVE_QUERY_POLICY
});
const telemetry = useQuery({
queryKey: ['monitor', 'vehicle-card', 'telemetry', vin],
queryFn: ({ signal }) => api.latestTelemetry(vin, signal),
enabled: enabled && vehicle?.primaryProtocol === 'GB32960',
staleTime: 10_000,
refetchInterval: enabled && activelyTracked && vehicle?.primaryProtocol === 'GB32960' ? 20_000 : false,
gcTime: QUERY_MEMORY.highVolumeGcTime,
...LIVE_QUERY_POLICY
});
const address = useQuery({
queryKey: ['monitor', 'vehicle-card', 'address', addressPoint?.vin, addressPoint?.key],
queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({
longitude: addressPoint!.longitude.toFixed(4),
latitude: addressPoint!.latitude.toFixed(4)
}), signal),
enabled: enabled && addressPoint?.vin === vin,
staleTime: 60 * 60_000,
gcTime: QUERY_MEMORY.highVolumeGcTime
});
return { detail, activeAlerts, telemetry, address };
}