修复图表时间轴不全与城市决策卡模型补齐卡顿

- 后端 Open-Meteo 请求加 past_days=1,小时数据从 00:00 开始
- 前端图表层填充完整 00:00-23:00 时间轴,缺失时段置空
- 城市深度分析门槛从 >1 降为 >=1,单模型城市不再被拦
- hydration 队列加最大重试 3 次,防止永久卡在等待模型补齐
- 移除右侧面板历史对账按钮
This commit is contained in:
2569718930@qq.com
2026-05-16 20:14:05 +08:00
parent b27bedbca3
commit 2e02133696
5 changed files with 37 additions and 38 deletions
+5 -30
View File
@@ -6,7 +6,6 @@ import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { ForecastTable } from "@/components/dashboard/PanelSections"; import { ForecastTable } from "@/components/dashboard/PanelSections";
import { import {
useDashboardHistory,
useDashboardModal, useDashboardModal,
useDashboardStore, useDashboardStore,
useProAccess, useProAccess,
@@ -38,7 +37,6 @@ export function DetailPanel({
} = {}) { } = {}) {
const store = useDashboardStore(); const store = useDashboardStore();
const modal = useDashboardModal(); const modal = useDashboardModal();
const history = useDashboardHistory();
const { proAccess } = useProAccess(); const { proAccess } = useProAccess();
const { locale, t } = useI18n(); const { locale, t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -76,7 +74,7 @@ export function DetailPanel({
); );
const [heavyContentReady, setHeavyContentReady] = useState(false); const [heavyContentReady, setHeavyContentReady] = useState(false);
const isOverlayOpen = const isOverlayOpen =
Boolean(modal.futureModalDate) || history.historyState.isOpen; Boolean(modal.futureModalDate);
const isVisible = isRail const isVisible = isRail
? Boolean(store.selectedCity) && !isOverlayOpen ? Boolean(store.selectedCity) && !isOverlayOpen
: store.isPanelOpen && Boolean(store.selectedCity) && !isOverlayOpen; : store.isPanelOpen && Boolean(store.selectedCity) && !isOverlayOpen;
@@ -139,24 +137,20 @@ export function DetailPanel({
} }
}; };
const handleFeatureAccess = (feature: "today" | "history") => { const handleTodayAccess = () => {
blurActiveElement(); blurActiveElement();
if (!isPro) { if (!isPro) {
trackAppEvent("paywall_feature_clicked", { trackAppEvent("paywall_feature_clicked", {
entry: "detail_panel", entry: "detail_panel",
feature, feature: "today",
city: store.selectedCity, city: store.selectedCity,
user_state: isAuthenticated ? "logged_in" : "guest", user_state: isAuthenticated ? "logged_in" : "guest",
}); });
} }
if (isPro) { if (isPro) {
if (feature === "today") { void modal.openTodayModal();
void modal.openTodayModal();
return;
}
void history.openHistory();
return; return;
} }
@@ -165,11 +159,7 @@ export function DetailPanel({
return; return;
} }
if (feature === "today") { void modal.openTodayModal();
void modal.openTodayModal();
return;
}
void history.openHistory();
}; };
useEffect(() => { useEffect(() => {
@@ -348,21 +338,6 @@ export function DetailPanel({
{locale === "en-US" ? "Market" : "市场页"} {locale === "en-US" ? "Market" : "市场页"}
</a> </a>
) : null} ) : null}
<button
type="button"
className={clsx(
"panel-action-button",
"panel-action-button-secondary",
!isPro && "pro-locked",
)}
title={
isPro ? t("detail.history") : `${t("detail.history")} (Pro)`
}
onClick={() => handleFeatureAccess("history")}
disabled={!store.selectedCity}
>
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -47,8 +47,8 @@ export function isFullEnoughForDeepAnalysis(detail?: CityDetail | null) {
return false; return false;
} }
return ( return (
countDetailModels(detail, detail.local_date) > 1 && countDetailModels(detail, detail.local_date) >= 1 &&
countDetailForecastDays(detail) > 1 countDetailForecastDays(detail) >= 1
); );
} }
@@ -30,6 +30,7 @@ export function useAiPinnedCityWorkspace({
const aiFullHydrationRef = useRef<Set<string>>(new Set()); const aiFullHydrationRef = useRef<Set<string>>(new Set());
const aiHydrationQueueRef = useRef<string[]>([]); const aiHydrationQueueRef = useRef<string[]>([]);
const aiHydrationRunningRef = useRef(false); const aiHydrationRunningRef = useRef(false);
const aiHydrationRetriesRef = useRef<Map<string, number>>(new Map());
const runAiHydrationQueue = useCallback(async () => { const runAiHydrationQueue = useCallback(async () => {
if (aiHydrationRunningRef.current) return; if (aiHydrationRunningRef.current) return;
@@ -47,10 +48,22 @@ export function useAiPinnedCityWorkspace({
"full", "full",
); );
if (!isFullEnoughForDeepAnalysis(detail)) { if (!isFullEnoughForDeepAnalysis(detail)) {
aiFullHydrationRef.current.delete(key); const retries = aiHydrationRetriesRef.current.get(key) || 0;
if (retries >= 3) {
aiFullHydrationRef.current.delete(key);
aiHydrationRetriesRef.current.delete(key);
} else {
aiHydrationRetriesRef.current.set(key, retries + 1);
}
} }
} catch { } catch {
aiFullHydrationRef.current.delete(key); const retries = aiHydrationRetriesRef.current.get(key) || 0;
if (retries >= 3) {
aiFullHydrationRef.current.delete(key);
aiHydrationRetriesRef.current.delete(key);
} else {
aiHydrationRetriesRef.current.set(key, retries + 1);
}
} }
} }
} finally { } finally {
@@ -132,6 +145,8 @@ export function useAiPinnedCityWorkspace({
aiPinnedCities.forEach((item) => { aiPinnedCities.forEach((item) => {
const key = normalizeCityKey(item.cityName); const key = normalizeCityKey(item.cityName);
if (!key || aiFullHydrationRef.current.has(key)) return; if (!key || aiFullHydrationRef.current.has(key)) return;
const retries = aiHydrationRetriesRef.current.get(key) || 0;
if (retries >= 3) return;
const detail = findDetailForCity(store.cityDetailsByName, item.cityName); const detail = findDetailForCity(store.cityDetailsByName, item.cityName);
const needsFullHydration = !isFullEnoughForDeepAnalysis(detail); const needsFullHydration = !isFullEnoughForDeepAnalysis(detail);
if (!needsFullHydration) return; if (!needsFullHydration) return;
+12 -4
View File
@@ -376,10 +376,18 @@ export function getTemperatureChartData(
value: Number(rawTemps[index]), value: Number(rawTemps[index]),
})) }))
.filter((entry) => entry.tail !== ""); .filter((entry) => entry.tail !== "");
const times = validEntries.map((entry) => entry.tail); const dataByHour = new Map<string, number | null>();
const temps = validEntries.map((entry) => validEntries.forEach((entry) => {
Number.isFinite(entry.value) ? entry.value : null, dataByHour.set(entry.tail, Number.isFinite(entry.value) ? entry.value : null);
); });
const times: string[] = [];
const temps: Array<number | null> = [];
for (let h = 0; h < 24; h++) {
const hh = String(h).padStart(2, "0");
const key = `${hh}:00`;
times.push(key);
temps.push(dataByHour.has(key) ? dataByHour.get(key)! : null);
}
const suppressAnkaraMgmObservation = isTurkishMgmCity(detail); const suppressAnkaraMgmObservation = isTurkishMgmCity(detail);
if (!times.length) return null; if (!times.length) return null;
@@ -386,6 +386,7 @@ class NwsOpenMeteoSourceMixin:
"daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration", "daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration",
"timezone": "auto", "timezone": "auto",
"forecast_days": forecast_days, "forecast_days": forecast_days,
"past_days": 1,
} }
# 显式指定单位,防止 API 默认行为漂移 # 显式指定单位,防止 API 默认行为漂移