feat: implement initial PolyWeather application frontend with map, city list, detailed weather panel, and guide modals.

This commit is contained in:
2569718930@qq.com
2026-03-09 05:56:22 +08:00
parent d45ee8a981
commit d162b91ed9
3 changed files with 125 additions and 12 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<main className="h-screen w-screen overflow-hidden bg-black">
<iframe
title="PolyWeather Legacy Dashboard"
src="/legacy/index.html?v=legacy-v16"
src="/legacy/index.html?v=legacy-v19"
className="h-full w-full border-0"
/>
</main>
+3 -3
View File
@@ -14,7 +14,7 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="/static/style.css?v=legacy-v16">
<link rel="stylesheet" href="/static/style.css?v=legacy-v19">
</head>
<body>
@@ -216,7 +216,7 @@
</section>
<section class="future-modal-section">
<h3>&#9201; &#x672A;&#x6765; 0-2 &#x5C0F;&#x65F6;&#x4E34;&#x8FD1;&#x5224;&#x65AD;</h3>
<h3 id="futureNowcastTitle">&#9201; &#x76EE;&#x6807;&#x65E5;&#x5348;&#x540E;&#x5CF0;&#x503C;&#x7A97;&#x53E3;</h3>
<div id="futureNowcast" class="future-text-block"></div>
</section>
</div>
@@ -231,7 +231,7 @@
<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script src="/static/app.js?v=legacy-v16"></script>
<script src="/static/app.js?v=legacy-v19"></script>
</body>
</html>
+121 -8
View File
@@ -1036,6 +1036,51 @@ function renderForecast(data) {
sunEl.innerHTML = parts.map((p) => `<span>${p}</span>`).join("");
}
function renderAI(data) {
const container = document.getElementById("aiAnalysis");
if (!container) return;
const analysis = data?.ai_analysis;
const placeholder =
'<span class="ai-placeholder">暂无 AI 分析,当前以结构化气象与模型数据为主。</span>';
if (!analysis) {
container.innerHTML = placeholder;
return;
}
if (typeof analysis === "string") {
const trimmed = analysis.trim();
container.innerHTML = trimmed
? escapeHtml(trimmed).replace(/\n/g, "<br>")
: placeholder;
return;
}
const summary = analysis.summary || analysis.text || analysis.message || "";
const bullets = Array.isArray(analysis.highlights)
? analysis.highlights
: Array.isArray(analysis.points)
? analysis.points
: [];
if (!summary && !bullets.length) {
container.innerHTML = placeholder;
return;
}
const bulletHtml = bullets.length
? `<ul class="ai-list">${bullets
.map((item) => `<li>${escapeHtml(String(item))}</li>`)
.join("")}</ul>`
: "";
container.innerHTML = `
${summary ? `<div class="ai-summary">${escapeHtml(String(summary)).replace(/\n/g, "<br>")}</div>` : ""}
${bulletHtml}
`;
}
function switchForecastDate(cityName, dateStr) {
if (selectedCity !== cityName) return;
@@ -1402,13 +1447,77 @@ function computeFrontTrendSignal(data, dateStr) {
};
}
function buildShortTermNowcast(data, dateStr) {
function buildShortTermNowcast(data, dateStr, slice) {
if (dateStr && dateStr !== data.local_date) {
const futureSlice = Array.isArray(slice) && slice.length
? slice
: getFutureSlice(data, dateStr);
const peakCandidates = futureSlice.filter((point) => {
const hour = Number.parseInt(String(point.label || "").split(":")[0], 10);
return Number.isFinite(hour) && hour >= 12 && hour <= 18;
});
const targetSlice = peakCandidates.length ? peakCandidates : futureSlice;
const maxIndex = targetSlice.reduce((bestIdx, point, idx, arr) => {
const temp = Number(point.temp);
const bestTemp = Number(arr[bestIdx]?.temp);
if (!Number.isFinite(temp)) return bestIdx;
if (!Number.isFinite(bestTemp) || temp > bestTemp) return idx;
return bestIdx;
}, 0);
const peakSlice = targetSlice.slice(
Math.max(0, maxIndex - 1),
Math.min(targetSlice.length, maxIndex + 2),
);
if (!peakSlice.length) {
return `
<div><strong>目标日期:</strong>${escapeHtml(dateStr)}</div>
<div><strong>峰值窗口:</strong>暂无足够的小时级 forecast 数据,无法生成目标日午后峰值窗口判断。</div>
`;
}
const sym = data.temp_symbol || "\u00B0C";
const start = peakSlice[0];
const end = peakSlice[peakSlice.length - 1];
const startTemp = Number(start.temp);
const endTemp = Number(end.temp);
const startDew = Number(start.dewPoint);
const endDew = Number(end.dewPoint);
const startPressure = Number(start.pressure);
const endPressure = Number(end.pressure);
const precipValues = peakSlice
.map((point) => Number(point.precipProb))
.filter((v) => Number.isFinite(v));
const cloudValues = peakSlice
.map((point) => Number(point.cloudCover))
.filter((v) => Number.isFinite(v));
const maxPrecip = precipValues.length ? Math.max(...precipValues) : 0;
const maxCloud = cloudValues.length ? Math.max(...cloudValues) : 0;
const firstBucket = trendBucketFromDir(start.windDir);
const lastBucket = trendBucketFromDir(end.windDir);
const windowDelta =
Number.isFinite(startTemp) && Number.isFinite(endTemp)
? endTemp - startTemp
: null;
const windowDewDelta =
Number.isFinite(startDew) && Number.isFinite(endDew)
? endDew - startDew
: null;
const windowPressureDelta =
Number.isFinite(startPressure) && Number.isFinite(endPressure)
? endPressure - startPressure
: null;
const peakPoint = targetSlice[maxIndex] || peakSlice[peakSlice.length - 1];
const peakTemp = Number(peakPoint?.temp);
return `
<div><strong>适用范围:</strong>近 0-2 小时临近判断只适用于当前日期,不适用于未来日期。</div>
<div><strong>目标日期:</strong>${escapeHtml(dateStr)}</div>
<div><strong>当前主站</strong>${data.current?.temp ?? "--"}${data.temp_symbol || "\u00B0C"} @ ${data.current?.obs_time || "--"}</div>
<div><strong>说明</strong>未来日期请主要参考上方温度走势、结算概率分布、多模型预报,以及未来 6-48 小时趋势。</div>
<div><strong>峰值窗口</strong>${escapeHtml(start.label)} - ${escapeHtml(end.label)}(优先取 12:00-18:00</div>
<div><strong>峰值预判</strong>${Number.isFinite(peakTemp) ? `${peakTemp.toFixed(1)}${sym}` : "--"} @ ${escapeHtml(peakPoint?.label || "--")}</div>
<div><strong>窗口温度:</strong>${Number.isFinite(startTemp) ? `${startTemp.toFixed(1)}${sym}` : "--"}${Number.isFinite(endTemp) ? `${endTemp.toFixed(1)}${sym}` : "--"}${formatDelta(windowDelta, sym)}</div>
<div><strong>露点变化:</strong>${formatDelta(windowDewDelta, sym)},用于判断午后暖湿输入是否增强。</div>
<div><strong>风向演变:</strong>${bucketLabel(firstBucket)}${bucketLabel(lastBucket)},关注峰值前后是否转南风或回摆北风。</div>
<div><strong>气压变化:</strong>${formatDelta(windowPressureDelta, " hPa")},上升更偏向冷空气压入。</div>
<div><strong>降水 / 云量:</strong>${Math.round(maxPrecip)}% / ${Math.round(maxCloud)}%,用于判断峰值时段是否受云系压制。</div>
`;
}
@@ -1472,9 +1581,6 @@ function buildShortTermNowcast(data, dateStr) {
);
}
lines.push(
"<div><strong>\u5de5\u7a0b\u53e3\u5f84\uff1a</strong>\u77ed\u65f6\u5224\u65ad\u4ee5 METAR \u4e3b\u7ad9\u4e3a\u51c6\uff1bAnkara \u53e0\u52a0 MGM \u5468\u8fb9\u7ad9\uff0c\u7f8e\u56fd\u57ce\u5e02\u540e\u7eed\u53ef\u53e0\u52a0 Mesonet \u4f5c\u4e3a\u589e\u5f3a\u5c42\u3002</div>",
);
return lines.join("");
}
@@ -1568,6 +1674,7 @@ function openFutureForecastModal(data, dateStr) {
const modelBars = document.getElementById("futureModelBars");
const trendGrid = document.getElementById("futureTrendGrid");
const nowcast = document.getElementById("futureNowcast");
const nowcastTitle = document.getElementById("futureNowcastTitle");
const sym = data.temp_symbol || "\u00B0C";
const forecastEntry = (data.forecast?.daily || []).find((item) => item.date === dateStr) || null;
@@ -1598,7 +1705,13 @@ function openFutureForecastModal(data, dateStr) {
probBars.innerHTML = buildProbabilityBarsHtml(probabilities);
modelBars.innerHTML = buildModelBarsHtml(models, deb, sym);
trendGrid.innerHTML = buildTrendCards(front.metrics);
nowcast.innerHTML = buildShortTermNowcast(data, dateStr);
if (nowcastTitle) {
nowcastTitle.innerHTML =
dateStr === data.local_date
? "&#9201; 未来 0-2 小时临近判断"
: "&#9201; 目标日午后峰值窗口";
}
nowcast.innerHTML = buildShortTermNowcast(data, dateStr, slice);
renderFutureForecastChart(data, dateStr, slice);
modal.classList.remove("hidden");