修复图表时间轴不全与城市决策卡模型补齐卡顿
- 后端 Open-Meteo 请求加 past_days=1,小时数据从 00:00 开始 - 前端图表层填充完整 00:00-23:00 时间轴,缺失时段置空 - 城市深度分析门槛从 >1 降为 >=1,单模型城市不再被拦 - hydration 队列加最大重试 3 次,防止永久卡在等待模型补齐 - 移除右侧面板历史对账按钮
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 默认行为漂移
|
||||||
|
|||||||
Reference in New Issue
Block a user