教程
之前的心知天气版本 已失效 ,本版本由作者开发,灵感来源于Shanshui’s Blog,实现来自于Kimi K2.6大模型
使用OpenMeteo天气API,支持自动定位(浏览器权限),反向查询来自 BigDataCloud 免费 API
本版本无需任何公钥与私钥
将以下代码放入/usr/themes/handsome/component/sidebar.php第 76 行左右
<?php include DIR . '/weather-widget.php'; ?>
随后在 /usr/themes/handsome/component/ 下,创建一个新文件
文件名称:weather-widget.php
文件中放入以下代码
<?php
/**
* 天气小组件 v4 - 适配 Typecho Handsome 主题原生样式
* 去除城市切换下拉框,纯自动定位/默认城市
* 使用 Open-Meteo 免费 API + BigDataCloud 反向地理编码
* 作者:センク森辞 以及 Kimi
* 灵感来源于 blog.shanshui.site
*
* 安装方法:
* 1. 将本文件保存到 /usr/themes/handsome/component/weather-widget.php
* 2. 在 /usr/themes/handsome/component/sidebar.php 中合适位置添加:
* <?php include __DIR__ . '/weather-widget.php'; ?>
* 3. 修改下方 $default_city 为你想默认显示的城市
*/
// ==================== 配置区域 ====================
$default_city = '北京'; // 默认城市(当定位失败时显示)
$cache_minutes = 10; // 缓存时间(分钟)
// ==================================================
?>
<section id="shanshui-weather" class="widget widget_categories wrapper-md clear">
<h5 class="widget-title m-t-none text-md"><?php _me("天气") ?></h5>
<div class="panel wrapper-sm">
<!-- 加载骨架屏 -->
<div id="sw-loading" class="text-center" style="padding:16px 0;">
<div class="m-b-sm">
<span class="inline" style="display:inline-block;width:20px;height:20px;background:rgba(128,128,128,0.15);border-radius:2px;"></span>
</div>
<div class="m-b-sm">
<span class="inline" style="display:inline-block;width:64px;height:14px;background:rgba(128,128,128,0.15);border-radius:2px;"></span>
</div>
<div>
<span class="inline" style="display:inline-block;width:96px;height:10px;background:rgba(128,128,128,0.15);border-radius:2px;"></span>
</div>
</div>
<!-- 天气数据 -->
<div id="sw-data" class="text-center" style="padding:12px 0;display:none;">
<div id="sw-icon" style="font-size:28px;line-height:1;margin-bottom:6px;"></div>
<div style="font-size:20px;font-weight:700;margin-bottom:4px;">
<span id="sw-temp"></span>°C
</div>
<div class="text-muted" style="font-size:12px;margin-bottom:4px;" id="sw-weather"></div>
<div class="text-muted" style="font-size:11px;" id="sw-detail"></div>
</div>
<!-- 错误提示 -->
<div id="sw-error" class="text-center text-muted" style="padding:16px 0;font-size:11px;display:none;"></div>
</div>
<style>
#shanshui-weather #sw-loading span {
animation: sw-pulse 1.5s ease-in-out infinite;
}
@keyframes sw-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
<script>
(function() {
const CACHE_KEY = 'shanshui-weather-cache-v1';
const CACHE_TIME = <?php echo $cache_minutes; ?> * 60 * 1000;
const DEFAULT_CITY = '<?php echo $default_city; ?>';
const $ = id => document.getElementById(id);
const show = id => $(id).style.display = '';
const hide = id => $(id).style.display = 'none';
function getCache(key) {
try {
const data = JSON.parse(sessionStorage.getItem(CACHE_KEY) || '{}');
const item = data[key];
if (item && Date.now() - item.ts < CACHE_TIME) return item.data;
} catch(e) {}
return null;
}
function setCache(key, data) {
try {
const all = JSON.parse(sessionStorage.getItem(CACHE_KEY) || '{}');
all[key] = { data, ts: Date.now() };
sessionStorage.setItem(CACHE_KEY, JSON.stringify(all));
} catch(e) {}
}
async function getCityCoords(city) {
const cache = getCache('coords_' + city);
if (cache) return cache;
const res = await fetch(
'https://geocoding-api.open-meteo.com/v1/search?name=' +
encodeURIComponent(city) + '&count=1&language=zh&format=json'
);
const data = await res.json();
if (data.results && data.results[0]) {
const r = data.results[0];
const coords = { lat: r.latitude, lon: r.longitude, name: r.name };
setCache('coords_' + city, coords);
return coords;
}
throw new Error('未找到城市:' + city);
}
async function getWeather(lat, lon) {
const cacheKey = 'weather_' + lat + '_' + lon;
const cache = getCache(cacheKey);
if (cache) return cache;
const res = await fetch(
'https://api.open-meteo.com/v1/forecast?latitude=' + lat +
'&longitude=' + lon +
'¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m&timezone=auto'
);
const data = await res.json();
setCache(cacheKey, data);
return data;
}
async function reverseGeocode(lat, lon) {
const cacheKey = 'geo_' + lat + '_' + lon;
const cache = getCache(cacheKey);
if (cache) return cache;
try {
const res = await fetch(
'https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=' + lat +
'&longitude=' + lon + '&localityLanguage=zh'
);
const data = await res.json();
const city = data.city || data.locality || data.principalSubdivision || '';
if (city) {
setCache(cacheKey, city);
return city;
}
} catch(e) {}
return '';
}
function getWindDir(deg) {
const dirs = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
return dirs[Math.round(deg / 45) % 8];
}
function getWeatherInfo(code) {
const map = {
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: ['雷暴伴冰雹', '⛈️']
};
return map[code] || ['未知', '🌤️'];
}
function getWindLevel(speed) {
if (speed < 0.3) return 0;
if (speed < 1.6) return 1;
if (speed < 3.4) return 2;
if (speed < 5.5) return 3;
if (speed < 8.0) return 4;
if (speed < 10.8) return 5;
if (speed < 13.9) return 6;
if (speed < 17.2) return 7;
if (speed < 20.8) return 8;
if (speed < 24.5) return 9;
if (speed < 28.5) return 10;
if (speed < 32.7) return 11;
return 12;
}
function render(data, cityName) {
const current = data.current;
const info = getWeatherInfo(current.weather_code);
$('sw-icon').textContent = info[1];
$('sw-temp').textContent = Math.round(current.temperature_2m);
$('sw-weather').textContent = info[0];
$('sw-detail').textContent = cityName + ' · ' +
getWindDir(current.wind_direction_10m) + '风 ' +
getWindLevel(current.wind_speed_10m) + '级 · 湿度' +
current.relative_humidity_2m + '%';
hide('sw-loading');
hide('sw-error');
show('sw-data');
}
async function showError(msg) {
console.log('[天气组件]', msg);
try {
await loadCity(DEFAULT_CITY);
} catch(e) {
$('sw-error').textContent = msg;
hide('sw-loading');
hide('sw-data');
show('sw-error');
}
}
async function loadCity(city) {
show('sw-loading');
hide('sw-data');
hide('sw-error');
const coords = await getCityCoords(city);
const weather = await getWeather(coords.lat, coords.lon);
render(weather, coords.name || city);
}
async function init() {
try {
const isHttps = window.location.protocol === 'https:';
if (!isHttps) {
console.log('[天气组件] HTTP 站点,使用默认城市');
await loadCity(DEFAULT_CITY);
return;
}
if (navigator.geolocation) {
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
timeout: 10000, enableHighAccuracy: false
});
});
const lat = position.coords.latitude.toFixed(4);
const lon = position.coords.longitude.toFixed(4);
const [weather, cityName] = await Promise.all([
getWeather(lat, lon),
reverseGeocode(lat, lon)
]);
render(weather, cityName || '本地');
return;
} catch(e) {
console.log('[天气组件] 定位失败:', e.message || e);
}
}
await loadCity(DEFAULT_CITY);
} catch(e) {
console.error('[天气组件] 初始化失败:', e);
await showError('获取天气失败');
}
}
init();
})();
</script>
</section>
如果你后续想换成和风天气或高德天气 API(需要你自己申请 Key),只需要替换getCityCoords和getWeather两个函数即可。