From fc48795299162f10b72c170f15324c199d5f674b Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Thu, 14 May 2026 00:18:26 +0800
Subject: [PATCH] =?UTF-8?q?=E7=9B=91=E6=8E=A7=E9=A1=B5=E4=BB=8E=20iframe?=
=?UTF-8?q?=20=E6=94=B9=E4=B8=BA=E5=8E=9F=E7=94=9F=20React=20=E7=BB=84?=
=?UTF-8?q?=E4=BB=B6=EF=BC=9A=E6=97=A0=E5=8A=A0=E8=BD=BD=E5=BB=B6=E8=BF=9F?=
=?UTF-8?q?=EF=BC=8C30s=20JSON=20=E8=BD=AE=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/app/api/m/route.ts | 6 +-
.../dashboard/ScanTerminalDashboard.tsx | 11 +-
.../dashboard/monitoring/MonitorPanel.tsx | 188 ++++++++++++++++++
web/monitor_routes.py | 12 ++
4 files changed, 209 insertions(+), 8 deletions(-)
create mode 100644 frontend/components/dashboard/monitoring/MonitorPanel.tsx
diff --git a/frontend/app/api/m/route.ts b/frontend/app/api/m/route.ts
index 41f78fc5..6e7eb4eb 100644
--- a/frontend/app/api/m/route.ts
+++ b/frontend/app/api/m/route.ts
@@ -8,9 +8,13 @@ export async function GET(req: NextRequest) {
return new NextResponse("API not configured", { status: 500 });
}
const isCards = req.nextUrl.searchParams.get("cards") === "1";
- const path = isCards ? "/m/cards" : "/m";
+ const asJson = req.nextUrl.searchParams.get("json") === "1";
+ const path = asJson ? "/m/json" : isCards ? "/m/cards" : "/m";
const auth = await buildBackendRequestHeaders(req, { includeSupabaseIdentity: false });
const res = await fetch(`${API_BASE}${path}`, { headers: auth.headers });
+ if (asJson) {
+ return NextResponse.json(await res.json());
+ }
const html = await res.text();
return new NextResponse(html, {
status: res.status,
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx
index db106fe6..3c759ffa 100644
--- a/frontend/components/dashboard/ScanTerminalDashboard.tsx
+++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx
@@ -46,6 +46,7 @@ import {
useScanTerminalTheme,
useUserLocalClock,
} from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state";
+import MonitorPanel from "@/components/dashboard/monitoring/MonitorPanel";
type ContentView = "analysis" | "map" | "monitor";
@@ -597,13 +598,9 @@ function ScanTerminalScreen() {
) : (
renderMainView()
)}
-
-
-
+ {resolvedView === "monitor" && (
+
+ )}
diff --git a/frontend/components/dashboard/monitoring/MonitorPanel.tsx b/frontend/components/dashboard/monitoring/MonitorPanel.tsx
new file mode 100644
index 00000000..b0182ba3
--- /dev/null
+++ b/frontend/components/dashboard/monitoring/MonitorPanel.tsx
@@ -0,0 +1,188 @@
+"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",
+};
diff --git a/web/monitor_routes.py b/web/monitor_routes.py
index e2513e23..27dc6946 100644
--- a/web/monitor_routes.py
+++ b/web/monitor_routes.py
@@ -254,3 +254,15 @@ async def monitor_page(request: Request):
async def monitor_cards(request: Request):
cards, gts = _build_cards()
return HTMLResponse(_render(cards, gts))
+
+@router.get("/m/json")
+async def monitor_json(request: Request):
+ cards, gts = _build_cards()
+ return [{
+ "en_name": c["en_name"], "airport": c["airport"],
+ "obs_time": c["time"], "current_temp": c["ct"],
+ "max_so_far": c["msf"], "max_temp_time": c["mtt"],
+ "trend_sym": c["tsym"], "trend_css": c["tcss"],
+ "obs_age_min": c["age"], "new_high": c["new_high"],
+ "temp_warm": c["warm"], "runway_pairs": c["rw_html"],
+ } for c in cards]