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 (
{config ? (
) :
}
{showFilters ? (
setShowFilters(false)}
/>
) : (
)}
{!showDetails ? (
) : null}
{metric === 'locations' ? '定位活跃度' : '车辆覆盖度'}{loading ? : null}
{metric === 'locations'
? '同一网格累计每日首个定位,同车跨天重复计数;对数平滑强度。'
: '同一网格按 VIN 去重,同车仅计 1 辆;平方根平滑强度。'}
相对较低相对较高
{error ? (
) : null}
{showDetails ? (
setNearby(null)}
onHide={() => setShowDetails(false)}
/>
) : null}
);
}