Files
blog/usr/themes/HarmonyHues/components/widgets/widget-weather.php

258 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 天气小部件
*
* @author 星语社长
* @link https://biibii.cn
* @update 2026-03-03
*/
if ( ! defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
// 只有在侧边栏开启且不是文章页面时显示
if ( ! empty($this->options->sidebarBlock) && in_array('ShowSidebarWeather', $this->options->sidebarBlock) && ! $this->is('post')): ?>
<?php
$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, $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;
}
// 使用和风天气QWeatherAPIv7 now使用自定义 API Host 替换公共域名
$host = !empty($apiHost) ? $apiHost : 'devapi.qweather.com';
// 如果 city 不是纯数字city id先使用 Geo API 查找城市 IDgeo/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, $apiHost);
// 用于调试:记录请求 URL和风天气
$reqUrl = 'https://' . $apiHost . '/v7/weather/now?location=' . urlencode($city)
. '&key=' . $apiKey . '&lang=zh';
?>
<?php
// 统一解析不同天气接口的返回为标准字段
function hh_normalize_weather($raw) {
$out = [
'ok' => 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'];
}
}
?>
<!-- weather widget debug: city=<?php echo htmlspecialchars($city, ENT_QUOTES); ?>, key_len=<?php echo strlen($apiKey); ?>, url=<?php echo htmlspecialchars($reqUrl, ENT_QUOTES); ?>, allow_url_fopen=<?php echo ini_get('allow_url_fopen') ? 'on' : 'off'; ?>, last_err=<?php $e = error_get_last(); echo $e ? htmlspecialchars($e['message'], ENT_QUOTES) : 'none'; ?> -->
<div class="hh-widget mt-3 weather-widget" style="font-size:0.9rem;line-height:1.4;">
<?php if ($norm['ok']): ?>
<div style="font-size:0.95rem;font-weight:600;margin-bottom:6px;">作者的城市天气</div>
<div class="weather-header" style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;">
<div style="width:64px;height:64px;display:flex;align-items:center;justify-content:center;margin-right:10px;">
<!-- 简易条件 SVG根据描述选择保持通用 -->
<?php if (stripos($norm['description'] ?? '', '雨') !== false): ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 13a4 4 0 014-4h1" stroke="#2b7cff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 9a5 5 0 0110 0" stroke="#2b7cff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 19l1.5-2M12 19l1.5-2M16 19l1.5-2" stroke="#2b7cff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
<?php elseif (stripos($norm['description'] ?? '', '云') !== false || stripos($norm['description'] ?? '', '阴') !== false): ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 17.5A4.5 4.5 0 0015.5 13H7a4 4 0 010-8 5 5 0 015 5h.5" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
<?php elseif (stripos($norm['description'] ?? '', '雾') !== false || stripos($norm['description'] ?? '', '霾') !== false): ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 12h20" stroke="#999" stroke-width="1.5" stroke-linecap="round"/><path d="M2 16h20" stroke="#999" stroke-width="1.5" stroke-linecap="round"/></svg>
<?php else: ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="4" stroke="#ffb400" stroke-width="1.5"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" stroke="#ffb400" stroke-width="1.2" stroke-linecap="round"/></svg>
<?php endif; ?>
</div>
<div>
<p style="margin:0;font-weight:bold;line-height:1;font-size:1rem;"><?php echo htmlspecialchars($norm['city'] ?: $city, ENT_QUOTES); ?></p>
<p style="margin:0;line-height:1;color:#333;"><?php echo htmlspecialchars($norm['description'] ?? '-', ENT_QUOTES); ?> • <strong><?php echo htmlspecialchars($norm['temp'] ?? '-', ENT_QUOTES); ?>°C</strong></p>
</div>
</div>
<div style="width:1px"></div>
</div>
<div class="weather-grid" style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:8px;font-size:0.9rem;color:#444;">
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M12 2v20" stroke="#ff6b6b" stroke-width="1.5" stroke-linecap="round"/></svg> 体感:<?php echo htmlspecialchars($norm['feels_like'] ?? '-', ENT_QUOTES); ?>°C</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M3 12h18" stroke="#5aa9ff" stroke-width="1.5" stroke-linecap="round"/></svg> 湿度:<?php echo htmlspecialchars($norm['humidity'] ?? '-', ENT_QUOTES); ?>%</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M3 6h18" stroke="#888" stroke-width="1.5" stroke-linecap="round"/></svg> 气压:<?php echo htmlspecialchars($norm['pressure'] ?? '-', ENT_QUOTES); ?> hPa</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M3 6l3 6h12l3-6" stroke="#6c6cff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> 风:<?php echo htmlspecialchars($norm['wind_speed'] ?? '-', ENT_QUOTES); ?> m/s <?php echo htmlspecialchars($norm['wind_dir'] ?? '', ENT_QUOTES); ?><?php if (!empty($norm['wind_scale'])) echo ' 级别 ' . htmlspecialchars($norm['wind_scale'], ENT_QUOTES); ?></div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M12 2v4" stroke="#2b7cff" stroke-width="1.5" stroke-linecap="round"/></svg> 降水:<?php echo htmlspecialchars($norm['precip'] ?? '-', ENT_QUOTES); ?> mm</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M3 12h18" stroke="#999" stroke-width="1.5" stroke-linecap="round"/></svg> 能见度:<?php echo htmlspecialchars($norm['vis'] ?? '-', ENT_QUOTES); ?> km</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M12 2c4 0 7 3 7 7s-3 7-7 7-7-3-7-7 3-7 7-7z" stroke="#999" stroke-width="1.2"/></svg> 云量:<?php echo htmlspecialchars($norm['cloud'] ?? '-', ENT_QUOTES); ?>%</div>
<div style="display:flex;align-items:center;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right:6px"><path d="M3 12h18" stroke="#666" stroke-width="1.2"/></svg> 露点:<?php echo htmlspecialchars($norm['dew'] ?? '-', ENT_QUOTES); ?>°C</div>
</div>
<?php if (!empty($norm['refer'])): ?>
<div style="font-size:0.65rem;color:#888;margin-top:6px;">来源:<?php echo htmlspecialchars(implode(', ', $norm['refer']['sources'] ?? $norm['refer'] ?? []), ENT_QUOTES); ?> &nbsp;•&nbsp; 更新时间:<?php echo htmlspecialchars($displayObsTime, ENT_QUOTES); ?></div>
<?php endif; ?>
<?php else: ?>
<p>天气获取失败(请检查 API Key 与网络)</p>
<?php endif; ?>
</div>
<?php endif; ?>