feat: Implement initial PolyWeather map application frontend with dark theme UI and data collection module.

This commit is contained in:
2569718930@qq.com
2026-03-05 17:10:52 +08:00
parent 1df14c6d3b
commit c6cf2c79a1
4 changed files with 140 additions and 19 deletions
+14 -3
View File
@@ -1390,12 +1390,23 @@ class WeatherDataCollector:
}
if city_lower in turkish_provinces:
istno, province = turkish_provinces[city_lower]
# 核心逻辑:实测用 istno (17128), 预报强制去 17130 拿
mgm_data = self.fetch_from_mgm(istno)
# 如果当前是机场站 (17128),我们额外去 17130 拿一次预报
if istno == "17128":
mgm_city_center = self.fetch_from_mgm("17130")
if mgm_city_center and mgm_data:
# 用市中心的预报覆盖机场可能缺失的预报
mgm_data["today_high"] = mgm_city_center.get("today_high")
mgm_data["daily_forecasts"] = mgm_city_center.get("daily_forecasts")
logger.info("⚡ 已同步 MGM 安卡拉总部 (17130) 的官方最高温预报")
if mgm_data:
results["mgm"] = mgm_data
nearby = self.fetch_mgm_nearby_stations(province, root_ist_no=istno)
if nearby:
results["mgm_nearby"] = nearby
nearby = self.fetch_mgm_nearby_stations(province, root_ist_no=istno)
if nearby:
results["mgm_nearby"] = nearby
# 全球通用:对有预定义集群的城市,抓取周边 METAR 参考站
if city_lower in self.CITY_METAR_CLUSTERS and "mgm_nearby" not in results:
+15 -2
View File
@@ -1270,17 +1270,30 @@ document.addEventListener("DOMContentLoaded", async () => {
if (e.key === "Escape") closePanel();
});
// History Modal Events
// Modal Event Listeners
const histModal = document.getElementById("historyModal");
const guideModal = document.getElementById("guideModal");
document
.getElementById("btnShowHistory")
.addEventListener("click", openHistoryModal);
document
.getElementById("historyModalClose")
.addEventListener("click", closeHistoryModal);
document.getElementById("historyModal").addEventListener("click", (e) => {
histModal.addEventListener("click", (e) => {
if (e.target.id === "historyModal") closeHistoryModal();
});
document.getElementById("btnShowGuide").addEventListener("click", () => {
guideModal.classList.remove("hidden");
});
document.getElementById("guideModalClose").addEventListener("click", () => {
guideModal.classList.add("hidden");
});
guideModal.addEventListener("click", (e) => {
if (e.target.id === "guideModal") guideModal.classList.add("hidden");
});
// Refresh all button
document
.getElementById("refreshAllBtn")
+39 -14
View File
@@ -29,12 +29,12 @@
<h1>🌡️ PolyWeather</h1>
<span class="subtitle">天气衍生品智能分析</span>
</div>
<div class="header-right">
<div class="live-badge" id="liveBadge">
<span class="pulse-dot"></span>
<span>实时</span>
</div>
<button class="refresh-btn" id="refreshAllBtn" title="刷新所有数据"></button>
<button class="info-btn" id="btnShowGuide" title="查看系统技术说明">️ 技术说明</button>
<div class="live-badge" id="liveBadge">
<span class="pulse-dot"></span>
<span>实时</span>
</div>
<button class="refresh-btn" id="refreshAllBtn" title="刷新所有数据"></button>
</div>
</header>
@@ -148,17 +148,42 @@
<span>正在获取气象数据,请稍候...</span>
</div>
<!-- ── History Chart Modal ── -->
<div id="historyModal" class="modal-overlay hidden">
<div class="modal-content">
<!-- ── Technical Guide Modal ── -->
<div id="guideModal" class="modal-overlay hidden">
<div class="modal-content large">
<div class="modal-header">
<h2>📊 <span id="historyModalTitle">历史准确率</span></h2>
<button class="modal-close" id="historyModalClose"></button>
<h2>📚 PolyWeather 系统技术说明</h2>
<button class="modal-close" id="guideModalClose"></button>
</div>
<div class="modal-body">
<div class="history-stats" id="historyStats"></div>
<div class="history-chart-wrapper">
<canvas id="historyChart"></canvas>
<div class="guide-grid">
<div class="guide-card">
<h3>🧬 DEB 动态融合预测</h3>
<p><b>Dynamic Ensemble Blending</b> 是系统的核心算法。它不只是简单的平均值,而是根据各个模型(ECMWF, GFS, MGM等)在**过去 30
天**内的实际表现,动态分配权重。表现越稳的模型,在最终决策中占比越高。</p>
</div>
<div class="guide-card">
<h3>🎲 结算概率引擎 (μ)</h3>
<p>基于正态分布模型。我们提取所有预报模型的离散度作为**不确定性系数 ($\sigma$)**,结合 DEB 预测值作为**期望值 ($\mu$)**。这能告诉你某个温度区间(如 10°C
盘口)发生的真实概率,辅助对冲决策。</p>
</div>
<div class="guide-card">
<h3>📍 结算点 (Airport) 逻辑</h3>
<p>Polymarket 结算是以**机场 METAR
气象站**为准。在安卡拉等复杂城市,我们同时监控**市中心总站**(潜力热岛)和**机场站**(物理结算点)。当两者温差巨大时,往往预示着随后的补涨跳升机会。</p>
</div>
<div class="guide-card">
<h3>⚠️ 风险偏置档案</h3>
<p>系统记录了每个城市机场站与市区的距离、海拔差及季度性偏差。例如,首尔仁川机场因靠海,最高温通常比市区偏低;慕尼黑机场因海拔高,夜间降温极快。这些偏置已自动整合进 AI 分析逻辑中。
</p>
</div>
<div class="guide-card">
<h3>📈 未来日期概率分布</h3>
<p>当你切换到未来 3-5 天时,系统会基于各模型对该日期的预报分歧度重新计算概率。分歧越大,分布越扁平(风险高);共识越强,分布越尖锐(机会明朗)。</p>
</div>
</div>
<div class="guide-footer">
<p>※ 数据来源:NOAA METAR, Turkish MGM, Open-Meteo Ensemble, Meteoblue, NWS.</p>
</div>
</div>
</div>
+72
View File
@@ -1183,3 +1183,75 @@ body {
height: 300px;
width: 100%;
}
/* ── Info Button ── */
.info-btn {
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.3);
color: var(--accent-blue);
padding: 6px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 6px;
}
.info-btn:hover {
background: rgba(99, 102, 241, 0.2);
border-color: var(--accent-blue);
transform: translateY(-1px);
}
/* ── Guide Modal Customizations ── */
.modal-content.large {
max-width: 900px;
}
.guide-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
padding: 4px;
}
.guide-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 16px;
transition: var(--transition);
}
.guide-card:hover {
background: rgba(255, 255, 255, 0.05);
border-color: var(--border-glass);
}
.guide-card h3 {
font-size: 15px;
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.guide-card p {
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
margin-bottom: 0;
}
.guide-card b {
color: var(--text-primary);
}
.guide-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--border-subtle);
text-align: center;
font-size: 11px;
color: var(--text-muted);
}