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)
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 {
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) {
return (points || [])
.filter((p) => validNumber(p.temp) !== null)
.filter((p) => validNumber(p.temp) !== null && toTimestamp(p.time) !== null)
.slice(-limit)
.map((p, i) => ({
ts: toTimestamp(p.time) ?? (Date.now() - (limit - i) * 60_000),
.map((p) => ({
ts: toTimestamp(p.time)!,
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]
.forEach((v) => { if (validNumber(v) !== null) values.add(validNumber(v)!); });
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);
if (sorted.length) return sorted;
@@ -323,20 +324,16 @@ export function LiveTemperatureThresholdChart({
isEn: boolean;
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 city = String(row?.city || "").toLowerCase().trim();
useEffect(() => {
if (!city) return;
const cached = hourlyCache.get(city);
const cached = _hourlyCache.get(city);
if (cached && Date.now() - cached.ts < HOURLY_CACHE_TTL_MS) {
setHourly(cached.data);
return;
}
setHourly(null);
let cancelled = false;
fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=panel&force_refresh=false`, {
cache: "no-store",
@@ -355,7 +352,7 @@ const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; // 30 min
temps: json.hourly.temps || [],
modelCurves: json.models_hourly?.curves || undefined,
};
hourlyCache.set(city, { ts: Date.now(), data });
_hourlyCache.set(city, { ts: Date.now(), data });
setHourly(data);
})
.catch(() => {});
@@ -414,7 +411,7 @@ const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; // 30 min
<div className="grid grid-cols-3 gap-1">
<span>now: {temp(item.latest)}</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>