feat: Implement PolyWeather extension side panel with weather data display and interaction logic.
This commit is contained in:
@@ -66,6 +66,23 @@ body {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.risk-badge {
|
||||||
padding: 5px 9px;
|
padding: 5px 9px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div id="freshnessHint" class="freshness-hint hidden"></div>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h3 id="profileTitle">城市档案</h3>
|
<h3 id="profileTitle">城市档案</h3>
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
|
|||||||
+57
-2
@@ -39,7 +39,10 @@ const I18N = {
|
|||||||
refreshFailed: "刷新温度数据失败",
|
refreshFailed: "刷新温度数据失败",
|
||||||
initFailed: "初始化失败",
|
initFailed: "初始化失败",
|
||||||
publicReadHint: "当前插件是公开读模式,Token 可留空。请检查后端是否仍开启了接口鉴权。",
|
publicReadHint: "当前插件是公开读模式,Token 可留空。请检查后端是否仍开启了接口鉴权。",
|
||||||
publicModeHint: "公开模式只需配置 API Base;Token 可留空。"
|
publicModeHint: "公开模式只需配置 API Base;Token 可留空。",
|
||||||
|
freshnessRecent: "数据约 {minutes} 分钟前更新。",
|
||||||
|
freshnessWarn: "数据已 {minutes} 分钟未更新,建议点右上角刷新。",
|
||||||
|
freshnessStale: "数据已 {minutes} 分钟未更新,当前结果可能偏旧,请立即刷新。"
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
loadingWeather: "Loading weather data...",
|
loadingWeather: "Loading weather data...",
|
||||||
@@ -71,7 +74,10 @@ const I18N = {
|
|||||||
refreshFailed: "Failed to refresh weather data",
|
refreshFailed: "Failed to refresh weather data",
|
||||||
initFailed: "Initialization failed",
|
initFailed: "Initialization failed",
|
||||||
publicReadHint: "The extension is in public read mode. Token can be empty. Check whether the backend still requires auth.",
|
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"),
|
nearbyLabel: document.getElementById("nearbyLabel"),
|
||||||
trendTitle: document.getElementById("trendTitle"),
|
trendTitle: document.getElementById("trendTitle"),
|
||||||
forecastTitle: document.getElementById("forecastTitle")
|
forecastTitle: document.getElementById("forecastTitle")
|
||||||
|
,
|
||||||
|
freshnessHint: document.getElementById("freshnessHint")
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeBase(url) {
|
function normalizeBase(url) {
|
||||||
@@ -258,6 +266,14 @@ function showError(message) {
|
|||||||
els.errorBox.classList.remove("hidden");
|
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() {
|
function clearError() {
|
||||||
els.errorBox.textContent = "";
|
els.errorBox.textContent = "";
|
||||||
els.errorBox.classList.add("hidden");
|
els.errorBox.classList.add("hidden");
|
||||||
@@ -400,6 +416,44 @@ function formatForecastDate(day, index) {
|
|||||||
return `${m[2]}/${m[3]}`;
|
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) {
|
function parseTimeToMinute(value) {
|
||||||
const text = String(value || "");
|
const text = String(value || "");
|
||||||
const m = text.match(/(\d{1,2}):(\d{2})/);
|
const m = text.match(/(\d{1,2}):(\d{2})/);
|
||||||
@@ -717,6 +771,7 @@ function renderForecast(detail) {
|
|||||||
function renderDetail(detail) {
|
function renderDetail(detail) {
|
||||||
state.detail = detail;
|
state.detail = detail;
|
||||||
renderRiskBadge(detail);
|
renderRiskBadge(detail);
|
||||||
|
renderFreshness(detail);
|
||||||
|
|
||||||
const profile = getSettlementSourceDisplay(detail);
|
const profile = getSettlementSourceDisplay(detail);
|
||||||
els.settlementLabel.textContent = profile.label;
|
els.settlementLabel.textContent = profile.label;
|
||||||
|
|||||||
Reference in New Issue
Block a user