60s 轮询扩展:跑道曲线 + 站点观测同步刷新
原来只轮询 summary 拿 current.temp 更新数字。 现在同时 fetch detail?depth=full 合并 amos/runway_plate_history/ airport_primary_today_obs 等字段,跑道曲线和站点线每 60s 刷新。 后端 CITY_FULL_CACHE_TTL_SEC=60 保证轻量命中缓存。
This commit is contained in:
@@ -1211,10 +1211,11 @@ export function LiveTemperatureThresholdChart({
|
|||||||
};
|
};
|
||||||
}, [city, row, isActive, slotIndex]);
|
}, [city, row, isActive, slotIndex]);
|
||||||
|
|
||||||
// ── 60s lightweight live-temp poll ──
|
// ── 60s live poll: summary (number) + detail amos/runway (curves) ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!city) return;
|
if (!city) return;
|
||||||
const fetchLiveTemp = () => {
|
const poll = () => {
|
||||||
|
// Lightweight: current temp number
|
||||||
fetch(`/api/city/${encodeURIComponent(city)}/summary`)
|
fetch(`/api/city/${encodeURIComponent(city)}/summary`)
|
||||||
.then((res) => (res.ok ? res.json() : null))
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
@@ -1223,9 +1224,38 @@ export function LiveTemperatureThresholdChart({
|
|||||||
if (temp !== null) setLiveTemp(temp);
|
if (temp !== null) setLiveTemp(temp);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Refresh runway curves and station observations
|
||||||
|
fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=full&force_refresh=false`)
|
||||||
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
|
.then((json) => {
|
||||||
|
if (!json) return;
|
||||||
|
setHourly((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
amos: json.amos ?? prev.amos,
|
||||||
|
airportPrimary: json.airport_primary ?? prev.airportPrimary,
|
||||||
|
airportCurrent: json.airport_current ?? prev.airportCurrent,
|
||||||
|
airportPrimaryTodayObs: (json as any)?.official?.airport_primary_today_obs
|
||||||
|
|| json.airport_primary_today_obs
|
||||||
|
|| prev.airportPrimaryTodayObs,
|
||||||
|
runwayPlateHistory: (json as any)?.runway_plate_history
|
||||||
|
|| (json.amos as any)?.runway_plate_history
|
||||||
|
|| prev.runwayPlateHistory,
|
||||||
|
settlementTodayObs: (json as any).timeseries?.settlement_today_obs
|
||||||
|
|| (json as any)?.settlement_today_obs
|
||||||
|
|| prev.settlementTodayObs,
|
||||||
|
metarTodayObs: (json as any).timeseries?.metar_today_obs
|
||||||
|
|| (json as any)?.metar_today_obs
|
||||||
|
|| prev.metarTodayObs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
fetchLiveTemp();
|
poll();
|
||||||
const id = setInterval(fetchLiveTemp, 60_000);
|
const id = setInterval(poll, 60_000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [city]);
|
}, [city]);
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,60 @@ export function LoadingSignal({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("scan-loading-signal", compact && "compact")}
|
className={clsx(
|
||||||
|
"flex flex-col items-center justify-center text-center select-none",
|
||||||
|
compact ? "p-4 gap-2 w-full max-w-[280px]" : "p-6 gap-4 w-full max-w-[360px]"
|
||||||
|
)}
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<div className="scan-loading-spinner-wrapper" aria-hidden="true">
|
<div
|
||||||
<div className="scan-loading-spinner-ring" />
|
className={clsx(
|
||||||
<img
|
"relative flex items-center justify-center rounded-full shrink-0",
|
||||||
src="/apple-touch-icon.png"
|
compact ? "w-16 h-16" : "w-24 h-24"
|
||||||
alt="PolyWeather"
|
)}
|
||||||
className="scan-loading-spinner-logo"
|
aria-hidden="true"
|
||||||
/>
|
>
|
||||||
|
{/* Background Track Ring */}
|
||||||
|
<div className="absolute inset-0 rounded-full border-[3px] border-slate-200/80 dark:border-slate-800" />
|
||||||
|
|
||||||
|
{/* Active Spinning Ring */}
|
||||||
|
<div className="absolute inset-0 rounded-full border-[3px] border-transparent border-t-blue-500 border-r-blue-400 animate-spin" />
|
||||||
|
|
||||||
|
{/* Centered Rounded Logo Card */}
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"absolute bg-white dark:bg-slate-900 rounded-xl flex items-center justify-center shadow-sm border border-slate-100/80 dark:border-slate-800/80",
|
||||||
|
compact ? "w-10 h-10" : "w-14 h-14"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/apple-touch-icon.png"
|
||||||
|
alt="PolyWeather Logo"
|
||||||
|
className={clsx(
|
||||||
|
"object-contain rounded-lg",
|
||||||
|
compact ? "w-7 h-7" : "w-10 h-10"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-loading-copy-block">
|
|
||||||
<strong>{title}</strong>
|
{/* Text Copy */}
|
||||||
{description ? <span>{description}</span> : null}
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<strong className={clsx(
|
||||||
|
"font-bold text-slate-800 dark:text-slate-100",
|
||||||
|
compact ? "text-xs" : "text-sm"
|
||||||
|
)}>
|
||||||
|
{title}
|
||||||
|
</strong>
|
||||||
|
{description && (
|
||||||
|
<span className={clsx(
|
||||||
|
"text-slate-500 dark:text-slate-400 leading-normal",
|
||||||
|
compact ? "text-[10px] max-w-[200px]" : "text-xs max-w-[280px]"
|
||||||
|
)}>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export function runTests() {
|
|||||||
"selected city detail chart cache should align with 5-minute scan/metar cadence",
|
"selected city detail chart cache should align with 5-minute scan/metar cadence",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("setInterval(fetchLiveTemp, 60_000)"),
|
chartSource.includes("setInterval(poll, 60_000)"),
|
||||||
"selected city chart should poll live temperature every 60 seconds via lightweight summary endpoint",
|
"selected city chart should poll live temperature and runway curves every 60 seconds",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartSource.includes("_hourlyRequestCache") &&
|
chartSource.includes("_hourlyRequestCache") &&
|
||||||
|
|||||||
Reference in New Issue
Block a user