From 4bda402270b7c1b79c713cccd1367edf5c0894ce Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 14 May 2026 02:47:37 +0800 Subject: [PATCH] feat: implement MonitorPanel component for real-time weather monitoring and temperature tracking --- .../monitoring/MonitorPanel.module.css | 32 +++++++++++++- .../dashboard/monitoring/MonitorPanel.tsx | 44 +++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.module.css b/frontend/components/dashboard/monitoring/MonitorPanel.module.css index 9a679133..888c6d15 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.module.css +++ b/frontend/components/dashboard/monitoring/MonitorPanel.module.css @@ -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; +} diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.tsx b/frontend/components/dashboard/monitoring/MonitorPanel.tsx index 8c958ecb..b4c541e8 100644 --- a/frontend/components/dashboard/monitoring/MonitorPanel.tsx +++ b/frontend/components/dashboard/monitoring/MonitorPanel.tsx @@ -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>(new Set()); + const [lastRefreshed, setLastRefreshed] = useState(null); + const prevTempsRef = useRef>>({}); + const flashTimersRef = useRef>>({}); + 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 ? "🔔" : "🔕"} + {lastRefreshed && ( + + {isEn ? `↻ ${lastRefreshed}` : `↻ ${lastRefreshed}`} + + )} {time} @@ -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 (
} {/* Card header */} -
+
{detail.display_name || key} / {airportLabel(key, isEn)} {cur != null ? ( <> - + {cur.toFixed(1)} {tempSymbol}