258 lines
14 KiB
PHP
258 lines
14 KiB
PHP
<?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;
|
||
}
|
||
|
||
// 使用和风天气(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, $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); ?> • 更新时间:<?php echo htmlspecialchars($displayObsTime, ENT_QUOTES); ?></div>
|
||
<?php endif; ?>
|
||
<?php else: ?>
|
||
<p>天气获取失败(请检查 API Key 与网络)</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php endif; ?>
|