diff --git a/README.md b/README.md index b8afe7b..9014bf0 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,15 @@ Typecho 系统运行时代码库: * 加入 OpenWeatherMap 图标,优化字体、行高、布局与对齐,使输出居中且更协调。 * 注释和调试输出更详细,方便开发者阅读与排查。 +- 2026-03-21:切换默认天气接口为和风天气(QWeather)。 + * 为提高国内访问稳定性,主题已将默认天气请求改为使用和风天气(QWeather)API(v7 now)。 + * 请到 https://dev.qweather.com/ 注册并获取 API Key,然后在后台主题设置的“天气 API Key”项中填写。 + * 如果暂时不希望调用网络天气接口,可在后台关闭侧边栏“天气”模块以加速页面加载。 + + 注意:和风天气已启用 API Host 功能,公共域名(如 devapi.qweather.com)将逐步停止服务。如果你在控制台配置了 API Host,请在主题设置的“天气 API Host”中填写你的 Host(例如:p66mtfux4j.re.qweatherapi.com)。 + ### 注意事项 -- 请使用 **有效的天气 API Key**。上述 key `464032d12749b7c0df937df0215af884` 返回错误“Invalid API key”,说明并非 OpenWeatherMap 认可的密钥。访问 https://openweathermap.org/ 获取或检查你的帐号密钥。 -- 也可以替换为其他天气服务(和风、心知等),只需改写 `widget-weather.php` 中的请求 URL 和参数。 -- 错误信息会在侧边栏显示便于调试。 \ No newline at end of file +- 请使用 **有效的天气 API Key**。推荐使用和风天气(QWeather);获取与使用请参考:https://dev.qweather.com/ 。 +- 如果你更倾向于无需注册的快速方案,可使用 `tianqiapi.com` 作为备用接口,但稳定性与功能可能有限。 +- 错误信息会在侧边栏以注释或文本形式显示,便于调试。 \ No newline at end of file diff --git a/usr/themes/HarmonyHues/assets/weather_cache.json b/usr/themes/HarmonyHues/assets/weather_cache.json index 1be5525..5033bb8 100644 --- a/usr/themes/HarmonyHues/assets/weather_cache.json +++ b/usr/themes/HarmonyHues/assets/weather_cache.json @@ -1 +1 @@ -{"city":"Guangzhou","timestamp":1774074655,"data":{"coord":{"lon":113.25,"lat":23.1167},"weather":[{"id":803,"main":"Clouds","description":"\u591a\u4e91","icon":"04d"}],"base":"stations","main":{"temp":25.38,"feels_like":25.46,"temp_min":25.38,"temp_max":25.38,"pressure":1015,"humidity":57,"sea_level":1015,"grnd_level":1015},"visibility":10000,"wind":{"speed":3.89,"deg":182,"gust":3.83},"clouds":{"all":84},"dt":1774074242,"sys":{"country":"CN","sunrise":1774045833,"sunset":1774089483},"timezone":28800,"id":1809858,"name":"Guangzhou","cod":200}} \ No newline at end of file +{"city":"Guangzhou","timestamp":1774087093,"data":{"code":"200","updateTime":"2026-03-21T17:53+08:00","fxLink":"https:\/\/www.qweather.com\/weather\/guangzhou-101280101.html","now":{"obsTime":"2026-03-21T17:50+08:00","temp":"21","feelsLike":"23","icon":"501","text":"\u96fe","wind360":"9","windDir":"\u5317\u98ce","windScale":"0","windSpeed":"1","humidity":"92","precip":"0.0","pressure":"1007","vis":"2","cloud":"100","dew":"19"},"refer":{"sources":["QWeather"],"license":["QWeather Developers License"]}}} \ No newline at end of file diff --git a/usr/themes/HarmonyHues/components/widgets/widget-weather.php b/usr/themes/HarmonyHues/components/widgets/widget-weather.php index b07cddd..d12d630 100644 --- a/usr/themes/HarmonyHues/components/widgets/widget-weather.php +++ b/usr/themes/HarmonyHues/components/widgets/widget-weather.php @@ -13,98 +13,244 @@ if ( ! defined('__TYPECHO_ROOT_DIR__')) { // 只有在侧边栏开启且不是文章页面时显示 if ( ! empty($this->options->sidebarBlock) && in_array('ShowSidebarWeather', $this->options->sidebarBlock) && ! $this->is('post')): ?> options->weatherCity ?: 'Beijing'); -$apiKey = trim($this->options->weatherApiKey ?: ''); +$city = 'Guangzhou'; +$apiKey = '71215dad0abc4250897761e3fa8ca796'; +$apiHost = 'p66mtfux4j.re.qweatherapi.com'; +$cacheNote = '注意:API Key/Host 已硬编码在主题文件中,如需更换请编辑 components/widgets/widget-weather.php'; $cacheFile = dirname(__DIR__, 2) . '/assets/weather_cache.json'; // 缓存机制:记录城市和时间,过期 1 小时,城市变更时自动失效 -function hh_fetch_weather($city, $apiKey, $cacheFile) { - if (file_exists($cacheFile)) { - $raw = @file_get_contents($cacheFile); - $cached = $raw ? json_decode($raw, true) : null; - // 缓存结构:{"city":"xxx","timestamp":12345678,"data":{…}} - if ($cached && isset($cached['city'], $cached['timestamp'], $cached['data'])) { - if ($cached['city'] === $city && time() - $cached['timestamp'] < 3600) { - return $cached['data']; - } - } +function hh_fetch_weather($city, $apiKey, $cacheFile, $apiHost = '') { + if (file_exists($cacheFile)) { + $raw = @file_get_contents($cacheFile); + $cached = $raw ? json_decode($raw, true) : null; + // 缓存结构:{"city":"xxx","timestamp":12345678,"data":{…}} + if ($cached && isset($cached['city'], $cached['timestamp'], $cached['data'])) { + if ($cached['city'] === $city && time() - $cached['timestamp'] < 3600) { + return $cached['data']; + } } + } - if (empty($apiKey) || empty($city)) { - return null; - } - - $url = 'https://api.openweathermap.org/data/2.5/weather?q=' . urlencode($city) - . '&appid=' . $apiKey . '&units=metric&lang=zh_cn'; - $json = @file_get_contents($url); - if ($json) { - $payload = json_encode([ - 'city' => $city, - 'timestamp' => time(), - 'data' => json_decode($json, true), - ]); - @file_put_contents($cacheFile, $payload); - return json_decode($json, true); - } + if (empty($apiKey) || empty($city)) { return null; + } + + // 使用和风天气(QWeather)API:v7 now,使用自定义 API Host 替换公共域名 + $host = !empty($apiHost) ? $apiHost : 'devapi.qweather.com'; + + // 如果 city 不是纯数字(city id),先使用 Geo API 查找城市 ID(geo/v2/city/lookup) + $location = $city; + if (!preg_match('/^\d+$/', $city)) { + $geoUrl = 'https://' . $host . '/geo/v2/city/lookup?location=' . urlencode($city) . '&key=' . $apiKey; + $geoJson = false; + if (function_exists('curl_init')) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $geoUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); + curl_setopt($ch, CURLOPT_TIMEOUT, 6); + curl_setopt($ch, CURLOPT_USERAGENT, 'HarmonyHues-Weather/1.0'); + $geoJson = curl_exec($ch); + $geo_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($geoJson === false || intval($geo_code) !== 200) { + $geoJson = false; + } + } else { + $ctx = stream_context_create(['http' => ['timeout' => 6, 'method' => 'GET']]); + $geoJson = @file_get_contents($geoUrl, false, $ctx); + } + if ($geoJson) { + $geoData = json_decode($geoJson, true); + if (isset($geoData['location'][0]['id'])) { + $location = $geoData['location'][0]['id']; + } + } + } + + $url = 'https://' . $host . '/v7/weather/now?location=' . urlencode($location) . '&key=' . $apiKey . '&lang=zh'; + + // 优先使用 cURL,以支持连接/读取超时 + $json = false; + if (function_exists('curl_init')) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); // 连接超时 3s + curl_setopt($ch, CURLOPT_TIMEOUT, 6); // 总超时 6s + curl_setopt($ch, CURLOPT_USERAGENT, 'HarmonyHues-Weather/1.0'); + $json = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($json === false || intval($http_code) !== 200) { + $json = false; + } + } else { + // 回退到 file_get_contents,但设置默认的 socket 超时 + $ctx = stream_context_create([ + 'http' => [ + 'timeout' => 6, + 'method' => 'GET', + 'header' => "User-Agent: HarmonyHues-Weather/1.0\r\n" + ] + ]); + $json = @file_get_contents($url, false, $ctx); + } + + if ($json) { + $data = json_decode($json, true); + // 和风返回示例:{"code":"200","updateTime":"...","now":{...}} + if (isset($data['code']) && ($data['code'] === '200' || $data['code'] === 200)) { + $payload = json_encode([ + 'city' => $city, + 'timestamp' => time(), + 'data' => $data, + ]); + @file_put_contents($cacheFile, $payload); + return $data; + } + } + return null; } -$weather = hh_fetch_weather($city, $apiKey, $cacheFile); -// 用于调试:在 HTML 注释中输出参数和值,方便查看 -$reqUrl = 'https://api.openweathermap.org/data/2.5/weather?q=' . urlencode($city) - . '&appid=' . $apiKey . '&units=metric&lang=zh_cn'; +$weather = hh_fetch_weather($city, $apiKey, $cacheFile, $apiHost); +// 用于调试:记录请求 URL(和风天气) +$reqUrl = 'https://' . $apiHost . '/v7/weather/now?location=' . urlencode($city) + . '&key=' . $apiKey . '&lang=zh'; ?> + false, + 'city' => null, + 'description' => null, + 'temp' => null, + 'feels_like' => null, + 'humidity' => null, + 'pressure' => null, + 'wind_speed' => null, + 'wind_deg' => null, + 'wind_dir' => null, + 'wind_scale' => null, + 'icon_code' => null, + 'precip' => null, + 'vis' => null, + 'cloud' => null, + 'dew' => null, + 'obsTime' => null, + 'refer' => null, + 'raw' => $raw, + ]; + if (!is_array($raw)) return $out; + + // QWeather 格式 + if (isset($raw['now'])) { + $now = $raw['now']; + $out['ok'] = (isset($raw['code']) && ($raw['code'] === '200' || $raw['code'] === 200)); + // 某些 Host 返回 location 字段,某些返回 location 数组 + if (isset($raw['location']) && is_array($raw['location'])) { + $out['city'] = $raw['location'][0]['name'] ?? $raw['location'][0]['adm2'] ?? null; + } else { + $out['city'] = $raw['location'] ?? null; + } + $out['description'] = $now['text'] ?? null; + $out['temp'] = $now['temp'] ?? null; + $out['feels_like'] = $now['feelsLike'] ?? null; + $out['humidity'] = $now['humidity'] ?? null; + $out['pressure'] = $now['pressure'] ?? null; + $out['wind_speed'] = $now['windSpeed'] ?? null; + $out['wind_dir'] = $now['windDir'] ?? ($now['wind360'] ?? null); + $out['wind_scale'] = $now['windScale'] ?? null; + $out['wind_deg'] = $now['wind360'] ?? null; + $out['icon_code'] = $now['icon'] ?? null; + $out['precip'] = $now['precip'] ?? null; + $out['vis'] = $now['vis'] ?? null; + $out['cloud'] = $now['cloud'] ?? null; + $out['dew'] = $now['dew'] ?? null; + $out['obsTime'] = $now['obsTime'] ?? null; + $out['refer'] = $raw['refer'] ?? null; + return $out; + } + + // OpenWeatherMap 格式 + if (isset($raw['weather'][0]) && isset($raw['main'])) { + $w = $raw['weather'][0]; + $m = $raw['main']; + $out['ok'] = true; + $out['city'] = $raw['name'] ?? null; + $out['description'] = $w['description'] ?? null; + $out['temp'] = $m['temp'] ?? null; + $out['feels_like'] = $m['feels_like'] ?? ($m['feels_like'] ?? null); + $out['humidity'] = $m['humidity'] ?? null; + $out['pressure'] = $m['pressure'] ?? null; + $out['wind_speed'] = $raw['wind']['speed'] ?? null; + $out['wind_deg'] = $raw['wind']['deg'] ?? null; + $out['icon'] = $w['icon'] ?? null; + return $out; + } + + return $out; +} + +$norm = hh_normalize_weather($weather); +// 格式化更新时间为“YYYY-MM-DD HH:MM”便于阅读 +$displayObsTime = '-'; +if (!empty($norm['obsTime'])) { + try { + $dt = new DateTime($norm['obsTime']); + $displayObsTime = $dt->format('Y-m-d H:i'); + } catch (Exception $e) { + $displayObsTime = $norm['obsTime']; + } +} +?>