1157 字
6 分钟
Fuwari侧边栏天气组件实现
2026-05-06
Purely By AI

为 Fuwari 侧边栏添加天气小组件,自动定位后显示当前所在地的温度、天气、湿度、风速等信息,点击设置按钮可手动搜索城市。

一、技术选型#

1.1 天气 API:Open-Meteo#

Open-Meteo 是完全免费的天气 API,无需 API Key,无需注册。两个接口:

接口
https://geocoding-api.open-meteo.com/v1/search?name={城市名}&count=5&language=zh
https://api.open-meteo.com/v1/forecast?latitude={纬度}&longitude={经度}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,apparent_temperature

1.2 定位:浏览器 Geolocation API#

优先使用浏览器原生定位,失败时降级为手动搜城市。定位得到经纬度后,再通过 Open-Meteo Geocoding API 反查城市名。

1.3 Swup 兼容性#

Fuwari 使用 Swup 做页面切换。Swup 只替换两个容器的内容:#swup-container(主内容区)和 #toc(目录)。侧边栏在 Swup 容器之外,跨页面持久存在,不需要注册任何 Swup hooks。

这一点很重要:只要把天气组件放在侧边栏,它就不会被 Swup 替换,只需要初始化一次。

二、实现步骤#

2.1 新建 Weather.astro#

src/components/widget/ 下新建 Weather.astro,复用 Fuwari 已有的 WidgetLayout 组件(和 Tags、Categories 用法一致)。

组件结构分为三部分:

  • 模板:Astro 模板,只负责渲染 HTML 骨架,数据由客户端 JS 填充
  • SVG 图标:WMO 天气码到内联 SVG 的映射表,嵌入在 <script> 里,不依赖外部 CDN
  • 客户端脚本<script is:inline> 块,负责 API 调用、DOM 操作、缓存管理

模板部分#

---
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import WidgetLayout from "./WidgetLayout.astro";
---
<WidgetLayout name={i18n(I18nKey.weather)} id="weather">
<div id="weather-content">
<div id="weather-loading">Loading...</div>
<div id="weather-display" class="hidden">
<!-- 图标、温度、描述 -->
<!-- 湿度、风速、体感温度 -->
<!-- 城市名 + 设置按钮 -->
</div>
<div id="weather-settings" class="hidden">
<!-- 自动定位按钮 -->
<!-- 城市搜索输入框 + 下拉建议 -->
</div>
</div>
</WidgetLayout>

模板里没有任何动态数据——温度、图标、城市名全是占位符(--°C--),由客户端脚本填充。

IMPORTANT

必须用 <script is:inline>,不能用普通的 <script>。普通 <script> 会被 Astro 打包成 ES module 注入 <head>,但 Fuwari 配置了 updateHead: false,Swup 切页时不会重新执行。is:inline 让脚本直接写入 HTML,页面加载时同步执行——这样才能在首帧渲染前就把 localStorage 缓存的天气数据显示出来,避免页面加载后”闪一下”。

SVG 天气图标#

Open-Meteo 返回的 weather_code 是 WMO 标准天气码(0 = 晴,2 = 多云,61 = 小雨,95 = 雷暴……)。需要一张映射表把天气码转成图标。

不用 Iconify CDN,也不装额外依赖——直接把 Material Symbols 的 SVG path 写在 JS 对象里:

var WMO_ICONS = {
0: '<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10"><path d="M12 7c-2.76 0-5..."/></svg>',
2: '<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10"><path d="M19.35 10.04A7.49..."/></svg>',
// ... 其余 WMO 码
};

图标的 SVG 数据从 @iconify-json/material-symbols 包里提取,也可以直接去 Iconify 网站复制。天气描述同理,用一个 WMO_DESC 对象映射成中文。

客户端脚本核心逻辑#

整个脚本包在一个 IIFE 里,用 var 避免污染全局(is:inline 脚本不做模块化处理)。

数据流:

页面加载
检查 localStorage 缓存(30 分钟有效期)
↓ 有缓存 → 立即渲染,结束
↓ 无缓存
尝试 Geolocation API
↓ 成功 → 拿到经纬度 → 反查城市名 → 请求天气 → 渲染 + 存缓存
↓ 失败 → 显示城市搜索面板,等用户手动输入

localStorage 缓存结构:

{
"timestamp": 1714980000000,
"temperature": 23,
"humidity": 65,
"weather_code": 2,
"wind": 12,
"feels_like": 21,
"city": "上海",
"lat": 31.23,
"lon": 121.47
}

反向地理编码:

Geolocation API 只返回经纬度,不返回城市名。用 Open-Meteo Geocoding API 做反查:

async function reverseGeocode(lat, lon) {
var url = GEOCODING_API + "?name=" + lat.toFixed(1) + "," + lon.toFixed(1) + "&count=1&language=zh";
var resp = await fetch(url);
var json = await resp.json();
if (json.results && json.results.length > 0) return json.results[0].name;
return lat.toFixed(2) + ", " + lon.toFixed(2);
}

城市搜索(带防抖):

输入框监听 input 事件,300ms 防抖后调用 Geocoding API,结果渲染为下拉列表:

cityInput.addEventListener("input", function() {
clearTimeout(debounceTimer);
var query = cityInput.value.trim();
if (query.length < 2) { suggestions.classList.add("hidden"); return; }
debounceTimer = setTimeout(async function() {
var results = await searchCity(query);
// 渲染下拉建议,每项绑定 click → fetchWeather(r.latitude, r.longitude, r.name)
}, 300);
});

点击建议项后清空输入框、隐藏下拉、请求天气数据。

2.2 注册到 SideBar.astro#

编辑 src/components/widget/SideBar.astro,在 Tags 组件后面加上 Weather:

---
// ... 已有 import
import Weather from "./Weather.astro";
---
<!-- 在 #sidebar-sticky 容器内,Tags 后面 -->
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
<Weather class="onload-animation" style="animation-delay: 250ms"></Weather>

animation-delay: 250ms 保证入场动画在 Tags(200ms)之后,形成依次出现的视觉效果。

2.3 添加 i18n 翻译#

src/i18n/i18nKey.ts 枚举里加一个键:

weather = "weather",

然后在每个语言文件里添加对应翻译:

文件翻译
zh_CN.ts"天气"
en.ts"Weather"
ja.ts"天気"
ko.ts"날씨"
es.ts"Clima"
其余语言文件对应翻译

i18n 函数在 Astro 服务端渲染时调用,读的是 siteConfig.lang,不需要客户端参与。

这篇文章对你有帮助吗?