feat: Implement the PolyWeather dashboard including frontend components, data collection, analysis, and API endpoints.

This commit is contained in:
2569718930@qq.com
2026-03-10 04:45:40 +08:00
parent 020c62676e
commit aab4477ab3
24 changed files with 2835 additions and 524 deletions
+66 -63
View File
@@ -110,6 +110,7 @@ export function useLeafletMap({
const nearbyLayerRef = useRef<L.LayerGroup | null>(null);
const autoNearbyCityRef = useRef<string | null>(null);
const loadingAutoNearbyRef = useRef(false);
const handlingAutoNearbyRef = useRef(false);
const lastMovedCityRef = useRef<string | null>(null);
const suspendMotionRef = useRef(suspendMotion);
const hasFittedInitialBoundsRef = useRef(false);
@@ -335,77 +336,79 @@ export function useLeafletMap({
}
async function maybeAutoShowNearbyStations() {
if (suspendMotion) {
map.stop();
if (handlingAutoNearbyRef.current) {
return;
}
if (selectedDetail) {
// Just render stations, no camera move from here
renderNearbyStations(selectedDetail, true);
return;
}
// If no city selected, reset the move tracker
lastMovedCityRef.current = null;
if (map.getZoom() < AUTO_NEARBY_MIN_ZOOM) {
autoNearbyCityRef.current = null;
layer.clearLayers();
return;
}
const center = map.getCenter();
let best: { cityName: string; distance: number } | null = null;
for (const [cityName, entry] of Object.entries(markersRef.current)) {
const distance = map.distance(
center,
L.latLng(entry.city.lat, entry.city.lon),
);
if (distance > AUTO_NEARBY_MAX_DISTANCE_M) continue;
if (!best || distance < best.distance) {
best = { cityName, distance };
}
}
const targetCity = best?.cityName || null;
if (!targetCity) {
autoNearbyCityRef.current = null;
layer.clearLayers();
return;
}
if (
autoNearbyCityRef.current === targetCity &&
layer.getLayers().length > 0
) {
return;
}
autoNearbyCityRef.current = targetCity;
const cachedDetail = cityDetailsByName[targetCity];
if (cachedDetail) {
renderNearbyStations(cachedDetail, true);
return;
}
if (loadingAutoNearbyRef.current) return;
loadingAutoNearbyRef.current = true;
handlingAutoNearbyRef.current = true;
try {
const detail = await onEnsureCityDetailRef.current(targetCity, false);
renderNearbyStations(detail, true);
} catch {
if (selectedDetail) {
// Just render stations, no camera move from here
renderNearbyStations(selectedDetail, true);
return;
}
if (suspendMotion) {
return;
}
// If no city selected, reset the move tracker
lastMovedCityRef.current = null;
if (map.getZoom() < AUTO_NEARBY_MIN_ZOOM) {
autoNearbyCityRef.current = null;
layer.clearLayers();
return;
}
const center = map.getCenter();
let best: { cityName: string; distance: number } | null = null;
for (const [cityName, entry] of Object.entries(markersRef.current)) {
const distance = map.distance(
center,
L.latLng(entry.city.lat, entry.city.lon),
);
if (distance > AUTO_NEARBY_MAX_DISTANCE_M) continue;
if (!best || distance < best.distance) {
best = { cityName, distance };
}
}
const targetCity = best?.cityName || null;
if (!targetCity) {
autoNearbyCityRef.current = null;
layer.clearLayers();
return;
}
if (
autoNearbyCityRef.current === targetCity &&
layer.getLayers().length > 0
) {
return;
}
autoNearbyCityRef.current = targetCity;
const cachedDetail = cityDetailsByName[targetCity];
if (cachedDetail) {
renderNearbyStations(cachedDetail, true);
return;
}
if (loadingAutoNearbyRef.current) return;
loadingAutoNearbyRef.current = true;
try {
const detail = await onEnsureCityDetailRef.current(targetCity, false);
renderNearbyStations(detail, true);
} catch {
} finally {
loadingAutoNearbyRef.current = false;
}
} finally {
loadingAutoNearbyRef.current = false;
handlingAutoNearbyRef.current = false;
}
}
const syncVisibility = () => {
if (suspendMotion) {
map.stop();
return;
}
if (map.getZoom() < 7) {
if (map.hasLayer(layer)) {
map.removeLayer(layer);