城市决策卡新增 AMOS 跑道温度面板
- 新增 AmosRunwayPanel 组件:展示每条跑道的温度/露点/能见度/RVR/风速 - 跑道卡片网格布局 (auto-fit minmax 160px) - 每条跑道独立显示:跑道编号、温度(含露点)、能见度、RVR、风速范围 - 官方 METAR 标签 vs 跑道中位数标签 - CityDetail 类型新增 AmosData 接口 - 暗色/浅色主题均已适配 - 仅首尔/釜山(有 AMOS 数据)时显示
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user