Files
PolyWeather/frontend/components/dashboard/scan-terminal/RunwayObservationsPanel.tsx
T

181 lines
5.5 KiB
TypeScript
Raw Normal View History

2026-05-15 01:41:49 +08:00
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { RefreshCw } from "lucide-react";
import { useCityDetails, useDashboardActions } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
import type { CityDetail } from "@/lib/dashboard-types";
const CHINA_RUNWAY_CITIES = [
{ key: "beijing", zh: "北京", en: "Beijing", icao: "ZBAA" },
{ key: "shanghai", zh: "上海", en: "Shanghai", icao: "ZSPD" },
{ key: "guangzhou", zh: "广州", en: "Guangzhou", icao: "ZGGG" },
{ key: "shenzhen", zh: "深圳", en: "Shenzhen", icao: "ZGSZ" },
{ key: "chengdu", zh: "成都", en: "Chengdu", icao: "ZUUU" },
{ key: "chongqing", zh: "重庆", en: "Chongqing", icao: "ZUCK" },
{ key: "wuhan", zh: "武汉", en: "Wuhan", icao: "ZHHH" },
] as const;
function formatTemp(value: number | null | undefined, symbol = "°C") {
return value == null || !Number.isFinite(Number(value))
? "-"
: `${Number(value).toFixed(1)}${symbol}`;
}
function getRunwayRows(detail?: CityDetail | null) {
return detail?.amos?.runway_obs?.point_temperatures ?? [];
}
function sourceIsAmsc(detail?: CityDetail | null) {
return detail?.amos?.source === "amsc_awos";
}
function RunwayCityCard({
detail,
isEn,
label,
}: {
detail?: CityDetail | null;
isEn: boolean;
label: (typeof CHINA_RUNWAY_CITIES)[number];
}) {
const rows = getRunwayRows(detail);
const tempSymbol = detail?.temp_symbol || "°C";
const range = detail?.amos?.runway_temp_range;
const obsLocal =
detail?.amos?.observation_time_local || detail?.amos?.observation_time;
2026-05-15 01:41:49 +08:00
const hasAmsc = sourceIsAmsc(detail) && rows.length > 0;
return (
<article className="monitor-card">
<div className="monitor-card-head">
<span className="monitor-city-name">{isEn ? label.en : label.zh}</span>
<span className="monitor-airport-name">/ {label.icao}</span>
<span className="monitor-obs-time">{obsLocal || "--"}</span>
</div>
<div className="monitor-stats">
<div className="monitor-high-row">
<span className="monitor-stat-label">AMSC AWOS</span>
<span className="monitor-high-value">
{range
? `${range[0].toFixed(1)}${range[1].toFixed(1)}${tempSymbol}`
: "--"}
2026-05-15 01:41:49 +08:00
</span>
</div>
<div className="monitor-obs-row">
<span className="monitor-stat-label">
{isEn ? "Runway-point air temperature" : "跑道观测点气温"}
</span>
<span className="monitor-obs-age fresh">
{isEn ? "not pavement temp" : "非道面温度"}
</span>
</div>
</div>
{hasAmsc ? (
<>
<div className="monitor-divider" />
<div className="monitor-rw-row">
<span className="monitor-rw-label">{isEn ? "Runway" : "跑道"}</span>
<span className="monitor-rw-temp">TDZ / MID / END</span>
</div>
{rows.map((row) => (
<div
key={row.runway || `${row.tdz_temp}-${row.end_temp}`}
className="monitor-rw-row"
>
2026-05-15 01:41:49 +08:00
<span className="monitor-rw-label">{row.runway || "--"}</span>
<span className="monitor-rw-temp">
{formatTemp(row.tdz_temp, tempSymbol)} /{" "}
{formatTemp(row.mid_temp, tempSymbol)} /{" "}
{formatTemp(row.end_temp, tempSymbol)}
2026-05-15 01:41:49 +08:00
</span>
</div>
))}
{detail?.amos?.raw_metar ? (
<div className="scan-amos-runway-detail">
METAR {detail.amos.raw_metar.replace(/^METAR\s+/i, "")}
</div>
) : null}
</>
) : (
<div className="scan-empty-state compact">
{isEn
? "No AMSC runway observation loaded yet."
: "暂无 AMSC 跑道观测。"}
2026-05-15 01:41:49 +08:00
</div>
)}
</article>
);
}
export function RunwayObservationsPanel() {
const { locale } = useI18n();
const isEn = locale === "en-US";
const { cityDetailsByName } = useCityDetails();
const { ensureCityDetail } = useDashboardActions();
const [refreshing, setRefreshing] = useState(false);
const loadAll = useCallback(
async (force: boolean) => {
setRefreshing(true);
try {
await Promise.allSettled(
CHINA_RUNWAY_CITIES.map((city) =>
ensureCityDetail(city.key, force, "panel"),
),
);
} finally {
setRefreshing(false);
}
},
[ensureCityDetail],
);
useEffect(() => {
void loadAll(false);
}, [loadAll]);
const cards = useMemo(
() =>
CHINA_RUNWAY_CITIES.map((city) => ({
...city,
detail: cityDetailsByName[city.key],
})),
[cityDetailsByName],
);
return (
<div className="monitor-panel runway-observations-panel">
<div className="monitor-toolbar">
<div>
<div className="monitor-title">
{isEn ? "Runway Observations" : "跑道观测"}
</div>
</div>
<button
type="button"
className="monitor-refresh-button"
disabled={refreshing}
onClick={() => void loadAll(true)}
>
<RefreshCw size={14} className={refreshing ? "spin" : undefined} />
{isEn ? "Refresh" : "刷新"}
</button>
</div>
<div className="monitor-grid">
{cards.map((city) => (
<RunwayCityCard
key={city.key}
detail={city.detail}
isEn={isEn}
label={city}
/>
))}
</div>
</div>
);
}