城市决策卡新增 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user