feat: add scan terminal dashboard components, monitoring panels, and associated utility hooks

This commit is contained in:
2569718930@qq.com
2026-05-15 12:52:39 +08:00
parent 1a284c9990
commit e7fc3c97cb
13 changed files with 316 additions and 68 deletions
@@ -5,14 +5,17 @@ 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" },
const RUNWAY_OBSERVATION_CITIES = [
{ key: "seoul", zh: "首尔", en: "Seoul", icao: "RKSI", sourceLabel: "AMOS" },
{ key: "busan", zh: "釜山", en: "Busan", icao: "RKPK", sourceLabel: "AMOS" },
{ key: "beijing", zh: "北京", en: "Beijing", icao: "ZBAA", sourceLabel: "AMSC AWOS" },
{ key: "shanghai", zh: "上海", en: "Shanghai", icao: "ZSPD", sourceLabel: "AMSC AWOS" },
{ key: "guangzhou", zh: "广州", en: "Guangzhou", icao: "ZGGG", sourceLabel: "AMSC AWOS" },
{ key: "shenzhen", zh: "深圳", en: "Shenzhen", icao: "ZGSZ", sourceLabel: "AMSC AWOS" },
{ key: "qingdao", zh: "青岛", en: "Qingdao", icao: "ZSQD", sourceLabel: "AMSC AWOS" },
{ key: "chengdu", zh: "成都", en: "Chengdu", icao: "ZUUU", sourceLabel: "AMSC AWOS" },
{ key: "chongqing", zh: "重庆", en: "Chongqing", icao: "ZUCK", sourceLabel: "AMSC AWOS" },
{ key: "wuhan", zh: "武汉", en: "Wuhan", icao: "ZHHH", sourceLabel: "AMSC AWOS" },
] as const;
function formatTemp(value: number | null | undefined, symbol = "°C") {
@@ -25,6 +28,19 @@ function getRunwayRows(detail?: CityDetail | null) {
return detail?.amos?.runway_obs?.point_temperatures ?? [];
}
function getRunwayPairRows(detail?: CityDetail | null) {
const runway_pairs = detail?.amos?.runway_obs?.runway_pairs ?? [];
const runway_temps =
detail?.amos?.runway_obs?.temperatures ??
detail?.amos?.runway_temps ??
[];
return runway_pairs.map(([from, to], index) => ({
label: `${from}/${to}`,
temp: runway_temps[index]?.[0] ?? null,
dew: runway_temps[index]?.[1] ?? null,
}));
}
function sourceIsAmsc(detail?: CityDetail | null) {
return detail?.amos?.source === "amsc_awos";
}
@@ -36,14 +52,17 @@ function RunwayCityCard({
}: {
detail?: CityDetail | null;
isEn: boolean;
label: (typeof CHINA_RUNWAY_CITIES)[number];
label: (typeof RUNWAY_OBSERVATION_CITIES)[number];
}) {
const rows = getRunwayRows(detail);
const pairRows = getRunwayPairRows(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;
const hasAmsc = sourceIsAmsc(detail) && rows.length > 0;
const hasPointRows = sourceIsAmsc(detail) && rows.length > 0;
const hasPairRows = pairRows.some((row) => row.temp != null || row.dew != null);
const hasRunwayData = hasPointRows || hasPairRows;
return (
<article className="monitor-card">
@@ -55,41 +74,65 @@ function RunwayCityCard({
<div className="monitor-stats">
<div className="monitor-high-row">
<span className="monitor-stat-label">AMSC AWOS</span>
<span className="monitor-stat-label">{label.sourceLabel}</span>
<span className="monitor-high-value">
{range
? `${range[0].toFixed(1)}${range[1].toFixed(1)}${tempSymbol}`
: hasPairRows
? pairRows
.map((row) => row.temp)
.filter((value): value is number => value != null)
.map((value) => value.toFixed(1))
.join(" / ") + tempSymbol
: "--"}
</span>
</div>
</div>
{hasAmsc ? (
{hasRunwayData ? (
<>
<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"
>
<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)}
</span>
{hasPointRows ? (
<>
<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"
>
<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)}
</span>
</div>
))}
</>
) : (
<div className="monitor-rw-row">
<span className="monitor-rw-label">{isEn ? "Runway" : "跑道"}</span>
<span className="monitor-rw-temp">{isEn ? "Temp / Dew" : "温度 / 露点"}</span>
</div>
))}
)}
{hasPairRows &&
pairRows.map((row) => (
<div key={row.label} className="monitor-rw-row">
<span className="monitor-rw-label">{row.label}</span>
<span className="monitor-rw-temp">
{formatTemp(row.temp, tempSymbol)} / {formatTemp(row.dew, tempSymbol)}
</span>
</div>
))}
</>
) : (
<div className="scan-empty-state compact">
{isEn
? "No AMSC runway observation loaded yet."
: "暂无 AMSC 跑道观测。"}
? `No ${label.sourceLabel} runway observation loaded yet.`
: `暂无 ${label.sourceLabel} 跑道观测。`}
</div>
)}
</article>
@@ -104,7 +147,7 @@ export function RunwayObservationsPanel() {
const loadAll = useCallback(
async () => {
await Promise.allSettled(
CHINA_RUNWAY_CITIES.map((city) =>
RUNWAY_OBSERVATION_CITIES.map((city) =>
ensureCityDetail(city.key, false, "panel"),
),
);
@@ -118,7 +161,7 @@ export function RunwayObservationsPanel() {
void loadAll();
intervalRef.current = setInterval(() => {
void Promise.allSettled(
CHINA_RUNWAY_CITIES.map((city) =>
RUNWAY_OBSERVATION_CITIES.map((city) =>
ensureCityDetail(city.key, true, "panel"),
),
);
@@ -130,7 +173,7 @@ export function RunwayObservationsPanel() {
const cards = useMemo(
() =>
CHINA_RUNWAY_CITIES.map((city) => ({
RUNWAY_OBSERVATION_CITIES.map((city) => ({
...city,
detail: cityDetailsByName[city.key],
})),