城市决策卡新增 AMOS 跑道温度面板

- 新增 AmosRunwayPanel 组件:展示每条跑道的温度/露点/能见度/RVR/风速
- 跑道卡片网格布局 (auto-fit minmax 160px)
- 每条跑道独立显示:跑道编号、温度(含露点)、能见度、RVR、风速范围
- 官方 METAR 标签 vs 跑道中位数标签
- CityDetail 类型新增 AmosData 接口
- 暗色/浅色主题均已适配
- 仅首尔/釜山(有 AMOS 数据)时显示
This commit is contained in:
2569718930@qq.com
2026-05-10 18:30:59 +08:00
parent ba352feb34
commit fe3f48f255
6 changed files with 232 additions and 0 deletions
@@ -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);
}
@@ -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;
}
@@ -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({
/>
</div>
{detail?.amos ? (
<AmosRunwayPanel
amos={detail.amos}
isEn={isEn}
tempSymbol={tempSymbol}
/>
) : null}
<ModelEvidencePanel detail={detail} isEn={isEn} />
</div>
) : !detail ? (
@@ -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 (
<div className="scan-amos-runway-panel">
<div className="scan-ai-city-section-title">
{isEn ? "Runway Observations" : "跑道实测"} · {amos.station_label || amos.icao || ""}
<span className="scan-amos-source-tag">
{amos.temp_source === "metar"
? isEn ? "Official METAR" : "官方 METAR"
: isEn ? "Runway median" : "跑道中位数"}
</span>
</div>
<div className="scan-amos-runway-grid">
{pairs.map(([rwyA, rwyB], idx) => {
const temps = runwayTemps[idx];
const wind = runwayWinds?.[idx];
const vis = runwayVis?.[idx];
const rvr = runwayRvr?.[idx];
return (
<div key={`${rwyA}/${rwyB}`} className="scan-amos-runway-card">
<div className="scan-amos-runway-label">
{rwyA}/{rwyB}
</div>
{temps ? (
<div className={`scan-amos-runway-temp ${runwayTempClass(temps[0])}`}>
{temps[0].toFixed(1)}{tempSymbol}
{temps[1] != null ? (
<small>
{isEn ? " Dew " : " 露点 "}
{temps[1].toFixed(1)}{tempSymbol}
</small>
) : null}
</div>
) : null}
{vis != null && vis > 0 ? (
<div className="scan-amos-runway-detail">
{isEn ? "Vis " : "能见度 "}
{vis >= 10000 ? (isEn ? "≥10km" : "≥10公里") : `${vis}m`}
</div>
) : null}
{rvr != null && rvr > 0 ? (
<div className="scan-amos-runway-detail">
RVR {rvr >= 2000 ? "≥2000m" : `${rvr}m`}
</div>
) : null}
{wind ? (
<div className="scan-amos-runway-detail">
{wind[0].toFixed(1)}kt
{wind[1] != null ? ` (${wind[1].toFixed(1)}${wind[2].toFixed(1)})` : ""}
</div>
) : null}
</div>
);
})}
</div>
</div>
);
}
+30
View File
@@ -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 {
+15
View File
@@ -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