write = array(__CLASS__, 'onWrite'); Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = array(__CLASS__, 'finishSaveWeatherData'); Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishSave = array(__CLASS__, 'finishSaveWeatherData'); // 删除文章时同步删除天气数据 Typecho_Plugin::factory('Widget_Contents_Post_Edit')->delete = array(__CLASS__, 'onDelete'); return '天气插件已激活!'; } public static function deactivate() { return '天气插件已禁用'; } public static function config(Typecho_Widget_Helper_Form $form) { echo '

高德地图API配置

'; $apiKey = new Typecho_Widget_Helper_Form_Element_Text('apiKey', NULL, '', '高德地图API_KEY', ''); $form->addInput($apiKey->addRule('required', '必须填写API Key')); // 添加测试按钮 echo '
'; echo ''; echo '
📝 如何获取高德API Key:
  1. 访问 高德开放平台
  2. 注册并登录账号
  3. 进入"控制台" → "应用管理" → "我的应用"
  4. 创建新应用,选择"Web服务"类型
  5. 添加Key,服务选择"Web服务"(不要选Web端)
  6. 免费版每日调用限制:天气API 5000次/Key
  7. 注意:需要实名认证后才能使用
'; } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} private static function createTable() { try { $db = Typecho_Db::get(); $prefix = $db->getPrefix(); // 检查表是否存在 $result = $db->fetchRow($db->query("SHOW TABLES LIKE '{$prefix}article_weather'", Typecho_Db::READ)); if (!$result) { // 表不存在,创建新表 $sql = "CREATE TABLE IF NOT EXISTS `{$prefix}article_weather` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `cid` int(10) unsigned NOT NULL, `location` varchar(100) DEFAULT '', `display_location` varchar(100) DEFAULT '', `weather` varchar(50) DEFAULT '', `temperature` varchar(50) DEFAULT '', `created` int(10) unsigned DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `cid` (`cid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"; $db->query($sql); } else { // 表已存在,检查是否已有display_location字段 $result = $db->fetchRow($db->query("SHOW COLUMNS FROM `{$prefix}article_weather` LIKE 'display_location'", Typecho_Db::READ)); if (!$result) { // 添加display_location字段 $db->query("ALTER TABLE `{$prefix}article_weather` ADD COLUMN `display_location` varchar(100) DEFAULT '' AFTER `location`"); } } } catch (Exception $e) { error_log("ArticleWeather创建表失败: " . $e->getMessage()); } } /** * 提供主题调用的添加字段方法 */ public static function addFieldToLayout($layout) { $cid = isset($_GET['cid']) ? intval($_GET['cid']) : 0; $currentLocation = ''; $currentDisplayLocation = ''; if ($cid > 0) { $weatherData = self::getWeatherData($cid); if ($weatherData) { $currentLocation = isset($weatherData['location']) ? $weatherData['location'] : ''; $currentDisplayLocation = isset($weatherData['display_location']) ? $weatherData['display_location'] : ''; } } // 天气地点字段 $locationField = new Typecho_Widget_Helper_Form_Element_Text('weather_location', NULL, $currentLocation, '天气地点', '填写城市名称,获取发布当日天气、温度,留空则不使用本功能'); $locationField->input->setAttribute('style', 'width: 50%; max-width: 200px;'); $locationField->input->setAttribute('placeholder', '例如:北京市'); $locationField->input->setAttribute('name', 'fields[weather_location]'); $layout->addItem($locationField); // 新增:发布地点字段 $displayLocationField = new Typecho_Widget_Helper_Form_Element_Text('display_location', NULL, $currentDisplayLocation, '发布地点', '填写要显示在文章页的发布地点,留空则使用天气地点'); $displayLocationField->input->setAttribute('style', 'width: 50%; max-width: 200px;'); $displayLocationField->input->setAttribute('placeholder', '例如:上海外等'); $displayLocationField->input->setAttribute('name', 'fields[display_location]'); $layout->addItem($displayLocationField); } /** * write 钩子 - 获取天气数据 */ public static function onWrite($contents, $widget) { // 处理天气地点 if (isset($_POST['fields']) && isset($_POST['fields']['weather_location'])) { $location = trim($_POST['fields']['weather_location']); if (!empty($location)) { $created = isset($contents['created']) && $contents['created'] ? $contents['created'] : time(); $publishDate = date('Y-m-d', $created); try { $weatherData = self::fetchWeatherFromAPI($location, $publishDate); self::$pendingWeatherData = array( 'location' => $location, 'weatherData' => $weatherData ); } catch (Exception $e) { // 保存错误信息,但不中断流程 self::$pendingWeatherData = array( 'location' => $location, 'error' => $e->getMessage() ); } } else { self::$pendingWeatherData = array(); } } return $contents; } /** * 文章保存完成后的钩子 */ public static function finishSaveWeatherData($contents, $widget) { if (!isset($widget->cid) || !$widget->cid) { return $contents; } $cid = $widget->cid; // 获取天气地点 $location = ''; if (isset($_POST['fields']) && isset($_POST['fields']['weather_location'])) { $location = trim($_POST['fields']['weather_location']); } // 获取显示地点 $displayLocation = ''; if (isset($_POST['fields']) && isset($_POST['fields']['display_location'])) { $displayLocation = trim($_POST['fields']['display_location']); } if (!empty(self::$pendingWeatherData) && self::$pendingWeatherData['location'] == $location) { $pendingData = self::$pendingWeatherData; if (isset($pendingData['error'])) { error_log("ArticleWeather API错误: " . $pendingData['error']); self::$pendingWeatherData = array(); return $contents; } $weatherData = $pendingData['weatherData']; // 如果显示地点为空,使用天气地点 if (empty($displayLocation) && !empty($location)) { $displayLocation = $location; } self::saveToDatabase($cid, $location, $displayLocation, $weatherData); self::$pendingWeatherData = array(); } else { if (!empty($location)) { $created = isset($widget->created) && $widget->created ? $widget->created : time(); $publishDate = date('Y-m-d', $created); try { $weatherData = self::fetchWeatherFromAPI($location, $publishDate); // 如果显示地点为空,使用天气地点 if (empty($displayLocation)) { $displayLocation = $location; } self::saveToDatabase($cid, $location, $displayLocation, $weatherData); } catch (Exception $e) { error_log("ArticleWeather API错误: " . $e->getMessage()); } } else { self::removeFromDatabase($cid); } } return $contents; } /** * 删除文章时的钩子 */ public static function onDelete($cid, $widget) { self::removeFromDatabase($cid); return $cid; } /** * 从API获取天气数据 - 使用高德地图天气API */ private static function fetchWeatherFromAPI($location, $date = null) { $options = Typecho_Widget::widget('Widget_Options')->plugin('ArticleWeather'); $apiKey = $options ? $options->apiKey : ''; if (empty($apiKey)) { throw new Exception('请先设置高德地图API Key'); } if (empty($date)) { $date = date('Y-m-d'); } // 使用高德地图天气API try { // 第一步:获取城市编码(adcode) $geocodeUrl = "https://restapi.amap.com/v3/geocode/geo?key=" . urlencode($apiKey) . "&address=" . urlencode($location); $geocodeResponse = self::httpRequest($geocodeUrl); if (!$geocodeResponse) { throw new Exception('地理位置编码请求失败,请检查网络连接'); } $geocodeData = json_decode($geocodeResponse, true); if (!$geocodeData || !isset($geocodeData['status'])) { throw new Exception('地理位置API返回数据格式错误'); } if ($geocodeData['status'] !== '1') { $msg = isset($geocodeData['info']) ? $geocodeData['info'] : '未知错误'; $code = isset($geocodeData['infocode']) ? $geocodeData['infocode'] : '未知'; if ($code === '10001') { throw new Exception('API Key无效或不存在,请检查Key是否正确'); } elseif ($code === '10003') { throw new Exception('API Key每日调用超限(5000次/天)'); } elseif ($code === '10004') { throw new Exception('API Key访问权限不足,请确认已开通Web服务'); } elseif ($code === '10005') { throw new Exception('请求频率超限'); } else { throw new Exception("地理位置查询失败 [{$code}]: {$msg}"); } } if (empty($geocodeData['geocodes'])) { throw new Exception('未找到该城市信息,请确认城市名称是否正确:' . $location); } // 获取城市编码(adcode) $adcode = $geocodeData['geocodes'][0]['adcode']; // 第二步:获取天气信息 // 高德天气API提供实时天气(base)和预报天气(all) // 这里我们使用实时天气,因为免费版可能不支持历史天气 $weatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=" . urlencode($apiKey) . "&city=" . urlencode($adcode) . "&extensions=base"; $weatherResponse = self::httpRequest($weatherUrl); if (!$weatherResponse) { throw new Exception('天气API请求失败'); } $weatherData = json_decode($weatherResponse, true); if (!$weatherData || !isset($weatherData['status'])) { throw new Exception('天气API返回数据格式错误'); } if ($weatherData['status'] !== '1') { $msg = isset($weatherData['info']) ? $weatherData['info'] : '未知错误'; $code = isset($weatherData['infocode']) ? $weatherData['infocode'] : '未知'; throw new Exception("天气查询失败 [{$code}]: {$msg}"); } if (empty($weatherData['lives']) || empty($weatherData['lives'][0])) { throw new Exception('天气API返回数据为空'); } $live = $weatherData['lives'][0]; // 检查是否是今天 $today = date('Y-m-d'); $reportTime = isset($live['reporttime']) ? substr($live['reporttime'], 0, 10) : $today; // 如果是过去日期,尝试获取预报天气 if ($date != $today && strtotime($date) > strtotime($today)) { // 尝试获取预报天气 $forecastUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=" . urlencode($apiKey) . "&city=" . urlencode($adcode) . "&extensions=all"; $forecastResponse = self::httpRequest($forecastUrl); if ($forecastResponse) { $forecastData = json_decode($forecastResponse, true); if ($forecastData && $forecastData['status'] === '1' && !empty($forecastData['forecasts'][0]['casts'])) { $casts = $forecastData['forecasts'][0]['casts']; // 查找指定日期的预报 foreach ($casts as $cast) { if ($cast['date'] == $date) { return array( 'weather' => $cast['dayweather'], 'temperature' => $cast['nighttemp'] . '~' . $cast['daytemp'] . '°C', 'date' => $date, 'note' => '预报天气' ); } } // 使用第一天预报 $cast = $casts[0]; return array( 'weather' => $cast['dayweather'], 'temperature' => $cast['nighttemp'] . '~' . $cast['daytemp'] . '°C', 'date' => $cast['date'], 'note' => '预报天气' ); } } } // 使用实时天气数据 return array( 'weather' => $live['weather'], 'temperature' => $live['temperature'] . '°C', 'date' => $reportTime, 'note' => '实时天气' ); } catch (Exception $e) { throw $e; } } /** * 通用的HTTP请求方法 */ private static function httpRequest($url) { // 优先使用cURL if (function_exists('curl_init')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_USERAGENT, 'ArticleWeather Plugin/22.1'); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($httpCode == 200 && $response) { return $response; } else { error_log("ArticleWeather cURL请求失败: URL: {$url}, HTTP Code: {$httpCode}, Error: {$error}"); return false; } } // 备选方案:使用file_get_contents $context = stream_context_create([ 'http' => [ 'timeout' => 10, 'ignore_errors' => true, 'header' => "User-Agent: ArticleWeather Plugin/22.1\r\n" ], 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ] ]); $response = @file_get_contents($url, false, $context); if ($response === false) { $error = error_get_last(); error_log("ArticleWeather HTTP请求失败: " . ($error['message'] ?? '未知错误')); return false; } return $response; } /** * 保存到数据库 */ private static function saveToDatabase($cid, $location, $displayLocation, $weatherData) { try { $db = Typecho_Db::get(); $data = array( 'cid' => $cid, 'location' => $location, 'display_location' => $displayLocation, 'weather' => isset($weatherData['weather']) ? $weatherData['weather'] : '', 'temperature' => isset($weatherData['temperature']) ? $weatherData['temperature'] : '', 'created' => time() ); // 检查是否已存在 $exists = $db->fetchRow($db->select()->from('table.' . self::$tableName)->where('cid = ?', $cid)); if ($exists) { $db->query($db->update('table.' . self::$tableName)->rows($data)->where('cid = ?', $cid)); } else { $db->query($db->insert('table.' . self::$tableName)->rows($data)); } return true; } catch (Exception $e) { error_log("ArticleWeather保存失败: " . $e->getMessage() . " CID: " . $cid); return false; } } /** * 从数据库删除记录 */ private static function removeFromDatabase($cid) { try { $db = Typecho_Db::get(); $db->query($db->delete('table.' . self::$tableName)->where('cid = ?', $cid)); return true; } catch (Exception $e) { error_log("ArticleWeather删除失败: " . $e->getMessage() . " CID: " . $cid); return false; } } /** * 获取天气数据 */ private static function getWeatherData($cid, $field = '') { if (!$cid) return null; try { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.' . self::$tableName)->where('cid = ?', $cid)); if ($row && !empty($row['location'])) { if ($field && isset($row[$field])) { return $row[$field]; } return $row; } } catch (Exception $e) { // 忽略错误 } return null; } /** * 生成天气HTML - 紧凑单行显示 */ private static function generateWeatherHtml($location, $weather, $temperature) { $weatherIcon = self::getWeatherIcon($weather); return << {$location} | {$weather} | {$temperature} HTML; } /** * 获取天气图标 */ private static function getWeatherIcon($weather) { if (strpos($weather, '晴') !== false) return '☀️'; if (strpos($weather, '多云') !== false) return '⛅'; if (strpos($weather, '阴') !== false) return '☁️'; if (strpos($weather, '雨') !== false) return '🌧️'; if (strpos($weather, '雪') !== false) return '❄️'; if (strpos($weather, '雷') !== false) return '⛈️'; if (strpos($weather, '雾') !== false) return '🌫️'; if (strpos($weather, '沙') !== false) return '🌪️'; return '🌤️'; } /** * 手动调用方法 - 主要使用这个 */ public static function show($cid = null) { if (!$cid) { $widget = Typecho_Widget::widget('Widget_Archive'); if (!$widget->is('single')) return ''; $cid = $widget->cid; } $weatherData = self::getWeatherData($cid); if ($weatherData && !empty($weatherData['location']) && !empty($weatherData['weather'])) { // 优先使用显示地点,如果为空则使用天气地点 $displayLocation = !empty($weatherData['display_location']) ? $weatherData['display_location'] : $weatherData['location']; return self::generateWeatherHtml( $displayLocation, $weatherData['weather'], $weatherData['temperature'] ); } return ''; } }