LiveTemperatureThresholdChart 第二轮修复:hourlyCache模块化、去闪烁、数据修复

P0: hourlyCache 从组件体移到模块作用域+移除 setHourly(null)闪烁。P1: normObs 过滤无时间戳数据点。seriesStats 标签 15m→Δ15。buildMarketTemperatureOptions 停止从 market_question 文本暴力提取数字。
This commit is contained in:
2569718930@qq.com
2026-05-25 07:04:59 +08:00
parent c74be88cf9
commit 10112d2f58
@@ -34,6 +34,8 @@ type EvidenceSeries = {
// Sliding window: keep at most this many observation points (24h at 1-min ≈ 1440) // Sliding window: keep at most this many observation points (24h at 1-min ≈ 1440)
const MAX_OBS_POINTS = 1440; const MAX_OBS_POINTS = 1440;
const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000;
const _hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>();
function validNumber(value: unknown): number | null { function validNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null; return typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -66,10 +68,10 @@ function formatTimestamp(ts: number): string {
function normObs(points?: ObsPoint[] | null, limit = MAX_OBS_POINTS) { function normObs(points?: ObsPoint[] | null, limit = MAX_OBS_POINTS) {
return (points || []) return (points || [])
.filter((p) => validNumber(p.temp) !== null) .filter((p) => validNumber(p.temp) !== null && toTimestamp(p.time) !== null)
.slice(-limit) .slice(-limit)
.map((p, i) => ({ .map((p) => ({
ts: toTimestamp(p.time) ?? (Date.now() - (limit - i) * 60_000), ts: toTimestamp(p.time)!,
value: Number(p.temp), value: Number(p.temp),
})); }));
} }
@@ -291,7 +293,6 @@ function buildMarketTemperatureOptions(row: ScanOpportunityRow | null) {
[row?.target_lower, row?.target_upper, row?.target_value, row?.target_threshold] [row?.target_lower, row?.target_upper, row?.target_value, row?.target_threshold]
.forEach((v) => { if (validNumber(v) !== null) values.add(validNumber(v)!); }); .forEach((v) => { if (validNumber(v) !== null) values.add(validNumber(v)!); });
parseTemperatureOptionsFromText(row?.target_label).forEach((x) => values.add(x)); parseTemperatureOptionsFromText(row?.target_label).forEach((x) => values.add(x));
parseTemperatureOptionsFromText(row?.market_question).forEach((x) => values.add(x));
const sorted = [...values].sort((a, b) => a - b); const sorted = [...values].sort((a, b) => a - b);
if (sorted.length) return sorted; if (sorted.length) return sorted;
@@ -323,20 +324,16 @@ export function LiveTemperatureThresholdChart({
isEn: boolean; isEn: boolean;
row: ScanOpportunityRow | null; row: ScanOpportunityRow | null;
}) { }) {
const hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>();
const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; // 30 min
const [hourly, setHourly] = useState<HourlyForecast>(null); const [hourly, setHourly] = useState<HourlyForecast>(null);
const city = String(row?.city || "").toLowerCase().trim(); const city = String(row?.city || "").toLowerCase().trim();
useEffect(() => { useEffect(() => {
if (!city) return; if (!city) return;
const cached = hourlyCache.get(city); const cached = _hourlyCache.get(city);
if (cached && Date.now() - cached.ts < HOURLY_CACHE_TTL_MS) { if (cached && Date.now() - cached.ts < HOURLY_CACHE_TTL_MS) {
setHourly(cached.data); setHourly(cached.data);
return; return;
} }
setHourly(null);
let cancelled = false; let cancelled = false;
fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=panel&force_refresh=false`, { fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=panel&force_refresh=false`, {
cache: "no-store", cache: "no-store",
@@ -355,7 +352,7 @@ const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; // 30 min
temps: json.hourly.temps || [], temps: json.hourly.temps || [],
modelCurves: json.models_hourly?.curves || undefined, modelCurves: json.models_hourly?.curves || undefined,
}; };
hourlyCache.set(city, { ts: Date.now(), data }); _hourlyCache.set(city, { ts: Date.now(), data });
setHourly(data); setHourly(data);
}) })
.catch(() => {}); .catch(() => {});
@@ -414,7 +411,7 @@ const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; // 30 min
<div className="grid grid-cols-3 gap-1"> <div className="grid grid-cols-3 gap-1">
<span>now: {temp(item.latest)}</span> <span>now: {temp(item.latest)}</span>
<span>max: {temp(item.high)}</span> <span>max: {temp(item.high)}</span>
<span>15m: {item.delta15 === null ? "--" : `${item.delta15 >= 0 ? "+" : ""}${item.delta15.toFixed(1)}°`}</span> <span>Δ15: {item.delta15 === null ? "--" : `${item.delta15 >= 0 ? "+" : ""}${item.delta15.toFixed(1)}°`}</span>
</div> </div>
)} )}
</div> </div>