feat: Implement PolyWeather extension side panel with weather data display and interaction logic.

This commit is contained in:
2569718930@qq.com
2026-03-21 19:15:21 +08:00
parent 2eb46853bf
commit 3c950956e3
3 changed files with 76 additions and 2 deletions
+17
View File
@@ -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;
+2
View File
@@ -26,6 +26,8 @@
</button>
</header>
<div id="freshnessHint" class="freshness-hint hidden"></div>
<section class="section">
<h3 id="profileTitle">城市档案</h3>
<div class="grid2">
+57 -2
View File
@@ -39,7 +39,10 @@ const I18N = {
refreshFailed: "刷新温度数据失败",
initFailed: "初始化失败",
publicReadHint: "当前插件是公开读模式,Token 可留空。请检查后端是否仍开启了接口鉴权。",
publicModeHint: "公开模式只需配置 API BaseToken 可留空。"
publicModeHint: "公开模式只需配置 API BaseToken 可留空。",
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;