feat: implement MonitorPanel component for real-time weather monitoring and temperature tracking

This commit is contained in:
2569718930@qq.com
2026-05-14 02:47:37 +08:00
parent 8bfe23138c
commit 4bda402270
2 changed files with 72 additions and 4 deletions
@@ -579,7 +579,6 @@
color: #2563EB;
}
/* ── Animations ── */
@keyframes monitorPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
@@ -589,3 +588,34 @@
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
/* ── Value-change flash (green pulse on new temperature) ── */
@keyframes monitorTempFlash {
0% { color: inherit; text-shadow: none; }
25% { color: #4ade80; text-shadow: 0 0 12px rgba(74, 222, 128, 0.55); }
100% { color: inherit; text-shadow: none; }
}
@keyframes monitorHeadFlash {
0% { background: transparent; }
25% { background: rgba(74, 222, 128, 0.10); }
100% { background: transparent; }
}
.root :global(.monitor-temp-value.flashed) {
animation: monitorTempFlash 0.9s ease-out;
}
.root :global(.monitor-card-head.flashed) {
animation: monitorHeadFlash 0.9s ease-out;
border-radius: 6px;
}
/* ── Last-refreshed timestamp ── */
.root :global(.monitor-last-refreshed) {
font-size: 12px;
color: var(--color-accent-primary, #4da3ff);
font-variant-numeric: tabular-nums;
opacity: 0.85;
letter-spacing: 0.01em;
}
@@ -195,6 +195,12 @@ export default function MonitorPanel({
const cancelledRef = useRef(false);
const globalFetchingRef = useRef(false);
/* Flash state: tracks cities whose temperature just changed */
const [flashingKeys, setFlashingKeys] = useState<ReadonlySet<string>>(new Set());
const [lastRefreshed, setLastRefreshed] = useState<string | null>(null);
const prevTempsRef = useRef<Partial<Record<MonitorKey, number>>>({});
const flashTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
useEffect(() => {
setTime(new Date().toLocaleTimeString());
const timer = setInterval(() => setTime(new Date().toLocaleTimeString()), 30_000);
@@ -206,6 +212,30 @@ export default function MonitorPanel({
return localStorage.getItem("monitor_notify") !== "off";
});
/* Detect temperature changes → trigger per-city flash + update lastRefreshed */
useEffect(() => {
const changed: MonitorKey[] = [];
for (const key of MONITOR_KEYS) {
const detail = details[key];
const cur = detail?.airport_current?.temp ?? detail?.current?.temp ?? null;
const prev = prevTempsRef.current[key];
if (cur != null && prev != null && cur !== prev) changed.push(key);
if (cur != null) prevTempsRef.current[key] = cur;
}
if (changed.length === 0) return;
setLastRefreshed(
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
);
setFlashingKeys((prev) => new Set([...prev, ...changed]));
for (const key of changed) {
clearTimeout(flashTimersRef.current[key]);
flashTimersRef.current[key] = setTimeout(() => {
setFlashingKeys((prev) => { const n = new Set(prev); n.delete(key); return n; });
}, 900);
}
}, [details]);
/* Per-city fetch with loading-key tracking */
const fetchCity = useCallback(
async (key: MonitorKey, force: boolean) => {
@@ -251,6 +281,9 @@ export default function MonitorPanel({
await Promise.allSettled(workers);
globalFetchingRef.current = false;
setLastRefreshed(
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
);
},
[fetchCity],
);
@@ -355,6 +388,11 @@ export default function MonitorPanel({
>
{notify ? "🔔" : "🔕"}
</button>
{lastRefreshed && (
<span className="monitor-last-refreshed" title={isEn ? "Last data refresh" : "上次数据刷新"}>
{isEn ? `${lastRefreshed}` : `${lastRefreshed}`}
</span>
)}
<span className="monitor-time">{time}</span>
</div>
</div>
@@ -398,7 +436,7 @@ export default function MonitorPanel({
const tr = trendClass(detail);
const rwPairs = detail.amos?.runway_obs?.runway_pairs ?? [];
const rwTemps = detail.amos?.runway_obs?.temperatures ?? [];
const isFlashing = flashingKeys.has(key);
return (
<div
@@ -419,7 +457,7 @@ export default function MonitorPanel({
{isRefreshing && <div className="monitor-refresh-bar" />}
{/* Card header */}
<div className="monitor-card-head">
<div className={`monitor-card-head${isFlashing ? " flashed" : ""}`}>
<span className="monitor-city-name">{detail.display_name || key}</span>
<span className="monitor-airport-name">/ {airportLabel(key, isEn)}</span>
<FreshnessDot
@@ -438,7 +476,7 @@ export default function MonitorPanel({
<div className="monitor-temp-display">
{cur != null ? (
<>
<span className={`monitor-temp-value${newHigh ? " new-high" : warm ? " warm" : ""}`}>
<span className={`monitor-temp-value${newHigh ? " new-high" : warm ? " warm" : ""}${isFlashing ? " flashed" : ""}`}>
{cur.toFixed(1)}
</span>
<span className="monitor-temp-unit">{tempSymbol}</span>