"use client"; import { useEffect, useState, useCallback } from "react"; interface RunwayPair { label: string; temp: number; } interface CitySnapshot { en_name: string; airport: string; obs_time: string; current_temp: number | null; max_so_far: number | null; max_temp_time: string | null; trend_sym: string; trend_css: string; obs_age_min: number | null; new_high: boolean; temp_warm: boolean; runway_pairs?: string; // HTML from backend } function parseRunway(html: string): RunwayPair[] { const pairs: RunwayPair[] = []; const re = /runway-label">([^<]+)<.*?runway-temp">([\d.]+)/g; let m; while ((m = re.exec(html)) !== null) { pairs.push({ label: m[1], temp: parseFloat(m[2]) }); } return pairs; } export default function MonitorPanel() { const [cities, setCities] = useState([]); const [time, setTime] = useState(""); const [notify, setNotify] = useState( () => typeof window !== "undefined" && localStorage.getItem("monitor_notify") !== "off" ); const fetchData = useCallback(async () => { try { const res = await fetch("/api/m?json=1"); const data = await res.json(); setCities(data); setTime(new Date().toLocaleTimeString()); } catch {} }, []); useEffect(() => { fetchData(); const t = setInterval(fetchData, 30_000); return () => clearInterval(t); }, [fetchData]); const toggleNotify = () => { const next = !notify; setNotify(next); localStorage.setItem("monitor_notify", next ? "on" : "off"); if (next && "Notification" in window && Notification.permission === "default") { Notification.requestPermission(); } }; return (
{/* Header */}

🔥 市场监控

{time}
{/* Card Grid */}
{cities.map((c) => { const cn = c.new_high ? " new-high-card" : ""; const wc = c.temp_warm ? " warm" : ""; const nv = c.new_high ? " new-high-val" : ""; const rw = c.runway_pairs ? parseRunway(c.runway_pairs) : []; return (
{/* Top */}
{c.en_name} / {c.airport} {c.obs_time} {c.new_high && ( ◆新高 )}
{/* Temp */}
{c.current_temp != null ? ( <> {c.current_temp.toFixed(1)} °C ) : ( -- )}
{/* Meta */}
High {c.max_so_far != null ? ( <> {c.max_so_far.toFixed(1)}°C {c.max_temp_time && ( {c.max_temp_time} )} ) : ( -- )} {c.trend_sym}
Obs {c.obs_age_min != null ? ( {c.obs_age_min} min ago ) : ( -- )}
{/* Runway */} {rw.length > 0 && (
{rw.map((r, i) => (
{r.label} {r.temp.toFixed(1)}°C
))}
)}
); })}
); } const cardStyle: React.CSSProperties = { background: "#161822", border: "1px solid #1e2130", borderRadius: 12, padding: "20px 24px", position: "relative", }; const cardTopStyle: React.CSSProperties = { display: "flex", alignItems: "center", gap: 8, marginBottom: 14, fontSize: 15, flexWrap: "wrap", };