feat: Implement core PolyWeather application with city weather query service, trend analysis, data collection, and dashboard UI.

This commit is contained in:
2569718930@qq.com
2026-03-11 10:49:35 +08:00
parent d2a40462c5
commit 1958b2764b
17 changed files with 24 additions and 279 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ const GUIDE_CARDS = {
title: "Ankara 专属增强",
},
{
body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市补充 Meteoblue;0-2 小时临近判断优先看 METAR 与周边站。",
body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主;0-2 小时临近判断优先看 METAR 与周边站。",
title: "未来日期分析",
},
{
@@ -48,7 +48,7 @@ const GUIDE_CARDS = {
title: "Ankara-specific Enhancement",
},
{
body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo, with Meteoblue in selected cities; 0-2h nowcast prioritizes METAR and nearby stations.",
body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo; 0-2h nowcast prioritizes METAR and nearby stations.",
title: "Future-date Analysis",
},
{
@@ -552,18 +552,8 @@ export function ModelForecast({
}) {
const { locale, t } = useI18n();
const view = getModelView(detail, targetDate);
const fallbackMeteoblue = detail.source_forecasts?.meteoblue?.today_high;
const modelsMap = { ...view.models };
// 强行插入 Meteoblue,防止 helper 函数或日期对齐导致其被剔除
if (
modelsMap["Meteoblue"] == null &&
fallbackMeteoblue != null &&
(!targetDate || targetDate === detail.local_date)
) {
modelsMap["Meteoblue"] = fallbackMeteoblue;
}
const modelEntries = Object.entries(modelsMap).filter(
([, value]) =>
value !== null && value !== undefined && Number.isFinite(Number(value)),
@@ -591,12 +581,6 @@ export function ModelForecast({
? Math.max(...comparisonValues) + 1
: 1;
const range = Math.max(maxValue - minValue, 1);
const getModelDisplayName = (name: string) => {
if (String(name).trim().toLowerCase() === "meteoblue") {
return "Meteoblue";
}
return name;
};
return (
<section className="models-section">
@@ -615,7 +599,7 @@ export function ModelForecast({
return (
<div key={name} className="model-row">
<div className="model-name" title={name}>
{getModelDisplayName(name)}
{name}
</div>
<div className="model-bar-track">
<div
-6
View File
@@ -168,12 +168,6 @@ export interface SourceForecasts {
weather_gov?: {
forecast_periods?: WeatherGovPeriod[];
};
meteoblue?: {
today_high?: number | null;
daily_highs?: Array<number | null>;
unit?: string | null;
url?: string | null;
};
}
export interface DailyModelForecast {
+2 -55
View File
@@ -330,67 +330,18 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
}
export function getModelView(detail: CityDetail, targetDate?: string | null) {
const toFiniteNumber = (value: unknown): number | null => {
if (value === null || value === undefined || value === "") return null;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
};
const pickMeteoblueForDate = (dateStr: string) => {
const meteoblue = detail.source_forecasts?.meteoblue;
if (!meteoblue) return null;
const dates = detail.forecast?.daily?.map((item) => item.date) || [];
const index = dates.findIndex((date) => date === dateStr);
const todayHigh = toFiniteNumber(meteoblue.today_high);
// Today must always use Meteoblue daily max (today_high) first.
if (dateStr === detail.local_date && todayHigh != null) {
return todayHigh;
}
const dailyHighs = Array.isArray(meteoblue.daily_highs)
? meteoblue.daily_highs
: [];
if (index >= 0) {
const byDailyHigh = toFiniteNumber(dailyHighs[index]);
if (byDailyHigh != null) return byDailyHigh;
}
if (index === 0 && todayHigh != null) return todayHigh;
return null;
};
const withMeteoblue = (
models: Record<string, number | null>,
dateStr: string,
) => {
const nextModels = { ...models };
const existing = toFiniteNumber(nextModels.Meteoblue);
if (existing == null) {
const meteoblueVal = pickMeteoblueForDate(dateStr);
if (meteoblueVal != null) {
nextModels.Meteoblue = meteoblueVal;
}
} else {
nextModels.Meteoblue = existing;
}
return nextModels;
};
const date = targetDate || detail.local_date;
const daily = detail.multi_model_daily?.[date];
if (daily) {
return {
deb: daily.deb?.prediction ?? null,
models: withMeteoblue(daily.models || {}, date),
models: daily.models || {},
};
}
return {
deb: detail.deb?.prediction ?? null,
models: withMeteoblue(detail.multi_model || {}, date),
models: detail.multi_model || {},
};
}
@@ -712,10 +663,6 @@ export function computeFrontTrendSignal(
? isEnglish(locale)
? "Temperature declines with pressure rebound and/or northerly shift. Next 6-48h leans cold-front suppression."
: "温度下滑、气压回升或风向转北,未来 6-48 小时更像冷锋或冷平流压制。"
: detail.name !== "ankara" && Boolean(detail.source_forecasts?.meteoblue)
? isEnglish(locale)
? "Structured trend layer mainly uses weather.gov, Open-Meteo and Meteoblue for 6-48h warm/cold flow judgement."
: "结构化来源以 weather.gov、Open-Meteo、Meteoblue 为主,用于判断未来 6-48 小时冷暖平流趋势。"
: isEnglish(locale)
? "Structured trend layer mainly uses weather.gov and Open-Meteo for 6-48h warm/cold flow judgement."
: "结构化来源以 weather.gov 和 Open-Meteo 为主,用于判断未来 6-48 小时冷暖平流趋势。",
+2 -2
View File
@@ -42,7 +42,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather 系统技术说明",
"guide.closeAria": "关闭技术说明",
"guide.footer":
"数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主,部分城市补充 Meteoblue。",
"数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主。",
"history.title": "📊 历史准确率对账 - {city}",
"history.closeAria": "关闭历史对账",
@@ -143,7 +143,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather Technical Overview",
"guide.closeAria": "Close technical overview",
"guide.footer":
"Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov, with Meteoblue added for selected cities.",
"Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov.",
"history.title": "📊 Historical Reconciliation - {city}",
"history.closeAria": "Close history reconciliation",
-1
View File
@@ -186,7 +186,6 @@ export interface ModelComparison {
JMA?: number;
MGM?: number;
NWS?: number;
Meteoblue?: number;
}
export interface DEBAnalysis {
+3 -6
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@@ -151,7 +151,7 @@
</div>
<div class="guide-card">
<h3>&#128198; &#x672A;&#x6765;&#x65E5;&#x671F;&#x5206;&#x6790;</h3>
<p>&#x70B9;&#x51FB;&#x591A;&#x65E5;&#x9884;&#x62A5;&#x540E;&#x6253;&#x5F00;&#x7684;&#x6A21;&#x6001;&#x6846;&#xFF0C;&#x4E3B;&#x8981;&#x7528;&#x4E8E;&#x5206;&#x6790;&#x4E0B;&#x4E00;&#x4E2A;&#x4EA4;&#x6613;&#x65E5;&#x3002; 6-48 &#x5C0F;&#x65F6;&#x8D8B;&#x52BF;&#x4EE5; weather.gov &#x548C; Open-Meteo &#x4E3A;&#x4E3B;&#xFF0C;&#x90E8;&#x5206;&#x57CE;&#x5E02;&#x53EF;&#x4F1A;&#x8865;&#x5145; Meteoblue&#xFF1B; 0-2 &#x5C0F;&#x65F6;&#x4E34;&#x8FD1;&#x5224;&#x65AD;&#x5219;&#x4F18;&#x5148;&#x770B; METAR &#x4E0E;&#x5468;&#x8FB9;&#x7AD9;&#x3002;</p>
<p>&#x70B9;&#x51FB;&#x591A;&#x65E5;&#x9884;&#x62A5;&#x540E;&#x6253;&#x5F00;&#x7684;&#x6A21;&#x6001;&#x6846;&#xFF0C;&#x4E3B;&#x8981;&#x7528;&#x4E8E;&#x5206;&#x6790;&#x4E0B;&#x4E00;&#x4E2A;&#x4EA4;&#x6613;&#x65E5;&#x3002; 6-48 &#x5C0F;&#x65F6;&#x8D8B;&#x52BF;&#x4EE5; weather.gov &#x548C; Open-Meteo &#x4E3A;&#x4E3B;&#xFF1B; 0-2 &#x5C0F;&#x65F6;&#x4E34;&#x8FD1;&#x5224;&#x65AD;&#x5219;&#x4F18;&#x5148;&#x770B; METAR &#x4E0E;&#x5468;&#x8FB9;&#x7AD9;&#x3002;</p>
</div>
<div class="guide-card">
<h3>&#128202; &#x5386;&#x53F2;&#x5BF9;&#x8D26;&#x89C4;&#x5219;</h3>
@@ -159,7 +159,7 @@
</div>
</div>
<div class="guide-footer">
<p>&#x203B; &#x6570;&#x636E;&#x6E90;&#x4EE5; Aviation Weather / METAR&#x3001;Turkish MGM&#x3001;Open-Meteo&#x3001;weather.gov &#x4E3A;&#x4E3B;&#xFF0C;&#x90E8;&#x5206;&#x57CE;&#x5E02;&#x8865;&#x5145; Meteoblue&#x3002;</p>
<p>&#x203B; &#x6570;&#x636E;&#x6E90;&#x4EE5; Aviation Weather / METAR&#x3001;Turkish MGM&#x3001;Open-Meteo&#x3001;weather.gov &#x4E3A;&#x4E3B;&#x3002;</p>
</div>
</div>
</div>
@@ -236,6 +236,3 @@
</html>