城市决策卡新增 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;
|
font-weight: 800;
|
||||||
line-height: 1.5;
|
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) {
|
html.light .root :global(.scan-terminal.light .scan-retry-button:hover) {
|
||||||
background: rgba(37, 99, 235, 0.14);
|
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 { useEffect, useState } from "react";
|
||||||
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
|
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
|
||||||
import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel";
|
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 { CityCardHeader } from "@/components/dashboard/scan-terminal/CityCardHeader";
|
||||||
import { MobileDecisionCard } from "@/components/dashboard/scan-terminal/MobileDecisionCard";
|
import { MobileDecisionCard } from "@/components/dashboard/scan-terminal/MobileDecisionCard";
|
||||||
import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel";
|
import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel";
|
||||||
@@ -650,6 +651,14 @@ export function AiPinnedCityCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{detail?.amos ? (
|
||||||
|
<AmosRunwayPanel
|
||||||
|
amos={detail.amos}
|
||||||
|
isEn={isEn}
|
||||||
|
tempSymbol={tempSymbol}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<ModelEvidencePanel detail={detail} isEn={isEn} />
|
<ModelEvidencePanel detail={detail} isEn={isEn} />
|
||||||
</div>
|
</div>
|
||||||
) : !detail ? (
|
) : !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;
|
source_forecasts?: SourceForecasts;
|
||||||
market_scan?: MarketScan;
|
market_scan?: MarketScan;
|
||||||
intraday_meteorology?: IntradayMeteorology;
|
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 {
|
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