From 3c950956e31f9fe1eb0a245c305a1c0d9157fd29 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sat, 21 Mar 2026 19:15:21 +0800 Subject: [PATCH] feat: Implement PolyWeather extension side panel with weather data display and interaction logic. --- extension/sidepanel.css | 17 ++++++++++++ extension/sidepanel.html | 2 ++ extension/sidepanel.js | 59 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/extension/sidepanel.css b/extension/sidepanel.css index 047d0074..8468aa25 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -66,6 +66,23 @@ body { margin-bottom: 12px; } +.freshness-hint { + margin: -2px 0 12px; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(245, 158, 11, 0.28); + background: rgba(245, 158, 11, 0.08); + color: #fcd34d; + font-size: 12px; + line-height: 1.4; +} + +.freshness-hint.stale { + border-color: rgba(248, 113, 113, 0.38); + background: rgba(248, 113, 113, 0.1); + color: #fecaca; +} + .risk-badge { padding: 5px 9px; border-radius: 10px; diff --git a/extension/sidepanel.html b/extension/sidepanel.html index bedaca2a..29c0b18f 100644 --- a/extension/sidepanel.html +++ b/extension/sidepanel.html @@ -26,6 +26,8 @@ + +

城市档案

diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 2c1f2f51..8ff27267 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -39,7 +39,10 @@ const I18N = { refreshFailed: "刷新温度数据失败", initFailed: "初始化失败", publicReadHint: "当前插件是公开读模式,Token 可留空。请检查后端是否仍开启了接口鉴权。", - publicModeHint: "公开模式只需配置 API Base;Token 可留空。" + publicModeHint: "公开模式只需配置 API Base;Token 可留空。", + freshnessRecent: "数据约 {minutes} 分钟前更新。", + freshnessWarn: "数据已 {minutes} 分钟未更新,建议点右上角刷新。", + freshnessStale: "数据已 {minutes} 分钟未更新,当前结果可能偏旧,请立即刷新。" }, en: { loadingWeather: "Loading weather data...", @@ -71,7 +74,10 @@ const I18N = { refreshFailed: "Failed to refresh weather data", initFailed: "Initialization failed", publicReadHint: "The extension is in public read mode. Token can be empty. Check whether the backend still requires auth.", - publicModeHint: "In public mode only API Base is required; Token can be empty." + publicModeHint: "In public mode only API Base is required; Token can be empty.", + freshnessRecent: "Data updated about {minutes} min ago.", + freshnessWarn: "Data is {minutes} min old. Consider refreshing.", + freshnessStale: "Data is {minutes} min old and may be stale. Refresh now." } }; @@ -116,6 +122,8 @@ const els = { nearbyLabel: document.getElementById("nearbyLabel"), trendTitle: document.getElementById("trendTitle"), forecastTitle: document.getElementById("forecastTitle") + , + freshnessHint: document.getElementById("freshnessHint") }; function normalizeBase(url) { @@ -258,6 +266,14 @@ function showError(message) { els.errorBox.classList.remove("hidden"); } +function tf(key, params = {}) { + let text = t(key); + for (const [name, value] of Object.entries(params)) { + text = text.replace(`{${name}}`, String(value)); + } + return text; +} + function clearError() { els.errorBox.textContent = ""; els.errorBox.classList.add("hidden"); @@ -400,6 +416,44 @@ function formatForecastDate(day, index) { return `${m[2]}/${m[3]}`; } +function parseIsoDate(value) { + const text = String(value || "").trim(); + if (!text) return null; + const date = new Date(text); + if (Number.isNaN(date.getTime())) return null; + return date; +} + +function renderFreshness(detail) { + if (!els.freshnessHint) return; + const updatedAt = parseIsoDate(detail?.updated_at); + if (!updatedAt) { + els.freshnessHint.classList.add("hidden"); + els.freshnessHint.classList.remove("stale"); + els.freshnessHint.textContent = ""; + return; + } + + const minutes = Math.max( + 0, + Math.round((Date.now() - updatedAt.getTime()) / 60000) + ); + + if (minutes < 8) { + els.freshnessHint.classList.add("hidden"); + els.freshnessHint.classList.remove("stale"); + els.freshnessHint.textContent = ""; + return; + } + + const isStale = minutes >= 20; + els.freshnessHint.classList.remove("hidden"); + els.freshnessHint.classList.toggle("stale", isStale); + els.freshnessHint.textContent = isStale + ? tf("freshnessStale", { minutes }) + : tf("freshnessWarn", { minutes }); +} + function parseTimeToMinute(value) { const text = String(value || ""); const m = text.match(/(\d{1,2}):(\d{2})/); @@ -717,6 +771,7 @@ function renderForecast(detail) { function renderDetail(detail) { state.detail = detail; renderRiskBadge(detail); + renderFreshness(detail); const profile = getSettlementSourceDisplay(detail); els.settlementLabel.textContent = profile.label;