import { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { AlertTriangle, ChevronLeft, Clock3, MapPinned, Maximize2, Minimize2, SlidersHorizontal } from 'lucide-react'; import AmapHeatmapCanvas from './AmapHeatmapCanvas'; import HeatmapDetailPanel from './HeatmapDetailPanel'; import HeatmapFilters from './HeatmapFilters'; import { fetchAmapConfig, fetchHeatmapMeta, fetchHeatmapPoints, fetchNearbyVehicles } from './api'; import type { AmapConfig, HeatmapMeta, HeatmapMetric, HeatmapResponse, NearbyResponse } from './types'; const DEFAULT_START = '2026-01-01'; const DEFAULT_END = '2026-07-13'; const numberFormat = new Intl.NumberFormat('zh-CN'); function Metric({ value, label }: { value: number; label: string }) { return (

{numberFormat.format(value)}

{label}

); } export default function VehicleHeatmapModule() { const [meta, setMeta] = useState(null); const [config, setConfig] = useState(null); const [data, setData] = useState(null); const [nearby, setNearby] = useState(null); const [startDate, setStartDate] = useState(DEFAULT_START); const [endDate, setEndDate] = useState(DEFAULT_END); const [query, setQuery] = useState(''); const [batchModel, setBatchModel] = useState(''); const [metric, setMetric] = useState('locations'); const [isFullscreen, setIsFullscreen] = useState(false); const [showFilters, setShowFilters] = useState(() => typeof window === 'undefined' || !window.matchMedia('(max-width: 767px)').matches); const [showDetails, setShowDetails] = useState(() => typeof window === 'undefined' || !window.matchMedia('(max-width: 767px)').matches); const [loading, setLoading] = useState(true); const [nearbyLoading, setNearbyLoading] = useState(false); const [error, setError] = useState(''); const deferredQuery = useDeferredValue(query.trim()); useEffect(() => { if (!isFullscreen) return undefined; const previousOverflow = document.body.style.overflow; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setIsFullscreen(false); }; document.body.style.overflow = 'hidden'; document.addEventListener('keydown', handleKeyDown); return () => { document.body.style.overflow = previousOverflow; document.removeEventListener('keydown', handleKeyDown); }; }, [isFullscreen]); const toggleFullscreen = useCallback(() => { setIsFullscreen((current) => !current); }, []); useEffect(() => { const controller = new AbortController(); Promise.all([fetchHeatmapMeta(controller.signal), fetchAmapConfig(controller.signal)]) .then(([nextMeta, nextConfig]) => { setMeta(nextMeta); setConfig(nextConfig); setStartDate(nextMeta.startDate); setEndDate(nextMeta.endDate); }) .catch((requestError) => { if (requestError?.name !== 'AbortError') setError('热力图基础配置加载失败'); }); return () => controller.abort(); }, []); useEffect(() => { const controller = new AbortController(); const timer = window.setTimeout(() => { setLoading(true); setError(''); fetchHeatmapPoints({ startDate, endDate, query: deferredQuery, batchModel, metric }, controller.signal) .then((response) => { setData(response); setNearby(null); }) .catch((requestError) => { if (requestError?.name !== 'AbortError') setError('热力图数据加载失败,请稍后重试'); }) .finally(() => setLoading(false)); }, 220); return () => { window.clearTimeout(timer); controller.abort(); }; }, [batchModel, deferredQuery, endDate, metric, startDate]); const handleMapClick = useCallback((longitude: number, latitude: number) => { const controller = new AbortController(); setNearbyLoading(true); fetchNearbyVehicles({ lng: longitude, lat: latitude, startDate, endDate, query: deferredQuery, batchModel }, controller.signal) .then(setNearby) .catch((requestError) => { if (requestError?.name !== 'AbortError') setError('区域车辆明细加载失败'); }) .finally(() => setNearbyLoading(false)); }, [batchModel, deferredQuery, endDate, startDate]); const reset = () => { setStartDate(meta?.startDate || DEFAULT_START); setEndDate(meta?.endDate || DEFAULT_END); setQuery(''); setBatchModel(''); setMetric('locations'); setNearby(null); }; const detail = useMemo(() => { if (nearby) { return { title: '选中区域', subtitle: `${nearby.center.lng.toFixed(3)}, ${nearby.center.lat.toFixed(3)} · 半径 ${nearby.radiusKm} 公里`, locationCount: nearby.locationCount, vehicleCount: nearby.vehicleCount, vehicles: nearby.topVehicles, selected: true, }; } return { title: deferredQuery ? '车辆定位概览' : '全国 · 当前范围', subtitle: `${batchModel ? `${batchModel} · ` : ''}${startDate} ~ ${endDate}`, locationCount: data?.locationCount || 0, vehicleCount: data?.vehicleCount || 0, vehicles: data?.topVehicles || [], selected: false, }; }, [batchModel, data, deferredQuery, endDate, nearby, startDate]); return (

车辆运营热力图

每日每车首个有效定位 · 上海时区

数据更新至 {meta?.endDate || DEFAULT_END}

{config ? ( ) :
} {showFilters ? (
setShowFilters(false)} />
) : ( )} {!showDetails ? ( ) : null}
{metric === 'locations' ? '定位活跃度' : '车辆覆盖度'}{loading ? : null}

{metric === 'locations' ? '同一网格累计每日首个定位,同车跨天重复计数;对数平滑强度。' : '同一网格按 VIN 去重,同车仅计 1 辆;平方根平滑强度。'}

相对较低相对较高
{error ? (
{error}
) : null}
{showDetails ? ( setNearby(null)} onHide={() => setShowDetails(false)} /> ) : null}
); }