1281 字
6 分钟

博客侧边栏美化:集成一个智能天气卡片

通过一个 Svelte 组件,为您的博客侧边栏添加一个美观且功能强大的天气卡片。它能自动根据访客IP定位,并提供精确定位选项,还能根据天气情况显示不同图标。
2025-08-18
Views: ...
  • 前言 📝
  1. 一个个性化的博客,除了内容优质,一些有趣的小部件也能极大地提升访客体验。
  2. 天气卡片就是一个常见但非常实用的小功能,它能让访客在浏览您的博客时,顺便了解当地的天气情况,增加页面的互动性和趣味性。
  3. 本教程将展示如何为博客添加一个智能天气卡片。它不仅会自动根据访客的 IP 地址显示天气,还提供了一个“精确定位”按钮,让用户可以获取更准确的天气信息。我们还会用漂亮的图标和渐变色来美化它。
  • 方案概述 🗺️ 我们的天气卡片是一个独立的 Svelte 组件,它负责处理所有与天气相关的功能:
  1. IP 定位 📍:组件加载时,会首先通过 ipapi.co 服务获取访客的大致位置(城市)。
  2. 天气查询 🌦️:根据获取到的经纬度,调用 Open-Meteo 的免费 API 来获取实时天气数据。
  3. 精确定位(可选) 🛰️:用户可以点击一个按钮,授权浏览器使用更高精度的地理位置(如 GPS),并通过 Nominatim 服务将经纬度反向解析成城市名称,从而获得更准确的天气。
  4. UI 展示 ✨:使用 Svelte 来动态展示天气信息,包括温度、风速,并根据天气代码显示不同的 Emoji 图标,同时卡片拥有现代化的渐变背景和布局。
  • 实现步骤 ⚙️

整个实现过程都封装在一个 Svelte 组件中,非常便于集成。

  1. 创建 Svelte 组件 我们在 src/components/widget/ 目录下创建了 Weather.svelte 文件。这是所有逻辑的核心。

  2. 编写组件代码 组件的 <script> 部分包含了所有功能逻辑:

    • onMount:组件挂载后,立即开始 IP 定位和天气查询流程。
    • fetchWeather:一个专门的函数,负责根据经纬度调用天气 API。
    • getCityFromCoords:当使用精确定位时,这个函数负责将经纬度转换成城市名。
    • usePreciseLocation:处理“精确定位”按钮的点击事件,调用浏览器 Geolocation API。
    • getWeatherIcon:一个辅助函数,根据 Open-Meteo 返回的天气代码(weathercode)匹配一个合适的 Emoji 图标,让界面更生动。
  3. 集成到侧边栏 最后,我们只需要在侧边栏组件 src/components/widget/SideBar.astro 中引入并使用这个新的天气组件即可。

    ---
    import Weather from "./Weather.svelte";
    ---
    <Weather client:load />

    client:load 指令告诉 Astro,这个组件需要在客户端加载和渲染,因为它的功能依赖于浏览器环境(如 fetchnavigator.geolocation)。

  • 结束 🎉 通过这个小小的天气卡片,我们的博客侧边栏变得更加生动和实用。这个功能不仅展示了 Svelte 在构建交互式组件方面的强大能力,也体现了通过组合多个第三方 API 来实现复杂功能的灵活性。 希望这个小教程能给你的博客带来一点新的色彩。如果你有任何问题或改进建议,欢迎随时提出!

附录:最终配置文件#

天气卡片组件: src/components/widget/Weather.svelte

<script lang="ts">
import { onMount } from 'svelte';
let weather: any = null;
let city: string = '';
let error: string = '';
let loading: boolean = true;
// Weather code to SVG icon mapping
function getWeatherIcon(code: number): string {
if (code === 0) return '☀️'; // Clear sky
if (code >= 1 && code <= 3) return '☁️'; // Mainly clear, partly cloudy, overcast
if (code >= 45 && code <= 48) return '🌫️'; // Fog
if (code >= 51 && code <= 65) return '🌧️'; // Drizzle, Rain
if (code >= 80 && code <= 82) return '🌦️'; // Rain showers
if (code >= 95 && code <= 99) return '⛈️'; // Thunderstorm
return '-';
}
async function fetchWeather(latitude: number, longitude: number) {
const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true`);
if (!weatherResponse.ok) {
throw new Error('获取天气信息失败');
}
const weatherData = await weatherResponse.json();
weather = weatherData.current_weather;
}
async function getCityFromCoords(latitude: number, longitude: number) {
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=zh-CN`);
if (!response.ok) return '当前位置';
const data = await response.json();
return data.address.city || data.address.town || data.address.village || '当前位置';
} catch (e) {
console.error("Reverse geocoding failed", e);
return '当前位置';
}
}
async function usePreciseLocation() {
loading = true;
error = '';
if (!navigator.geolocation) {
error = '您的浏览器不支持精确定位。';
loading = false;
return;
}
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
city = await getCityFromCoords(latitude, longitude);
await fetchWeather(latitude, longitude);
loading = false;
},
(err) => {
error = err.code === err.PERMISSION_DENIED ? '您已拒绝位置权限,无法使用精确定位。' : '无法获取精确定位。';
loading = false;
}
);
}
onMount(async () => {
try {
const ipResponse = await fetch('https://ipapi.co/json/');
if (!ipResponse.ok) throw new Error('获取IP信息失败');
const ipData = await ipResponse.json();
city = ipData.city || '未知位置';
const { latitude, longitude } = ipData;
if (!latitude || !longitude) throw new Error('无法通过IP确定位置');
await fetchWeather(latitude, longitude);
} catch (e: any) {
error = e.message;
console.error(e);
} finally {
loading = false;
}
});
</script>
<div class="weather-widget p-4 rounded-lg shadow-md bg-gradient-to-br from-blue-200 to-cyan-200 dark:from-gray-700 dark:to-gray-800 text-gray-800 dark:text-gray-200">
<div class="flex justify-between items-center mb-3">
<h2 class="text-lg font-bold">{city} 天气</h2>
<button on:click={usePreciseLocation} class="text-sm text-blue-600 dark:text-blue-400 hover:underline focus:outline-none">
精确定位
</button>
</div>
{#if loading}
<div class="flex items-center justify-center h-24">
<p>正在加载天气...</p>
</div>
{:else if weather && !error}
<div class="flex items-center justify-around text-center">
<div class="text-5xl">
{getWeatherIcon(weather.weathercode)}
</div>
<div class="pl-4">
<div class="text-2xl font-bold">{weather.temperature}°C</div>
<div class="text-sm mt-1">
<span title="风速">🌬️</span> {weather.windspeed} km/h
</div>
</div>
</div>
{:else if error}
<div class="flex items-center justify-center h-24">
<p class="text-red-500">{error}</p>
</div>
{/if}
</div>
博客侧边栏美化:集成一个智能天气卡片
https://www.497995.xyz/posts/add-weather-widget/
作者
或许是一只龙
发布于
2025-08-18
许可协议
CC BY-NC-SA 4.0