perf(web): release monitor resources on unmount

This commit is contained in:
lingniu
2026-07-16 00:59:57 +08:00
parent c00f50551a
commit e80e1dcea9
9 changed files with 194 additions and 37 deletions

View File

@@ -4,15 +4,29 @@ import QRCode from 'qrcode';
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
import type { AlertEvent, MapReverseGeocode, VehicleDetail as VehicleDetailData, VehicleRealtimeRow } from '../../api/types';
import type { VehicleRealtimeRow } from '../../api/types';
import { FleetMap } from '../map/FleetMap';
import { EmptyState, InlineError } from '../shared/AsyncState';
import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
import { QUERY_MEMORY } from '../queryPolicy';
const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
const statuses = ['', 'online', 'offline', 'driving', 'idle'];
const EMPTY_VEHICLES: VehicleRealtimeRow[] = [];
const MOBILE_MONITOR_QUERY = '(max-width: 760px)';
function useMobileMonitorLayout() {
const [mobile, setMobile] = useState(() => window.matchMedia(MOBILE_MONITOR_QUERY).matches);
useEffect(() => {
const media = window.matchMedia(MOBILE_MONITOR_QUERY);
const update = () => setMobile(media.matches);
media.addEventListener('change', update);
update();
return () => media.removeEventListener('change', update);
}, []);
return mobile;
}
type AddressCoordinate = { longitude: number; latitude: number; key: string };
@@ -50,21 +64,22 @@ const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehic
});
function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) {
const mobile = useMobileMonitorLayout();
return <section className="v2-monitor-table-panel">
<header><div><strong></strong><span></span></div><b>{total.toLocaleString('zh-CN')} </b></header>
<div className="v2-monitor-table-scroll"><table><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
{!mobile ? <div className="v2-monitor-table-scroll"><table><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>{rows.map((row) => <tr key={row.vin}>
<td className="v2-monitor-table-vehicle"><button type="button" title="在地图中定位" onClick={() => onSelect(row.vin)}><strong>{row.plate || '未绑定车牌'}</strong><span>{row.vin}</span></button></td>
<td><strong className="v2-monitor-live-value">{formatNumber(row.speedKmh, 1)}</strong><small className="v2-monitor-live-unit">km/h</small></td>
<td><strong className="v2-monitor-live-value">{formatNumber(row.totalMileageKm, 1)}</strong><small className="v2-monitor-live-unit">km</small></td>
<td><code className="v2-monitor-coordinate">{row.longitude.toFixed(6)}<br />{row.latitude.toFixed(6)}</code></td>
<td><MonitorAddressCell vehicle={row} /></td>
</tr>)}</tbody></table>{loading ? <div className="v2-monitor-table-loading"><i className="v2-spinner" /></div> : null}{!loading && !rows.length ? <EmptyState /> : null}</div>
<div className="v2-monitor-mobile-cards">{rows.map((row) => <article key={row.vin}>
</tr>)}</tbody></table>{loading ? <div className="v2-monitor-table-loading"><i className="v2-spinner" /></div> : null}{!loading && !rows.length ? <EmptyState /> : null}</div> : null}
{mobile ? <div className="v2-monitor-mobile-cards">{rows.map((row) => <article key={row.vin}>
<header><div><strong>{row.plate || '未绑定车牌'}</strong><span>{row.vin}</span></div><b>{formatNumber(row.speedKmh, 1)}<small>km/h</small></b></header>
<dl><div><dt></dt><dd>{formatNumber(row.totalMileageKm, 1)} km</dd></div><div><dt></dt><dd><code>{row.longitude.toFixed(6)}, {row.latitude.toFixed(6)}</code></dd></div><div className="is-address"><dt></dt><dd><MonitorAddressCell vehicle={row} /></dd></div></dl>
<footer><button type="button" onClick={() => onSelect(row.vin)}><IconMapPin /></button></footer>
</article>)}{loading ? <div className="v2-monitor-table-loading"><i className="v2-spinner" /></div> : null}{!loading && !rows.length ? <EmptyState /> : null}</div>
</article>)}{loading ? <div className="v2-monitor-table-loading"><i className="v2-spinner" /></div> : null}{!loading && !rows.length ? <EmptyState /> : null}</div> : null}
<footer><span> {page} / {totalPages} </span><div><button type="button" disabled={page <= 1} onClick={() => onPage(page - 1)}></button><button type="button" disabled={page >= totalPages} onClick={() => onPage(page + 1)}></button><select aria-label="每页车辆数" value={limit} onChange={(event) => onLimit(Number(event.target.value))}><option value="20">20 /</option><option value="50">50 /</option><option value="100">100 /</option></select></div></footer>
</section>;
}
@@ -95,19 +110,17 @@ const MemoFleetMap = memo(FleetMap);
function VehicleDetailCard({
vehicle,
detail,
activeAlerts,
address,
onCollapse,
onClear
}: {
vehicle: VehicleRealtimeRow;
detail?: VehicleDetailData;
activeAlerts?: { items: AlertEvent[]; total: number };
address?: MapReverseGeocode;
onCollapse: () => void;
onClear: () => void;
}) {
const card = useMonitorVehicleCard(vehicle.vin, vehicle, true);
const detail = card.detail.data;
const activeAlerts = card.activeAlerts.data;
const address = card.address.data;
const status = vehicleStatus(vehicle);
const dailyMileage = detail?.mileage.items[0]?.dailyMileageKm;
const latestAlert = activeAlerts?.items[0];
@@ -188,7 +201,8 @@ export default function MonitorPage() {
const realtimeListQuery = useQuery({
queryKey: ['monitor', 'vehicle-list', listParams.toString()],
queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal),
enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false
enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
gcTime: QUERY_MEMORY.highVolumeGcTime
});
const rows = useMemo(() => {
const data = vehicles.data?.items ?? [];
@@ -234,7 +248,6 @@ export default function MonitorPage() {
const selectMapVehicle = useCallback((vehicle: VehicleRealtimeRow) => selectVehicle(vehicle.vin), [selectVehicle]);
const collapseDetail = useCallback(() => setDetailOpen(false), []);
const expandDetail = useCallback(() => setDetailOpen(true), []);
const card = useMonitorVehicleCard(selected?.vin ?? '', selected, Boolean(selectedVin));
const driving = rows.filter((vehicle) => vehicleStatus(vehicle) === 'driving').length;
const idle = rows.filter((vehicle) => vehicleStatus(vehicle) === 'idle').length;
const offline = rows.filter((vehicle) => vehicleStatus(vehicle) === 'offline').length;
@@ -307,9 +320,6 @@ export default function MonitorPage() {
{selected && detailOpen ? (
<VehicleDetailCard
vehicle={selected}
detail={card.detail.data}
activeAlerts={card.activeAlerts.data}
address={card.address.data}
onCollapse={collapseDetail}
onClear={clearSelection}
/>