commit 14714a9e1bf225ba6747a7697b180e4c5f5ed44c Author: XIGE <710062962@qq.com> Date: Mon Feb 23 17:09:10 2026 +0800 1.0 diff --git a/Plugin.php b/Plugin.php new file mode 100644 index 0000000..8990095 --- /dev/null +++ b/Plugin.php @@ -0,0 +1,628 @@ +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. +
  5. 进入"控制台" → "应用管理" → "我的应用"
  6. +
  7. 创建新应用,选择"Web服务"类型
  8. +
  9. 添加Key,服务选择"Web服务"(不要选Web端)
  10. +
  11. 免费版每日调用限制:天气API 5000次/Key
  12. +
  13. 注意:需要实名认证后才能使用
  14. +
+
'; + } + + 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 ''; + } +} \ No newline at end of file