修复图表时间轴不全与城市决策卡模型补齐卡顿
- 后端 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 { ForecastTable } from "@/components/dashboard/PanelSections";
|
||||
import {
|
||||
useDashboardHistory,
|
||||
useDashboardModal,
|
||||
useDashboardStore,
|
||||
useProAccess,
|
||||
@@ -38,7 +37,6 @@ export function DetailPanel({
|
||||
} = {}) {
|
||||
const store = useDashboardStore();
|
||||
const modal = useDashboardModal();
|
||||
const history = useDashboardHistory();
|
||||
const { proAccess } = useProAccess();
|
||||
const { locale, t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -76,7 +74,7 @@ export function DetailPanel({
|
||||
);
|
||||
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
||||
const isOverlayOpen =
|
||||
Boolean(modal.futureModalDate) || history.historyState.isOpen;
|
||||
Boolean(modal.futureModalDate);
|
||||
const isVisible = isRail
|
||||
? 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();
|
||||
|
||||
if (!isPro) {
|
||||
trackAppEvent("paywall_feature_clicked", {
|
||||
entry: "detail_panel",
|
||||
feature,
|
||||
feature: "today",
|
||||
city: store.selectedCity,
|
||||
user_state: isAuthenticated ? "logged_in" : "guest",
|
||||
});
|
||||
}
|
||||
|
||||
if (isPro) {
|
||||
if (feature === "today") {
|
||||
void modal.openTodayModal();
|
||||
return;
|
||||
}
|
||||
void history.openHistory();
|
||||
void modal.openTodayModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,11 +159,7 @@ export function DetailPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
if (feature === "today") {
|
||||
void modal.openTodayModal();
|
||||
return;
|
||||
}
|
||||
void history.openHistory();
|
||||
void modal.openTodayModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -348,21 +338,6 @@ export function DetailPanel({
|
||||
{locale === "en-US" ? "Market" : "市场页"}
|
||||
</a>
|
||||
) : 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>
|
||||
|
||||
@@ -47,8 +47,8 @@ export function isFullEnoughForDeepAnalysis(detail?: CityDetail | null) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
countDetailModels(detail, detail.local_date) > 1 &&
|
||||
countDetailForecastDays(detail) > 1
|
||||
countDetailModels(detail, detail.local_date) >= 1 &&
|
||||
countDetailForecastDays(detail) >= 1
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useAiPinnedCityWorkspace({
|
||||
const aiFullHydrationRef = useRef<Set<string>>(new Set());
|
||||
const aiHydrationQueueRef = useRef<string[]>([]);
|
||||
const aiHydrationRunningRef = useRef(false);
|
||||
const aiHydrationRetriesRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
const runAiHydrationQueue = useCallback(async () => {
|
||||
if (aiHydrationRunningRef.current) return;
|
||||
@@ -47,10 +48,22 @@ export function useAiPinnedCityWorkspace({
|
||||
"full",
|
||||
);
|
||||
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 {
|
||||
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 {
|
||||
@@ -132,6 +145,8 @@ export function useAiPinnedCityWorkspace({
|
||||
aiPinnedCities.forEach((item) => {
|
||||
const key = normalizeCityKey(item.cityName);
|
||||
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 needsFullHydration = !isFullEnoughForDeepAnalysis(detail);
|
||||
if (!needsFullHydration) return;
|
||||
|
||||
@@ -376,10 +376,18 @@ export function getTemperatureChartData(
|
||||
value: Number(rawTemps[index]),
|
||||
}))
|
||||
.filter((entry) => entry.tail !== "");
|
||||
const times = validEntries.map((entry) => entry.tail);
|
||||
const temps = validEntries.map((entry) =>
|
||||
Number.isFinite(entry.value) ? entry.value : null,
|
||||
);
|
||||
const dataByHour = new Map<string, number | null>();
|
||||
validEntries.forEach((entry) => {
|
||||
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);
|
||||
|
||||
if (!times.length) return null;
|
||||
|
||||
@@ -386,6 +386,7 @@ class NwsOpenMeteoSourceMixin:
|
||||
"daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration",
|
||||
"timezone": "auto",
|
||||
"forecast_days": forecast_days,
|
||||
"past_days": 1,
|
||||
}
|
||||
|
||||
# 显式指定单位,防止 API 默认行为漂移
|
||||
|
||||
Reference in New Issue
Block a user