1281 字
6 分钟
博客侧边栏美化:集成一个智能天气卡片
通过一个 Svelte 组件,为您的博客侧边栏添加一个美观且功能强大的天气卡片。它能自动根据访客IP定位,并提供精确定位选项,还能根据天气情况显示不同图标。

- 前言 📝
- 一个个性化的博客,除了内容优质,一些有趣的小部件也能极大地提升访客体验。
- 天气卡片就是一个常见但非常实用的小功能,它能让访客在浏览您的博客时,顺便了解当地的天气情况,增加页面的互动性和趣味性。
- 本教程将展示如何为博客添加一个智能天气卡片。它不仅会自动根据访客的 IP 地址显示天气,还提供了一个“精确定位”按钮,让用户可以获取更准确的天气信息。我们还会用漂亮的图标和渐变色来美化它。
- 方案概述 🗺️ 我们的天气卡片是一个独立的 Svelte 组件,它负责处理所有与天气相关的功能:
- IP 定位 📍:组件加载时,会首先通过
ipapi.co
服务获取访客的大致位置(城市)。 - 天气查询 🌦️:根据获取到的经纬度,调用
Open-Meteo
的免费 API 来获取实时天气数据。 - 精确定位(可选) 🛰️:用户可以点击一个按钮,授权浏览器使用更高精度的地理位置(如 GPS),并通过
Nominatim
服务将经纬度反向解析成城市名称,从而获得更准确的天气。 - UI 展示 ✨:使用 Svelte 来动态展示天气信息,包括温度、风速,并根据天气代码显示不同的 Emoji 图标,同时卡片拥有现代化的渐变背景和布局。
- 实现步骤 ⚙️
整个实现过程都封装在一个 Svelte 组件中,非常便于集成。
-
创建 Svelte 组件 我们在
src/components/widget/
目录下创建了Weather.svelte
文件。这是所有逻辑的核心。 -
编写组件代码 组件的
<script>
部分包含了所有功能逻辑:onMount
:组件挂载后,立即开始 IP 定位和天气查询流程。fetchWeather
:一个专门的函数,负责根据经纬度调用天气 API。getCityFromCoords
:当使用精确定位时,这个函数负责将经纬度转换成城市名。usePreciseLocation
:处理“精确定位”按钮的点击事件,调用浏览器 Geolocation API。getWeatherIcon
:一个辅助函数,根据 Open-Meteo 返回的天气代码(weathercode)匹配一个合适的 Emoji 图标,让界面更生动。
-
集成到侧边栏 最后,我们只需要在侧边栏组件
src/components/widget/SideBar.astro
中引入并使用这个新的天气组件即可。---import Weather from "./Weather.svelte";---<Weather client:load />client:load
指令告诉 Astro,这个组件需要在客户端加载和渲染,因为它的功能依赖于浏览器环境(如fetch
和navigator.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}¤t_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/