教程

之前的心知天气版本 已失效 ,本版本由作者开发,灵感来源于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 + 
                '&current=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),只需要替换 getCityCoordsgetWeather 两个函数即可。
最后修改:2026 年 08 月 02 日
如果觉得我的文章对你有用,请请我喝杯咖啡,咖啡是我创作的唯一动力
END
本文作者:
文章标题:【已修改新版本】添加天气小组件(侧边栏)
本文地址:https://yuluo.xyz/archives/84.html
许可协议:知识共享署名-非商业性使用 4.0 国际许可协议
版权说明:若无注明,本文皆辞入森川原创,转载请保留文章出处。