想在側邊欄加上天氣
為此我們使用的是Free Weather API
可以自己選擇地點
新增一個**layout/_widget/weather.ejs
**
<div layout/_widget/weather.ejs
class="kira-widget-wrap card-weather" data-lat="25.0375" data-lon="121.5637" data-city="台北">
<h3 class="kira-widget-title">
<i class="kirafont icon-weather"></i>
台北 · 今日天氣
</h3>
<div class="weather-body">
<div class="w-top">
<div class="w-left">
<div class="w-temp"><span id="w-now">--</span><span class="unit">°C</span></div>
<div class="w-hilo">H <span id="w-max">--</span>° · L <span id="w-min">--</span>°</div>
</div>
<div class="w-right">
<div id="w-icon" class="w-icon">⛅</div>
<div id="w-desc" class="w-desc">--</div>
<div id="w-time" class="w-time">--:--</div>
</div>
</div>
<!-- 兩條長膠囊:降雨、濕度 -->
<div class="w-grid chips-wide">
<div class="chip">
<span class="label">降雨</span>
<span class="value"><span id="w-pop">--</span><span class="unit">%</span></span>
</div>
<div class="chip">
<span class="label">濕度</span>
<span class="value"><span id="w-rh">--</span><span class="unit">%</span></span>
</div>
</div>
</div>
</div>
然後是美化的部分source/weather-card.css
.kira-right-column .kira-widget-wrap.card-weather{ width:100%; }
/* 玻璃感卡片容器 */
.card-weather .weather-body{
border-radius:16px; background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04));
border:1px solid var(--card-border, rgba(255,255,255,.18)); box-shadow:0 10px 22px rgba(0,0,0,.06);
padding:14px 16px;
}
/* 頂部:左大字溫度、右側圖示/描述/時間 */
.card-weather .w-top{ display:flex; align-items:center; justify-content:space-between; gap:12px; }
.card-weather .w-left{ display:flex; flex-direction:column; gap:4px; }
.card-weather .w-temp{ font-weight:900; line-height:1; letter-spacing:.5px; }
.card-weather .w-temp #w-now{ font-size:36px; }
.card-weather .w-temp .unit{ font-size:16px; opacity:.85; margin-left:2px; }
.card-weather .w-hilo{ opacity:.9 }
.card-weather .w-right{ display:flex; flex-direction:column; align-items:flex-end; gap:2px; text-align:right; }
.card-weather .w-icon{ font-size:28px; line-height:1; }
.card-weather .w-desc{ font-size:.95rem; opacity:.95; }
.card-weather .w-time{ font-size:.85rem; opacity:.7 }
/* 兩條長膠囊(單欄) */
.card-weather .w-grid.chips-wide{ display:grid; grid-template-columns: 1fr; row-gap:8px; margin-top:12px; }
.card-weather .chip{ display:flex; align-items:center; justify-content:space-between; gap:12px;
border-radius:12px; padding:10px 12px; background: rgba(255,255,255,.10);
border:1px solid var(--card-border, rgba(255,255,255,.18)); font-size:.95rem; line-height:1; }
.card-weather .chip .label{ font-weight:600; letter-spacing:.3px; white-space:nowrap; opacity:.85; }
.card-weather .chip .value{ font-weight:800; font-variant-numeric: tabular-nums; white-space:nowrap; display:inline-flex; align-items:baseline; gap:4px; }
@media (prefers-color-scheme: light){
.card-weather .weather-body{ background:#fff; border-color:rgba(0,0,0,.08); box-shadow:0 10px 22px rgba(0,0,0,.05); }
.card-weather .chip{ background:#f7f9fb; border-color:rgba(0,0,0,.06); }
}
最後是腳本source/js/weather-card.js
(function () {
const box = document.querySelector('.kira-widget-wrap.card-weather');
if (!box) return;
const lat = parseFloat(box.dataset.lat || '25.0375');
const lon = parseFloat(box.dataset.lon || '121.5637');
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}`
+ `¤t=temperature_2m,precipitation,relative_humidity_2m,weather_code,wind_speed_10m`
+ `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset`
+ `&timezone=Asia%2FTaipei`;
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
const codeMap = (code, isNight) => {
const sun='☀️', moon='🌙', cloud='☁️', pcloud='⛅', rain='🌧️', lrain='🌦️', tstorm='⛈️', snow='❄️', fog='🌫️';
const txt = {0:'晴朗',1:'多雲時晴',2:'多雲',3:'陰',45:'霧',48:'霧凇',51:'毛毛雨',53:'小雨',55:'中雨',56:'凍雨(毛毛雨)',57:'凍雨',61:'小雨',63:'中雨',65:'大雨',66:'凍雨',67:'強凍雨',71:'小雪',73:'中雪',75:'大雪',77:'霰',80:'陣雨',81:'強陣雨',82:'暴陣雨',85:'陣雪',86:'強陣雪',95:'雷雨',96:'雷雨雹',99:'劇烈雷雨雹'};
const emoji = code===0 ? (isNight?moon:sun) : [1,2].includes(code)?pcloud : code===3?cloud :
[51,53,55,61,63,65,80,81,82].includes(code) ? (code===51?lrain:rain) :
[95,96,99].includes(code) ? tstorm : [71,73,75,77,85,86].includes(code)?snow : [45,48].includes(code)?fog : cloud;
return { emoji, text: txt[code] || '—' };
};
fetch(url).then(r=>r.json()).then(d=>{
const nowISO = d?.current?.time || new Date().toISOString();
const now = new Date(nowISO);
const sunrise = new Date(d?.daily?.sunrise?.[0] || now);
const sunset = new Date(d?.daily?.sunset?.[0] || now);
const isNight = now < sunrise || now > sunset;
const nowT = Math.round(d?.current?.temperature_2m ?? NaN);
const rh = d?.current?.relative_humidity_2m ?? null;
const wcode= d?.current?.weather_code ?? 3;
const maxT = Math.round(d?.daily?.temperature_2m_max?.[0] ?? NaN);
const minT = Math.round(d?.daily?.temperature_2m_min?.[0] ?? NaN);
const pop = d?.daily?.precipitation_probability_max?.[0];
if (Number.isFinite(nowT)) set('w-now', nowT);
if (Number.isFinite(maxT)) set('w-max', maxT);
if (Number.isFinite(minT)) set('w-min', minT);
if (typeof pop === 'number') set('w-pop', pop);
if (typeof rh === 'number') set('w-rh', rh);
const meta = codeMap(wcode, isNight);
set('w-icon', meta.emoji); set('w-desc', meta.text);
set('w-time', now.toLocaleTimeString('zh-TW',{hour:'2-digit',minute:'2-digit'}));
}).catch(()=>{
box.querySelector('.weather-body').innerHTML = '<div style="opacity:.85">天氣資料載入失敗,稍後再試</div>';
});
})();
同樣記得載入到header