fix: insights crash, dup grids, .map() guards
Add /api/insights/cards endpoint with proper card format matching frontend expectations. Fixes "Cannot read properties of undefined (reading 'map')" crash. Switch frontend to use new endpoint. Replace alert marker rectangles with circleMarkers so they don't look like a second grid. Default showAlertMarkers to false. Add || [] guards on data.features.map() and alerts.map(). Reduce RiskMap grid count 3000→1500, debounce 150ms→300ms.
This commit is contained in:
@@ -132,24 +132,20 @@ function AlertMapComponent({
|
||||
}
|
||||
|
||||
const isP1 = alert.priority === 'P1';
|
||||
const latHalf = 0.00045;
|
||||
const lonHalf = 0.00052;
|
||||
|
||||
const rect = L.rectangle(
|
||||
[
|
||||
[alert.latitude - latHalf, alert.longitude - lonHalf],
|
||||
[alert.latitude + latHalf, alert.longitude + lonHalf],
|
||||
],
|
||||
const marker = L.circleMarker(
|
||||
[alert.latitude, alert.longitude],
|
||||
{
|
||||
radius: isP1 ? 6 : 4,
|
||||
fillColor: isP1 ? '#ef4444' : '#f97316',
|
||||
fillOpacity: 0.4,
|
||||
fillOpacity: 0.7,
|
||||
color: isP1 ? '#ef4444' : '#f97316',
|
||||
weight: 2,
|
||||
dashArray: isP1 ? undefined : '4 2',
|
||||
}
|
||||
);
|
||||
|
||||
rect.bindTooltip(
|
||||
marker.bindTooltip(
|
||||
`<div style="font-size:12px;">
|
||||
<strong>${alert.priority}</strong> · ${(alert.risk_value * 100).toFixed(0)}%<br/>
|
||||
${alert.region || ''} ${alert.street || ''}
|
||||
@@ -157,11 +153,11 @@ function AlertMapComponent({
|
||||
{ direction: 'top', offset: [0, -5] }
|
||||
);
|
||||
|
||||
rect.on('click', () => {
|
||||
marker.on('click', () => {
|
||||
if (alert.grid_id) clickHandlerRef.current(alert.grid_id);
|
||||
});
|
||||
|
||||
rect.addTo(layer);
|
||||
marker.addTo(layer);
|
||||
}
|
||||
|
||||
layer.addTo(map);
|
||||
|
||||
@@ -298,6 +298,13 @@ export function LodGridLayer({
|
||||
});
|
||||
};
|
||||
|
||||
// Debounced full redraw (avoid thrashing during rapid pan/zoom)
|
||||
let redrawTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const debouncedRedraw = () => {
|
||||
if (redrawTimer) clearTimeout(redrawTimer);
|
||||
redrawTimer = setTimeout(redraw, 300);
|
||||
};
|
||||
|
||||
// During pan: apply CSS transform to track tile movement (fixes drift)
|
||||
const onMove = () => {
|
||||
const drawn = drawnOriginRef.current;
|
||||
@@ -312,11 +319,11 @@ export function LodGridLayer({
|
||||
canvas.style.transform = `translate(${dx}px, ${dy}px)`;
|
||||
};
|
||||
|
||||
// On moveend/zoomend: reset transform and do full redraw
|
||||
// On moveend/zoomend: reset transform and do debounced full redraw
|
||||
const onMoveEnd = () => {
|
||||
canvas.style.transform = '';
|
||||
drawnOriginRef.current = null;
|
||||
redraw();
|
||||
debouncedRedraw();
|
||||
};
|
||||
|
||||
const onResize = () => redraw();
|
||||
@@ -339,6 +346,7 @@ export function LodGridLayer({
|
||||
map.off('resize', onResize);
|
||||
map.off('click', handleMapClick);
|
||||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||||
if (redrawTimer) clearTimeout(redrawTimer);
|
||||
pane.removeChild(canvas);
|
||||
if (pane.parentNode) pane.parentNode.removeChild(pane);
|
||||
canvasRef.current = null;
|
||||
|
||||
@@ -134,8 +134,8 @@ function RiskMapComponent(props: RiskMapProps) {
|
||||
cellSize = 0.01;
|
||||
step = 2;
|
||||
} else {
|
||||
cellSize = 0.001;
|
||||
step = 1;
|
||||
cellSize = 0.005;
|
||||
step = 2;
|
||||
}
|
||||
|
||||
const bounds = map.getBounds();
|
||||
@@ -151,7 +151,11 @@ function RiskMapComponent(props: RiskMapProps) {
|
||||
const currentGridMap = gridMap;
|
||||
|
||||
let count = 0;
|
||||
const maxCount = 3000;
|
||||
const maxCount = 1500;
|
||||
|
||||
if (currentGridMap.size > 5000) {
|
||||
console.warn(`[RiskMap] Data too dense: ${currentGridMap.size} grid cells, rendering may be slow`);
|
||||
}
|
||||
|
||||
for (let lat = latStart; lat < maxLat && count < maxCount; lat += cellSize * step) {
|
||||
for (let lon = lonStart; lon < maxLon && count < maxCount; lon += cellSize * step) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export function AlertsDashboard() {
|
||||
const [selectedPriority, setSelectedPriority] = useState<'all' | 'P1' | 'P2'>('all');
|
||||
const [sortBy, setSortBy] = useState<'risk' | 'time'>('risk');
|
||||
const [showMap, setShowMap] = useState(true);
|
||||
const [showAlertMarkers, setShowAlertMarkers] = useState(true);
|
||||
const [showAlertMarkers, setShowAlertMarkers] = useState(false);
|
||||
const [selectedAlert, setSelectedAlert] = useState<string | null>(null);
|
||||
const [riskRange, setRiskRange] = useState<[number, number]>([0.6, 1.0]);
|
||||
const [debouncedRiskRange, setDebouncedRiskRange] = useState<[number, number]>([0.6, 1.0]);
|
||||
@@ -59,7 +59,7 @@ export function AlertsDashboard() {
|
||||
}, [fetchRiskMap, fetchAlerts]);
|
||||
|
||||
const extendedAlerts: ExtendedAlert[] = useMemo(() => {
|
||||
return alerts.map((alert) => {
|
||||
return (alerts || []).map((alert) => {
|
||||
const forecastDate = new Date(alert.forecast_time);
|
||||
const now = new Date();
|
||||
const diffDays = Math.ceil((forecastDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
@@ -54,28 +54,28 @@ export function Insights() {
|
||||
? [
|
||||
{
|
||||
label: '总洞察数',
|
||||
value: insights.total_insights,
|
||||
value: insights.total_insights || 0,
|
||||
icon: Lightbulb,
|
||||
color: 'text-primary',
|
||||
bg: 'bg-primary-muted',
|
||||
},
|
||||
{
|
||||
label: '预警',
|
||||
value: insights.warning_count + ((insights as any).danger_count || 0),
|
||||
value: (insights.warning_count || 0) + ((insights as any).danger_count || 0),
|
||||
icon: AlertTriangle,
|
||||
color: 'text-warning',
|
||||
bg: 'bg-warning-light',
|
||||
},
|
||||
{
|
||||
label: '正常',
|
||||
value: insights.success_count,
|
||||
value: insights.success_count || 0,
|
||||
icon: CheckCircle,
|
||||
color: 'text-success',
|
||||
bg: 'bg-success-light',
|
||||
},
|
||||
{
|
||||
label: '信息',
|
||||
value: insights.info_count,
|
||||
value: insights.info_count || 0,
|
||||
icon: Info,
|
||||
color: 'text-primary',
|
||||
bg: 'bg-primary-muted',
|
||||
@@ -130,7 +130,7 @@ export function Insights() {
|
||||
|
||||
{insights && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{insights.cards.map((card) => {
|
||||
{(insights.cards || []).map((card) => {
|
||||
const config = TYPE_CONFIG[card.type];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
|
||||
@@ -198,6 +198,7 @@ export const analysisApi = {
|
||||
|
||||
export const insightsApi = {
|
||||
getOverview: (): Promise<any> => cachedGet('/insights/overview'),
|
||||
getCards: (): Promise<any> => cachedGet('/insights/cards'),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -107,7 +107,7 @@ export const useAnalysisStore = create<AnalysisState>((set, get) => ({
|
||||
fetchInsights: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const data = await insightsApi.getOverview();
|
||||
const data = await insightsApi.getCards();
|
||||
set({ insights: data, isLoading: false });
|
||||
} catch (e) {
|
||||
if (isCancelError(e)) return;
|
||||
|
||||
@@ -191,7 +191,7 @@ export const useMonitoringStore = create<MonitoringState>((set) => ({
|
||||
try {
|
||||
const data = await gridApi.getGridsGeoJSON(date);
|
||||
|
||||
const features: GridFeature[] = data.features.map((f: any) => ({
|
||||
const features: GridFeature[] = (data.features || []).map((f: any) => ({
|
||||
grid_id: f.properties.grid_id,
|
||||
latitude: f.properties.latitude,
|
||||
longitude: f.properties.longitude,
|
||||
|
||||
Reference in New Issue
Block a user