feat: Add initial PolyWeather map application with a premium dark theme.
This commit is contained in:
+4
-1
@@ -354,6 +354,8 @@ def _analyze(city: str) -> Dict[str, Any]:
|
|||||||
if mgm:
|
if mgm:
|
||||||
mgc = mgm.get("current", {})
|
mgc = mgm.get("current", {})
|
||||||
mgm_data = {
|
mgm_data = {
|
||||||
|
"temp": _sf(mgc.get("temp")),
|
||||||
|
"time": mgc.get("time"),
|
||||||
"feels_like": _sf(mgc.get("feels_like")),
|
"feels_like": _sf(mgc.get("feels_like")),
|
||||||
"humidity": _sf(mgc.get("humidity")),
|
"humidity": _sf(mgc.get("humidity")),
|
||||||
"wind_dir": _sf(mgc.get("wind_dir")),
|
"wind_dir": _sf(mgc.get("wind_dir")),
|
||||||
@@ -462,7 +464,8 @@ def _analyze(city: str) -> Dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
proxy = _config.get("proxy")
|
proxy = _config.get("proxy")
|
||||||
fetch_weather_markets(proxy=proxy, timeout=10)
|
fetch_weather_markets(proxy=proxy, timeout=10)
|
||||||
city_mkts = get_city_markets(city, target_date=local_date_str)
|
# We don't filter by target_date here, we want ALL active contracts for the city
|
||||||
|
city_mkts = get_city_markets(city)
|
||||||
if city_mkts:
|
if city_mkts:
|
||||||
result["polymarket"] = {
|
result["polymarket"] = {
|
||||||
"markets": [
|
"markets": [
|
||||||
|
|||||||
+64
-3
@@ -435,6 +435,18 @@ function renderHero(data) {
|
|||||||
parts.push(`<span>👁️ ${cur.visibility_mi}mi</span>`);
|
parts.push(`<span>👁️ ${cur.visibility_mi}mi</span>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MGM info (Ankara specific)
|
||||||
|
if (data.mgm?.temp != null) {
|
||||||
|
let mgmTimeStr = "";
|
||||||
|
if (data.mgm.time) {
|
||||||
|
const match = data.mgm.time.match(/T?(\d{2}:\d{2})/);
|
||||||
|
if (match) mgmTimeStr = ` @${match[1]}`;
|
||||||
|
}
|
||||||
|
parts.push(
|
||||||
|
`<span style="color:#eab308;font-weight:600;background:rgba(234, 179, 8, 0.1);padding:2px 8px;border-radius:12px;border:1px solid rgba(234, 179, 8, 0.3);">⭐ MGM 实测: ${data.mgm.temp}${sym}${mgmTimeStr}</span>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Trend badge
|
// Trend badge
|
||||||
const trend = data.trend || {};
|
const trend = data.trend || {};
|
||||||
if (trend.is_dead_market) {
|
if (trend.is_dead_market) {
|
||||||
@@ -507,12 +519,29 @@ function renderChart(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MGM observation point (Ankara specific)
|
||||||
|
const mgmPoints = new Array(times.length).fill(null);
|
||||||
|
if (data.mgm?.temp != null && data.mgm?.time) {
|
||||||
|
const timeMatch = data.mgm.time.match(/T?(\d{2}):(\d{2})/);
|
||||||
|
if (timeMatch) {
|
||||||
|
let h = parseInt(timeMatch[1], 10);
|
||||||
|
const m = parseInt(timeMatch[2], 10);
|
||||||
|
if (m >= 30) h = (h + 1) % 24;
|
||||||
|
const hourKey = h.toString().padStart(2, "0") + ":00";
|
||||||
|
const idx = times.indexOf(hourKey);
|
||||||
|
if (idx >= 0) {
|
||||||
|
mgmPoints[idx] = data.mgm.temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = document.getElementById("tempChart").getContext("2d");
|
const ctx = document.getElementById("tempChart").getContext("2d");
|
||||||
if (tempChart) tempChart.destroy();
|
if (tempChart) tempChart.destroy();
|
||||||
|
|
||||||
const validDebTemps = debTemps.filter((t) => t != null);
|
const validDebTemps = debTemps.filter((t) => t != null);
|
||||||
const validMetar = metarPoints.filter((t) => t != null);
|
const validMetar = metarPoints.filter((t) => t != null);
|
||||||
const allVals = [...validDebTemps, ...validMetar];
|
const validMgm = mgmPoints.filter((t) => t != null);
|
||||||
|
const allVals = [...validDebTemps, ...validMetar, ...validMgm];
|
||||||
if (allVals.length === 0) {
|
if (allVals.length === 0) {
|
||||||
document.getElementById("chartLegend").textContent = "暂无数据";
|
document.getElementById("chartLegend").textContent = "暂无数据";
|
||||||
return;
|
return;
|
||||||
@@ -555,11 +584,26 @@ function renderChart(data) {
|
|||||||
pointHoverRadius: 7,
|
pointHoverRadius: 7,
|
||||||
pointStyle: "circle",
|
pointStyle: "circle",
|
||||||
fill: false,
|
fill: false,
|
||||||
showLine: false,
|
|
||||||
order: 0,
|
order: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (validMgm.length > 0) {
|
||||||
|
datasets.push({
|
||||||
|
label: "MGM实测",
|
||||||
|
data: mgmPoints,
|
||||||
|
borderColor: "#facc15",
|
||||||
|
backgroundColor: "#facc15",
|
||||||
|
borderWidth: 0,
|
||||||
|
pointRadius: 7,
|
||||||
|
pointHoverRadius: 9,
|
||||||
|
pointStyle: "star",
|
||||||
|
fill: false,
|
||||||
|
showLine: false,
|
||||||
|
order: -1, // Draw on very top
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add subtle OM reference line if DEB offset is significant
|
// Add subtle OM reference line if DEB offset is significant
|
||||||
if (Math.abs(offset) > 0.3) {
|
if (Math.abs(offset) > 0.3) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
@@ -648,6 +692,9 @@ function renderChart(data) {
|
|||||||
// Chart legend text
|
// Chart legend text
|
||||||
const legend = document.getElementById("chartLegend");
|
const legend = document.getElementById("chartLegend");
|
||||||
const legendParts = [];
|
const legendParts = [];
|
||||||
|
if (data.mgm?.temp != null) {
|
||||||
|
legendParts.push(`MGM: ${data.mgm.temp}${data.temp_symbol}`);
|
||||||
|
}
|
||||||
if (debMax != null && omMax != null && Math.abs(offset) > 0.3) {
|
if (debMax != null && omMax != null && Math.abs(offset) > 0.3) {
|
||||||
const sign = offset > 0 ? "+" : "";
|
const sign = offset > 0 ? "+" : "";
|
||||||
legendParts.push(`DEB偏移 ${sign}${offset.toFixed(1)}° vs OM`);
|
legendParts.push(`DEB偏移 ${sign}${offset.toFixed(1)}° vs OM`);
|
||||||
@@ -827,16 +874,18 @@ function renderAI(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderMarket(data) {
|
function renderMarket(data) {
|
||||||
|
const widget = document.getElementById("marketFloatingWidget");
|
||||||
const container = document.getElementById("marketOdds");
|
const container = document.getElementById("marketOdds");
|
||||||
if (
|
if (
|
||||||
!data.polymarket ||
|
!data.polymarket ||
|
||||||
!data.polymarket.markets ||
|
!data.polymarket.markets ||
|
||||||
data.polymarket.markets.length === 0
|
data.polymarket.markets.length === 0
|
||||||
) {
|
) {
|
||||||
container.innerHTML = '<div class="market-no-data">暂无相关天气合约</div>';
|
widget.classList.add("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
widget.classList.remove("hidden");
|
||||||
const { markets, divergence } = data.polymarket;
|
const { markets, divergence } = data.polymarket;
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
@@ -919,6 +968,13 @@ function closePanel() {
|
|||||||
const panel = document.getElementById("panel");
|
const panel = document.getElementById("panel");
|
||||||
panel.classList.remove("visible");
|
panel.classList.remove("visible");
|
||||||
setTimeout(() => panel.classList.add("hidden"), 400);
|
setTimeout(() => panel.classList.add("hidden"), 400);
|
||||||
|
|
||||||
|
// Hide Polymarket widget
|
||||||
|
const marketWidget = document.getElementById("marketFloatingWidget");
|
||||||
|
if (marketWidget) {
|
||||||
|
marketWidget.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
selectedCity = null;
|
selectedCity = null;
|
||||||
setSelectedMarker(null);
|
setSelectedMarker(null);
|
||||||
document
|
document
|
||||||
@@ -1173,6 +1229,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
// Panel close
|
// Panel close
|
||||||
document.getElementById("panelClose").addEventListener("click", closePanel);
|
document.getElementById("panelClose").addEventListener("click", closePanel);
|
||||||
|
|
||||||
|
// Market Widget close
|
||||||
|
document.getElementById("marketClose").addEventListener("click", () => {
|
||||||
|
document.getElementById("marketFloatingWidget").classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
// Escape key
|
// Escape key
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") closePanel();
|
if (e.key === "Escape") closePanel();
|
||||||
|
|||||||
@@ -131,13 +131,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Polymarket Odds ── -->
|
|
||||||
<section class="market-section">
|
|
||||||
<h3>📈 市场赔率 <span style="font-size:11px;color:var(--text-muted);font-weight:400;">Polymarket</span></h3>
|
|
||||||
<div id="marketOdds" class="market-odds">
|
|
||||||
<div style="color:var(--text-muted);font-size:13px;">点击城市后加载...</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Risk Profile ── -->
|
<!-- ── Risk Profile ── -->
|
||||||
<section class="risk-section">
|
<section class="risk-section">
|
||||||
@@ -147,6 +140,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- ── Polymarket Floating Widget ── -->
|
||||||
|
<div id="marketFloatingWidget" class="market-floating hidden">
|
||||||
|
<div class="market-widget-header">
|
||||||
|
<h3>📈 Polymarket</h3>
|
||||||
|
<button class="market-close" id="marketClose">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="marketOdds" class="market-odds"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Loading Overlay ── -->
|
<!-- ── Loading Overlay ── -->
|
||||||
<div id="loading" class="loading-overlay hidden">
|
<div id="loading" class="loading-overlay hidden">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
|
|||||||
+59
-1
@@ -804,11 +804,69 @@ body {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Market Odds Section ── */
|
/* ── Market Odds Section (Floating Widget) ── */
|
||||||
|
.market-floating {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 20px;
|
||||||
|
width: 320px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-glass);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-floating.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-widget-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-widget-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.market-close:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.market-odds {
|
.market-odds {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-card {
|
.market-card {
|
||||||
|
|||||||
Reference in New Issue
Block a user