From fe3f48f255e4cc07e83d46d7f747c803a5bb631d Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 10 May 2026 18:30:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=8E=E5=B8=82=E5=86=B3=E7=AD=96=E5=8D=A1?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AMOS=20=E8=B7=91=E9=81=93=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AmosRunwayPanel 组件:展示每条跑道的温度/露点/能见度/RVR/风速 - 跑道卡片网格布局 (auto-fit minmax 160px) - 每条跑道独立显示:跑道编号、温度(含露点)、能见度、RVR、风速范围 - 官方 METAR 标签 vs 跑道中位数标签 - CityDetail 类型新增 AmosData 接口 - 暗色/浅色主题均已适配 - 仅首尔/釜山(有 AMOS 数据)时显示 --- .../dashboard/ScanTerminalCard.module.css | 64 ++++++++++++++ .../ScanTerminalLightTheme.module.css | 28 ++++++ .../scan-terminal/AiPinnedCityCard.tsx | 9 ++ .../scan-terminal/AmosRunwayPanel.tsx | 86 +++++++++++++++++++ frontend/lib/dashboard-types.ts | 30 +++++++ grep.exe.stackdump | 15 ++++ 6 files changed, 232 insertions(+) create mode 100644 frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx create mode 100644 grep.exe.stackdump diff --git a/frontend/components/dashboard/ScanTerminalCard.module.css b/frontend/components/dashboard/ScanTerminalCard.module.css index 847f910a..7abedcc0 100644 --- a/frontend/components/dashboard/ScanTerminalCard.module.css +++ b/frontend/components/dashboard/ScanTerminalCard.module.css @@ -1225,3 +1225,67 @@ font-weight: 800; line-height: 1.5; } + +/* ── AMOS runway observation panel ── */ +.root :global(.scan-amos-runway-panel) { + margin-top: 14px; + border: 1px solid rgba(77, 163, 255, 0.12); + border-radius: 16px; + background: rgba(11, 18, 32, 0.24); + padding: 14px; +} + +.root :global(.scan-amos-source-tag) { + display: inline-block; + margin-left: 8px; + padding: 1px 7px; + border-radius: 999px; + background: rgba(77, 163, 255, 0.12); + color: var(--color-accent-secondary); + font-size: 10px; + font-weight: 700; +} + +.root :global(.scan-amos-runway-grid) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + margin-top: 10px; +} + +.root :global(.scan-amos-runway-card) { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: 1px solid rgba(77, 163, 255, 0.08); + border-radius: 12px; + background: rgba(11, 18, 32, 0.38); +} + +.root :global(.scan-amos-runway-label) { + font-size: 11px; + font-weight: 700; + color: var(--color-accent-secondary); + letter-spacing: 0.02em; +} + +.root :global(.scan-amos-runway-temp) { + font-size: 20px; + font-weight: 800; + font-variant-numeric: tabular-nums; + color: var(--color-text-primary); + line-height: 1.15; +} + +.root :global(.scan-amos-runway-temp small) { + font-size: 11px; + font-weight: 600; + color: var(--color-text-muted); +} + +.root :global(.scan-amos-runway-detail) { + font-size: 11px; + font-weight: 600; + color: var(--color-text-muted); +} diff --git a/frontend/components/dashboard/ScanTerminalLightTheme.module.css b/frontend/components/dashboard/ScanTerminalLightTheme.module.css index 5c2ef36c..d9422462 100644 --- a/frontend/components/dashboard/ScanTerminalLightTheme.module.css +++ b/frontend/components/dashboard/ScanTerminalLightTheme.module.css @@ -982,3 +982,31 @@ html.light .root :global(.scan-terminal.light .scan-retry-button) { html.light .root :global(.scan-terminal.light .scan-retry-button:hover) { background: rgba(37, 99, 235, 0.14); } + +/* ── AMOS runway panel light overrides ── */ +html.light .root :global(.scan-terminal.light .scan-amos-runway-panel) { + background: rgba(37, 99, 235, 0.03); + border-color: rgba(37, 99, 235, 0.12); +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-card) { + background: #ffffff; + border-color: rgba(37, 99, 235, 0.08); +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-label) { + color: #2563EB; +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-temp) { + color: #0F172A; +} + +html.light .root :global(.scan-terminal.light .scan-amos-source-tag) { + background: rgba(37, 99, 235, 0.08); + color: #2563EB; +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-detail) { + color: #64748B; +} diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx index 16c589db..0aaa23d0 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx @@ -5,6 +5,7 @@ import type { MouseEvent } from "react"; import { useEffect, useState } from "react"; import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart"; import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel"; +import { AmosRunwayPanel } from "@/components/dashboard/scan-terminal/AmosRunwayPanel"; import { CityCardHeader } from "@/components/dashboard/scan-terminal/CityCardHeader"; import { MobileDecisionCard } from "@/components/dashboard/scan-terminal/MobileDecisionCard"; import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel"; @@ -650,6 +651,14 @@ export function AiPinnedCityCard({ /> + {detail?.amos ? ( + + ) : null} + ) : !detail ? ( diff --git a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx new file mode 100644 index 00000000..837f8ec4 --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx @@ -0,0 +1,86 @@ +"use client"; + +import type { AmosData } from "@/lib/dashboard-types"; + +function runwayTempClass(temp: number | null | undefined): string { + if (temp == null || !Number.isFinite(temp)) return ""; + if (temp >= 40) return "temp-extreme-hot"; + if (temp <= -5) return "temp-extreme-cold"; + return ""; +} + +export function AmosRunwayPanel({ + amos, + isEn, + tempSymbol, +}: { + amos: AmosData; + isEn: boolean; + tempSymbol: string; +}) { + const runwayPairs = amos.runway_obs?.runway_pairs; + const runwayTemps = amos.runway_obs?.temperatures ?? amos.runway_temps; + const runwayWinds = amos.runway_obs?.wind_speeds; + const runwayVis = amos.runway_obs?.visibility_mor; + const runwayRvr = amos.runway_obs?.rvr; + + if (!runwayPairs || !runwayTemps || runwayPairs.length === 0) return null; + + const pairs = runwayPairs.slice(0, runwayTemps.length); + + return ( +
+
+ {isEn ? "Runway Observations" : "跑道实测"} · {amos.station_label || amos.icao || ""} + + {amos.temp_source === "metar" + ? isEn ? "Official METAR" : "官方 METAR" + : isEn ? "Runway median" : "跑道中位数"} + +
+
+ {pairs.map(([rwyA, rwyB], idx) => { + const temps = runwayTemps[idx]; + const wind = runwayWinds?.[idx]; + const vis = runwayVis?.[idx]; + const rvr = runwayRvr?.[idx]; + return ( +
+
+ {rwyA}/{rwyB} +
+ {temps ? ( +
+ {temps[0].toFixed(1)}{tempSymbol} + {temps[1] != null ? ( + + {isEn ? " Dew " : " 露点 "} + {temps[1].toFixed(1)}{tempSymbol} + + ) : null} +
+ ) : null} + {vis != null && vis > 0 ? ( +
+ {isEn ? "Vis " : "能见度 "} + {vis >= 10000 ? (isEn ? "≥10km" : "≥10公里") : `${vis}m`} +
+ ) : null} + {rvr != null && rvr > 0 ? ( +
+ RVR {rvr >= 2000 ? "≥2000m" : `${rvr}m`} +
+ ) : null} + {wind ? ( +
+ {wind[0].toFixed(1)}kt + {wind[1] != null ? ` (${wind[1].toFixed(1)}–${wind[2].toFixed(1)})` : ""} +
+ ) : null} +
+ ); + })} +
+
+ ); +} diff --git a/frontend/lib/dashboard-types.ts b/frontend/lib/dashboard-types.ts index e54edf4a..88525375 100644 --- a/frontend/lib/dashboard-types.ts +++ b/frontend/lib/dashboard-types.ts @@ -910,6 +910,36 @@ export interface CityDetail { source_forecasts?: SourceForecasts; market_scan?: MarketScan; intraday_meteorology?: IntradayMeteorology; + amos?: AmosData | null; +} + +export interface AmosData { + temp?: number | null; + temp_c?: number | null; + dew?: number | null; + dew_c?: number | null; + pressure_hpa?: number | null; + wind_kt?: number | null; + temp_source?: string | null; + runway_temps?: Array<[number, number]> | null; + source?: string | null; + source_label?: string | null; + icao?: string | null; + station_label?: string | null; + raw_metar?: string | null; + raw_taf?: string | null; + runway_obs?: { + runway_pairs?: Array<[string, string]> | null; + temperatures?: Array<[number, number]> | null; + pressures_hpa?: number[] | null; + wind_directions?: Array<[number, number, number]> | null; + wind_speeds?: Array<[number, number, number]> | null; + visibility_mor?: number[] | null; + rvr?: number[] | null; + } | null; + observation_source?: string | null; + observation_source_zh?: string | null; + observation_time?: string | null; } export interface HistoryPoint { diff --git a/grep.exe.stackdump b/grep.exe.stackdump new file mode 100644 index 00000000..4831183d --- /dev/null +++ b/grep.exe.stackdump @@ -0,0 +1,15 @@ +Stack trace: +Frame Function Args +000FFFFBD10 00180062F57 (000FFFFBF18, 00000000002, 00000000002, 000FFFFDE50) +00000000000 00180065045 (00000000064, 00000000000, 00000000190, 00000000000) +000FFFFC420 001801369F8 (00000000000, 00000000000, 00000000000, 00000000000) +000000000C1 0018013202B (00000000000, 00000000000, 00000000000, 00000000000) +000FFFFC840 00180132435 (001803551C0, 000FFFFCAB0, 000000000B5, 00100437000) +000FFFFC840 001802195E8 (001803551C0, 001803552B8, 00000000000, 000FFFFC8C0) +000FFFFC840 00100429F25 (001800DB6FE, 001800435C6, 000FFFFC940, 00000000000) +000FFFFC8B0 00100404B11 (0000000000B, 000FFFFCDF0, 0018005DAAF, 00000000000) +000FFFFCAA0 001004294B0 (000FFFFCC40, 00000000000, 00000000000, 000FFFFCD30) +000FFFFCD30 00180049B91 (00000000000, 00000000000, 00000000000, 00000000000) +000FFFFFFF0 00180047716 (00000000000, 00000000000, 00000000000, 00000000000) +000FFFFFFF0 001800477C4 (00000000000, 00000000000, 00000000000, 00000000000) +End of stack trace