feat: implement utility modules and AI-pinned city dashboard components for temperature forecasting
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
useAiCityForecast,
|
useAiCityForecast,
|
||||||
useCityMarketScan,
|
useCityMarketScan,
|
||||||
} from "@/components/dashboard/scan-terminal/use-ai-city-card-data";
|
} from "@/components/dashboard/scan-terminal/use-ai-city-card-data";
|
||||||
|
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||||
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { getModelView } from "@/lib/model-utils";
|
import { getModelView } from "@/lib/model-utils";
|
||||||
import { getTodayPaceView } from "@/lib/pace-utils";
|
import { getTodayPaceView } from "@/lib/pace-utils";
|
||||||
@@ -242,10 +243,11 @@ export function AiPinnedCityCard({
|
|||||||
"--";
|
"--";
|
||||||
const deb = detail?.deb?.prediction ?? row?.deb_prediction ?? null;
|
const deb = detail?.deb?.prediction ?? row?.deb_prediction ?? null;
|
||||||
const isHkoObservation = isHkoObservationCity(detail);
|
const isHkoObservation = isHkoObservationCity(detail);
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const currentTemp =
|
const currentTemp =
|
||||||
(isHkoObservation
|
(isHkoObservation
|
||||||
? detail?.current?.temp ?? row?.current_temp
|
? detail?.current?.temp ?? row?.current_temp
|
||||||
: detail?.airport_primary?.temp ??
|
: displayAirportPrimary?.temp ??
|
||||||
detail?.airport_current?.temp ??
|
detail?.airport_current?.temp ??
|
||||||
detail?.current?.temp ??
|
detail?.current?.temp ??
|
||||||
row?.current_temp) ?? null;
|
row?.current_temp) ?? null;
|
||||||
@@ -274,7 +276,7 @@ export function AiPinnedCityCard({
|
|||||||
: detail?.risk?.icao ||
|
: detail?.risk?.icao ||
|
||||||
detail?.current?.station_code ||
|
detail?.current?.station_code ||
|
||||||
detail?.airport_current?.station_code ||
|
detail?.airport_current?.station_code ||
|
||||||
detail?.airport_primary?.station_code ||
|
displayAirportPrimary?.station_code ||
|
||||||
"";
|
"";
|
||||||
const observationSourceZh = isHkoObservation ? "香港天文台观测" : "METAR 实测";
|
const observationSourceZh = isHkoObservation ? "香港天文台观测" : "METAR 实测";
|
||||||
const observationSourceEn = isHkoObservation ? "HKO observations" : "METAR observations";
|
const observationSourceEn = isHkoObservation ? "HKO observations" : "METAR observations";
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
getDisplayAirportPrimary,
|
||||||
|
shouldSuppressKoreanRunwayObservation,
|
||||||
|
} from "@/lib/airport-observation-display";
|
||||||
|
import type { CityDetail } from "@/lib/dashboard-types";
|
||||||
|
import { getTodayPaceView } from "@/lib/pace-utils";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseDetail(overrides: Partial<CityDetail>): CityDetail {
|
||||||
|
return {
|
||||||
|
name: "seoul",
|
||||||
|
display_name: "Seoul",
|
||||||
|
local_date: "2026-05-17",
|
||||||
|
local_time: "12:00",
|
||||||
|
temp_symbol: "°C",
|
||||||
|
current: {
|
||||||
|
temp: 25,
|
||||||
|
max_so_far: 27,
|
||||||
|
max_temp_time: null,
|
||||||
|
wu_settlement: null,
|
||||||
|
station_code: "RKSI",
|
||||||
|
obs_time: "12:00",
|
||||||
|
obs_age_min: 1,
|
||||||
|
wind_speed_kt: null,
|
||||||
|
wind_dir: null,
|
||||||
|
humidity: null,
|
||||||
|
cloud_desc: null,
|
||||||
|
clouds_raw: [],
|
||||||
|
visibility_mi: null,
|
||||||
|
wx_desc: null,
|
||||||
|
},
|
||||||
|
hourly: {
|
||||||
|
times: ["10:00", "11:00", "12:00", "13:00"],
|
||||||
|
temps: [24, 25, 26, 27],
|
||||||
|
},
|
||||||
|
forecast: { today_high: 29 },
|
||||||
|
deb: { prediction: 29 },
|
||||||
|
...overrides,
|
||||||
|
} as CityDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runTests() {
|
||||||
|
const seoul = baseDetail({
|
||||||
|
airport_primary: {
|
||||||
|
temp: 40,
|
||||||
|
max_so_far: 41,
|
||||||
|
obs_time: "12:00",
|
||||||
|
source_code: "amos",
|
||||||
|
source_label: "AMOS",
|
||||||
|
station_code: "RKSI",
|
||||||
|
},
|
||||||
|
airport_current: {
|
||||||
|
temp: 26,
|
||||||
|
obs_time: "12:00",
|
||||||
|
source_label: "METAR",
|
||||||
|
station_code: "RKSI",
|
||||||
|
},
|
||||||
|
amos: {
|
||||||
|
source: "amos",
|
||||||
|
runway_temp_range: [39, 41],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(
|
||||||
|
shouldSuppressKoreanRunwayObservation(seoul),
|
||||||
|
"Seoul AMOS runway observation should be suppressed in web city decision",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
getDisplayAirportPrimary(seoul) == null,
|
||||||
|
"Seoul airport_primary AMOS runway observation must not be exposed for display",
|
||||||
|
);
|
||||||
|
|
||||||
|
const pace = getTodayPaceView(seoul, "zh-CN");
|
||||||
|
assert(pace?.observedNow === 26, "pace view should fall back to METAR/current, not AMOS runway temp");
|
||||||
|
|
||||||
|
const busan = baseDetail({
|
||||||
|
name: "busan",
|
||||||
|
display_name: "Busan",
|
||||||
|
airport_primary: {
|
||||||
|
temp: 35,
|
||||||
|
obs_time: "12:00",
|
||||||
|
source_label: "AMSC AWOS",
|
||||||
|
station_code: "RKPK",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(
|
||||||
|
getDisplayAirportPrimary(busan) == null,
|
||||||
|
"Busan AMSC/AWOS runway observation must not be exposed for display",
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokyo = baseDetail({
|
||||||
|
name: "tokyo",
|
||||||
|
display_name: "Tokyo",
|
||||||
|
airport_primary: {
|
||||||
|
temp: 31,
|
||||||
|
obs_time: "12:00",
|
||||||
|
source_label: "AMOS",
|
||||||
|
station_code: "RJTT",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(
|
||||||
|
getDisplayAirportPrimary(tokyo)?.temp === 31,
|
||||||
|
"non-Korean cities should not be affected by the Seoul/Busan runway suppression",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
AiCityForecastPayload,
|
AiCityForecastPayload,
|
||||||
AiCityForecastState,
|
AiCityForecastState,
|
||||||
} from "@/components/dashboard/scan-terminal/types";
|
} from "@/components/dashboard/scan-terminal/types";
|
||||||
|
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||||
import type { CityDetail } from "@/lib/dashboard-types";
|
import type { CityDetail } from "@/lib/dashboard-types";
|
||||||
import { normalizeCityKey } from "./decision-utils";
|
import { normalizeCityKey } from "./decision-utils";
|
||||||
|
|
||||||
@@ -167,11 +168,12 @@ function computeFallbackPredictedMax(detail: CityDetail | null): number | null {
|
|||||||
predicted = values.reduce((a, b) => a + b, 0) / values.length;
|
predicted = values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
if (predicted == null) {
|
if (predicted == null) {
|
||||||
const isHkoObservation = isHkoObservationCity(detail);
|
const isHkoObservation = isHkoObservationCity(detail);
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const currentTemp =
|
const currentTemp =
|
||||||
(isHkoObservation
|
(isHkoObservation
|
||||||
? detail?.current?.temp
|
? detail?.current?.temp
|
||||||
: detail?.airport_current?.temp ??
|
: detail?.airport_current?.temp ??
|
||||||
detail?.airport_primary?.temp ??
|
displayAirportPrimary?.temp ??
|
||||||
detail?.current?.temp) ?? null;
|
detail?.current?.temp) ?? null;
|
||||||
predicted = currentTemp != null && Number.isFinite(currentTemp)
|
predicted = currentTemp != null && Number.isFinite(currentTemp)
|
||||||
? currentTemp
|
? currentTemp
|
||||||
@@ -193,11 +195,12 @@ export function buildAiCityFallbackPayload({
|
|||||||
}): AiCityForecastPayload {
|
}): AiCityForecastPayload {
|
||||||
const tempSymbol = detail?.temp_symbol || "°C";
|
const tempSymbol = detail?.temp_symbol || "°C";
|
||||||
const isHkoObservation = isHkoObservationCity(detail);
|
const isHkoObservation = isHkoObservationCity(detail);
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const currentTemp =
|
const currentTemp =
|
||||||
(isHkoObservation
|
(isHkoObservation
|
||||||
? detail?.current?.temp
|
? detail?.current?.temp
|
||||||
: detail?.airport_current?.temp ??
|
: detail?.airport_current?.temp ??
|
||||||
detail?.airport_primary?.temp ??
|
displayAirportPrimary?.temp ??
|
||||||
detail?.current?.temp) ?? null;
|
detail?.current?.temp) ?? null;
|
||||||
const currentText =
|
const currentText =
|
||||||
currentTemp != null && Number.isFinite(Number(currentTemp))
|
currentTemp != null && Number.isFinite(Number(currentTemp))
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { AirportCurrentConditions, CityDetail } from "@/lib/dashboard-types";
|
||||||
|
|
||||||
|
function normalizeCityKey(value?: string | null) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s_-]+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const KOREAN_RUNWAY_OBSERVATION_CITY_KEYS = new Set(["seoul", "busan"]);
|
||||||
|
|
||||||
|
function hasRunwayObservationPayload(detail: CityDetail) {
|
||||||
|
return Boolean(
|
||||||
|
detail.amos?.runway_temp_range ||
|
||||||
|
(detail.amos?.runway_temps?.length || 0) > 0 ||
|
||||||
|
(detail.amos?.runway_obs?.temperatures?.length || 0) > 0 ||
|
||||||
|
(detail.amos?.runway_obs?.point_temperatures?.length || 0) > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunwayObservationSource(
|
||||||
|
airportPrimary?: AirportCurrentConditions | null,
|
||||||
|
detail?: CityDetail | null,
|
||||||
|
) {
|
||||||
|
const sourceText = [
|
||||||
|
airportPrimary?.source_code,
|
||||||
|
airportPrimary?.source_label,
|
||||||
|
airportPrimary?.station_label,
|
||||||
|
detail?.amos?.source,
|
||||||
|
detail?.amos?.source_label,
|
||||||
|
detail?.amos?.temp_source,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
return (
|
||||||
|
sourceText.includes("amos") ||
|
||||||
|
sourceText.includes("amsc") ||
|
||||||
|
sourceText.includes("awos") ||
|
||||||
|
sourceText.includes("runway") ||
|
||||||
|
sourceText.includes("跑道")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldSuppressKoreanRunwayObservation(detail?: CityDetail | null) {
|
||||||
|
if (!detail) return false;
|
||||||
|
const cityKey = normalizeCityKey(detail.name) || normalizeCityKey(detail.display_name);
|
||||||
|
if (!KOREAN_RUNWAY_OBSERVATION_CITY_KEYS.has(cityKey)) return false;
|
||||||
|
return isRunwayObservationSource(detail.airport_primary, detail) || hasRunwayObservationPayload(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayAirportPrimary(detail?: CityDetail | null) {
|
||||||
|
if (!detail || shouldSuppressKoreanRunwayObservation(detail)) return undefined;
|
||||||
|
return detail.airport_primary;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CityDetail } from "@/lib/dashboard-types";
|
import type { CityDetail } from "@/lib/dashboard-types";
|
||||||
|
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||||
import type { Locale } from "@/lib/i18n";
|
import type { Locale } from "@/lib/i18n";
|
||||||
import {
|
import {
|
||||||
getNoaaStationCode,
|
getNoaaStationCode,
|
||||||
@@ -326,6 +327,7 @@ function normalizeObservationTimeForChart(
|
|||||||
function buildCurrentObservationFallback(
|
function buildCurrentObservationFallback(
|
||||||
detail: CityDetail,
|
detail: CityDetail,
|
||||||
): Array<{ time?: string; temp?: number | null; sourceLabel?: string | null }> {
|
): Array<{ time?: string; temp?: number | null; sourceLabel?: string | null }> {
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const candidates: Array<{
|
const candidates: Array<{
|
||||||
sourceLabel?: string | null;
|
sourceLabel?: string | null;
|
||||||
temp?: number | null;
|
temp?: number | null;
|
||||||
@@ -337,9 +339,9 @@ function buildCurrentObservationFallback(
|
|||||||
time: detail.current?.obs_time || detail.current?.report_time,
|
time: detail.current?.obs_time || detail.current?.report_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceLabel: detail.airport_primary?.source_label || "METAR",
|
sourceLabel: displayAirportPrimary?.source_label || "METAR",
|
||||||
temp: detail.airport_primary?.temp,
|
temp: displayAirportPrimary?.temp,
|
||||||
time: detail.airport_primary?.obs_time || detail.airport_primary?.report_time,
|
time: displayAirportPrimary?.obs_time || displayAirportPrimary?.report_time,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceLabel: detail.airport_current?.source_label || "METAR",
|
sourceLabel: detail.airport_current?.source_label || "METAR",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { formatTafMarkerType } from "@/lib/taf-utils";
|
import { formatTafMarkerType } from "@/lib/taf-utils";
|
||||||
import { normalizeHm } from "@/lib/time-utils";
|
import { normalizeHm } from "@/lib/time-utils";
|
||||||
import { getWeatherSummary } from "@/lib/weather-summary-utils";
|
import { getWeatherSummary } from "@/lib/weather-summary-utils";
|
||||||
|
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||||
|
|
||||||
export { getTemperatureChartData } from "@/lib/chart-utils";
|
export { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
export { getModelView, getProbabilityView } from "@/lib/model-utils";
|
export { getModelView, getProbabilityView } from "@/lib/model-utils";
|
||||||
@@ -1932,6 +1933,7 @@ function getOfficialObservationCandidates(detail: CityDetail) {
|
|||||||
|
|
||||||
function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
||||||
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const currentSource = String(
|
const currentSource = String(
|
||||||
detail.current?.settlement_source ||
|
detail.current?.settlement_source ||
|
||||||
detail.current?.settlement_source_label ||
|
detail.current?.settlement_source_label ||
|
||||||
@@ -1940,7 +1942,7 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
|||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
||||||
const isNmcAirport = String(detail.airport_primary?.source_label || "")
|
const isNmcAirport = String(displayAirportPrimary?.source_label || "")
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes("nmc");
|
.includes("nmc");
|
||||||
@@ -1950,8 +1952,8 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
|||||||
temp: detail.airport_current?.temp,
|
temp: detail.airport_current?.temp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceLabel: detail.airport_primary?.source_label || "METAR",
|
sourceLabel: displayAirportPrimary?.source_label || "METAR",
|
||||||
temp: isNmcAirport ? null : detail.airport_primary?.temp,
|
temp: isNmcAirport ? null : displayAirportPrimary?.temp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceLabel: detail.current?.settlement_source_label,
|
sourceLabel: detail.current?.settlement_source_label,
|
||||||
@@ -1988,6 +1990,7 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
|||||||
function getObservationUpdateProfile(detail: CityDetail, locale: Locale) {
|
function getObservationUpdateProfile(detail: CityDetail, locale: Locale) {
|
||||||
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
||||||
const mgmFirstRecord = asRecord(mgmNearby[0]);
|
const mgmFirstRecord = asRecord(mgmNearby[0]);
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
const currentSource = String(
|
const currentSource = String(
|
||||||
detail.current?.settlement_source ||
|
detail.current?.settlement_source ||
|
||||||
detail.current?.settlement_source_label ||
|
detail.current?.settlement_source_label ||
|
||||||
@@ -1998,11 +2001,11 @@ function getObservationUpdateProfile(detail: CityDetail, locale: Locale) {
|
|||||||
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
||||||
const rawValue = firstNonEmptyString([
|
const rawValue = firstNonEmptyString([
|
||||||
isNmcCurrent ? "" : detail.current?.obs_time,
|
isNmcCurrent ? "" : detail.current?.obs_time,
|
||||||
localObservationTimeCandidate(detail.airport_primary?.obs_time),
|
localObservationTimeCandidate(displayAirportPrimary?.obs_time),
|
||||||
localObservationTimeCandidate(detail.airport_current?.obs_time),
|
localObservationTimeCandidate(detail.airport_current?.obs_time),
|
||||||
localObservationTimeCandidate(mgmFirstRecord?.obs_time),
|
localObservationTimeCandidate(mgmFirstRecord?.obs_time),
|
||||||
localObservationTimeCandidate(mgmFirstRecord?.time),
|
localObservationTimeCandidate(mgmFirstRecord?.time),
|
||||||
detail.airport_primary?.report_time,
|
displayAirportPrimary?.report_time,
|
||||||
detail.airport_current?.report_time,
|
detail.airport_current?.report_time,
|
||||||
isNmcCurrent ? "" : detail.current?.report_time,
|
isNmcCurrent ? "" : detail.current?.report_time,
|
||||||
mgmFirstRecord?.report_time,
|
mgmFirstRecord?.report_time,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CityDetail } from "@/lib/dashboard-types";
|
import type { CityDetail } from "@/lib/dashboard-types";
|
||||||
|
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||||
import type { Locale } from "@/lib/i18n";
|
import type { Locale } from "@/lib/i18n";
|
||||||
import {
|
import {
|
||||||
hmToMinutes,
|
hmToMinutes,
|
||||||
@@ -18,10 +19,11 @@ export function getTodayPaceView(
|
|||||||
const times = hourly.times || [];
|
const times = hourly.times || [];
|
||||||
const temps = hourly.temps || [];
|
const temps = hourly.temps || [];
|
||||||
if (!times.length || !temps.length) return null;
|
if (!times.length || !temps.length) return null;
|
||||||
|
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||||
|
|
||||||
const currentMinutes =
|
const currentMinutes =
|
||||||
hmToMinutes(detail.local_time) ??
|
hmToMinutes(detail.local_time) ??
|
||||||
hmToMinutes(detail.airport_primary?.obs_time) ??
|
hmToMinutes(displayAirportPrimary?.obs_time) ??
|
||||||
hmToMinutes(detail.airport_current?.obs_time) ??
|
hmToMinutes(detail.airport_current?.obs_time) ??
|
||||||
hmToMinutes(detail.current?.obs_time);
|
hmToMinutes(detail.current?.obs_time);
|
||||||
if (currentMinutes == null) return null;
|
if (currentMinutes == null) return null;
|
||||||
@@ -39,7 +41,7 @@ export function getTodayPaceView(
|
|||||||
if (expectedNow == null) return null;
|
if (expectedNow == null) return null;
|
||||||
|
|
||||||
const observedNowCandidate = [
|
const observedNowCandidate = [
|
||||||
detail.airport_primary?.temp,
|
displayAirportPrimary?.temp,
|
||||||
detail.airport_current?.temp,
|
detail.airport_current?.temp,
|
||||||
detail.current?.temp,
|
detail.current?.temp,
|
||||||
]
|
]
|
||||||
@@ -75,7 +77,7 @@ export function getTodayPaceView(
|
|||||||
: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`;
|
: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`;
|
||||||
|
|
||||||
const topObservedCandidate = [
|
const topObservedCandidate = [
|
||||||
detail.airport_primary?.max_so_far,
|
displayAirportPrimary?.max_so_far,
|
||||||
detail.airport_current?.max_so_far,
|
detail.airport_current?.max_so_far,
|
||||||
detail.current?.max_so_far,
|
detail.current?.max_so_far,
|
||||||
observedNow,
|
observedNow,
|
||||||
@@ -105,7 +107,7 @@ export function getTodayPaceView(
|
|||||||
).padStart(2, "0")}:00`
|
).padStart(2, "0")}:00`
|
||||||
: "--";
|
: "--";
|
||||||
const observedLabel =
|
const observedLabel =
|
||||||
detail.airport_primary?.temp != null || detail.airport_current?.temp != null
|
displayAirportPrimary?.temp != null || detail.airport_current?.temp != null
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? "Airport obs"
|
? "Airport obs"
|
||||||
: "机场实测"
|
: "机场实测"
|
||||||
|
|||||||
Reference in New Issue
Block a user