feat: implement core logic and visualization components for runway temperature monitoring with SSE patching support

This commit is contained in:
2569718930@qq.com
2026-05-27 00:44:38 +08:00
parent 6b2c99cea4
commit 40dcd6e8f0
4 changed files with 137 additions and 22 deletions
@@ -327,6 +327,21 @@ function getLocalDayBounds(localDateStr: string): LocalDayBounds | null {
return Number.isFinite(start) ? { start, end: start + DAY_MS } : null;
}
function dateFromLocalTime(value?: string | null) {
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(value || "").trim());
return match ? `${match[1]}-${match[2]}-${match[3]}` : null;
}
function resolveChartLocalDate(row: ScanOpportunityRow | null, hourly: HourlyForecast) {
return (
hourly?.localDate ||
dateFromLocalTime(hourly?.localTime) ||
row?.local_date ||
dateFromLocalTime(row?.local_time) ||
new Date().toISOString().slice(0, 10)
);
}
function isWithinLocalDay(ts: number | null, bounds: LocalDayBounds | null) {
return ts !== null && Number.isFinite(ts) && (!bounds || (ts >= bounds.start && ts < bounds.end));
}
@@ -423,7 +438,7 @@ function getObservationDisplayMetrics(
settlementPlate?: { maxTemp: number | null } | null,
) {
const tzOffset = row?.tz_offset_seconds ?? 0;
const localDateStr = row?.local_date || null;
const localDateStr = resolveChartLocalDate(row, hourly);
const settlementObs = normObs(hourly?.settlementTodayObs || row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset, MAX_OBS_POINTS, localDateStr);
const metarObs = normObs(hourly?.metarTodayObs || row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, tzOffset, MAX_OBS_POINTS, localDateStr);
const madisObs = normObs(hourly?.airportPrimaryTodayObs, tzOffset, MAX_OBS_POINTS, localDateStr);
@@ -509,6 +524,7 @@ function runwayLabelFromPair(rawPair: unknown, index: number) {
type HourlyForecast = {
forecastTodayHigh?: number | null;
debPrediction?: number | null;
localDate?: string | null;
localTime?: string | null;
times: string[];
temps: Array<number | null>;
@@ -531,6 +547,7 @@ function seedHourlyForecastFromRow(row: ScanOpportunityRow | null): HourlyForeca
return {
forecastTodayHigh: null,
debPrediction: validNumber(row.deb_prediction),
localDate: row.local_date || null,
localTime: row.local_time || null,
times: [],
temps: [],
@@ -559,6 +576,7 @@ function parseHourlyForecastFromCityDetail(json: CityDetail | null): HourlyForec
return {
forecastTodayHigh: json.forecast?.today_high ?? null,
debPrediction: json.deb?.prediction ?? (json as any)?.overview?.deb_prediction ?? null,
localDate: json.local_date || (json as any)?.overview?.local_date || null,
localTime: json.local_time || null,
times: hourlySource.times || [],
temps: hourlySource.temps || [],
@@ -687,7 +705,7 @@ function getLiveObservationLabels(
: isTaipei ? "CWA (10分钟)"
: isWeatherStation ? "气象站实测"
: isRunwaySensorCity ? "跑道实测 (1分钟)"
: "机场气象站";
: "机场报文";
const metarHeaderLabel = (isShenzhen || isHKO) ? "天文台实测 (10分钟)"
: "METAR 结算 (30分钟)";
@@ -700,7 +718,7 @@ function getLiveObservationLabels(
: isTaipei ? "CWA"
: isWeatherStation ? "气象站"
: isRunwaySensorCity ? "跑道实测"
: "机场气象站";
: "机场报文";
const metarHighLabel = isShenzhen ? "天文台"
: isHKO ? "天文台"
@@ -735,6 +753,7 @@ function mergePatchIntoHourly(
...(prev || {
forecastTodayHigh: null,
debPrediction: null,
localDate: null,
localTime: null,
times: [],
temps: [],
@@ -744,6 +763,10 @@ function mergePatchIntoHourly(
...explicitHourlyPatch,
};
if (typeof (changes as any).local_date === "string") {
next.localDate = (changes as any).local_date;
}
if (changes.amos && typeof changes.amos === "object") {
const oldAmos = prev?.amos || {};
const newAmos = changes.amos as AmosData;
@@ -982,7 +1005,7 @@ function buildDailyChartData(
hourly: HourlyForecast,
daysCount: number,
): { data: Array<Record<string, string | number | null>>; series: EvidenceSeries[] } {
const localDateStr = row?.local_date || new Date().toISOString().slice(0, 10);
const localDateStr = resolveChartLocalDate(row, hourly);
const slots = generateDailySlots(localDateStr, daysCount);
const series: EvidenceSeries[] = [
@@ -1162,7 +1185,7 @@ function buildFullDayChartData(
isEn: boolean,
): { data: Array<Record<string, any>>; series: EvidenceSeries[] } {
const tzOffset = row?.tz_offset_seconds ?? 0;
const localDateStr = row?.local_date || new Date().toISOString().slice(0, 10);
const localDateStr = resolveChartLocalDate(row, hourly);
const localDayBounds = getLocalDayBounds(localDateStr);
const settlementObs = filterTimelinePointsToLocalDay(