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;