diff --git a/web/app.py b/web/app.py index fc0360e5..150af51d 100644 --- a/web/app.py +++ b/web/app.py @@ -359,6 +359,34 @@ def _analyze(city: str) -> Dict[str, Any]: } + # ── 15. Extended Multi-Model Daily ── + multi_model_daily = {} + mm_daily_raw = mm.get("daily_forecasts", {}) + for i, d_str in enumerate(dates): + if i == 0: + day_m = current_forecasts.copy() + d_val, d_winfo = deb_val, deb_weights + else: + day_m = mm_daily_raw.get(d_str, {}).copy() + if i < len(maxtemps) and maxtemps[i] is not None: + day_m["Open-Meteo"] = _sf(maxtemps[i]) + + d_val, d_winfo = None, "" + if day_m: + try: + blended, winfo = calculate_dynamic_weights(city, day_m) + if blended is not None: + d_val = blended + d_winfo = winfo + except Exception: + pass + + if day_m: + multi_model_daily[d_str] = { + "models": day_m, + "deb": {"prediction": d_val, "weights_info": d_winfo} + } + # ── Assemble result ── result = { "name": city, @@ -402,6 +430,7 @@ def _analyze(city: str) -> Dict[str, Any]: "sunshine_hours": sunshine_h, }, "multi_model": {k: v for k, v in current_forecasts.items() if v is not None}, + "multi_model_daily": multi_model_daily, "deb": {"prediction": deb_val, "weights_info": deb_weights}, "ensemble": ens_data, "probabilities": { diff --git a/web/static/app.js b/web/static/app.js index 589456bc..6068358a 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -38,6 +38,7 @@ function saveCache() { let selectedCity = null; let tempChart = null; const AUTO_REFRESH_MS = 60 * 60 * 1000; // 1 hour +let selectedForecastDate = null; // ────────────────────────────────────────────────────────── // Map Setup @@ -231,6 +232,7 @@ async function fetchCityDetail(cityName) { // ────────────────────────────────────────────────────────── async function loadCityDetail(cityName) { selectedCity = cityName; + selectedForecastDate = null; // Reset selection for new city setActiveCityItem(cityName); setSelectedMarker(cityName); @@ -316,9 +318,11 @@ function renderPanel(data) { renderChart(data); // Probabilities renderProbabilities(data); - // Multi-model + // Multi-model & Forecast synchronization + if (!selectedForecastDate) { + selectedForecastDate = data.local_date; + } renderModels(data); - // Forecast renderForecast(data); // AI renderAI(data); @@ -653,8 +657,18 @@ function renderProbabilities(data) { function renderModels(data) { const container = document.getElementById("modelBars"); - const models = data.multi_model || {}; - const deb = data.deb?.prediction; + const targetDate = selectedForecastDate || data.local_date; + + let models = {}; + let deb = null; + + if (data.multi_model_daily && data.multi_model_daily[targetDate]) { + models = data.multi_model_daily[targetDate].models || {}; + deb = data.multi_model_daily[targetDate].deb?.prediction; + } else { + models = data.multi_model || {}; + deb = data.deb?.prediction; + } if (Object.keys(models).length === 0) { container.innerHTML = @@ -717,9 +731,13 @@ function renderForecast(data) { let html = ""; daily.forEach((d, i) => { const isToday = i === 0; - const dateLabel = isToday ? "今天" : d.date.substring(5); + const isSelected = d.date === selectedForecastDate; + const dateLabel = isToday ? "今天" : d.date.substring(5).replace("-", "/"); + html += ` -
+
${dateLabel}
${d.max_temp}${sym}
@@ -737,6 +755,17 @@ function renderForecast(data) { sunEl.innerHTML = parts.map((p) => `${p}`).join(""); } +function switchForecastDate(cityName, dateStr) { + if (selectedCity !== cityName) return; + selectedForecastDate = dateStr; + + const data = cityDataCache[cityName]; + if (data) { + renderModels(data); + renderForecast(data); + } +} + function renderAI(data) { const container = document.getElementById("aiAnalysis"); const text = data.ai_analysis || ""; diff --git a/web/static/style.css b/web/static/style.css index 0e11ee63..a79470ac 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -768,6 +768,15 @@ body { color: var(--accent-cyan); } +.forecast-day.selected { + border-color: var(--accent-cyan); + background: rgba(34, 211, 238, 0.1); + box-shadow: 0 0 12px rgba(34, 211, 238, 0.2); +} +.forecast-day.selected .f-date { + color: var(--accent-cyan); +} + .sun-info { margin-top: 10px; font-size: 12px;