contentEx = array('MyTrack_Plugin', 'parseMapShortcode'); Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('MyTrack_Plugin', 'parseMapShortcode'); // 注册文章保存时的处理 Typecho_Plugin::factory('Widget_Abstract_Contents')->save = array('MyTrack_Plugin', 'onPostSave'); Typecho_Plugin::factory('Widget_Abstract_Contents')->write = array('MyTrack_Plugin', 'onPostWrite'); // 注册footer钩子来输出JS Typecho_Plugin::factory('Widget_Archive')->footer = array('MyTrack_Plugin', 'footer'); // 注册header钩子来输出CSS Typecho_Plugin::factory('Widget_Archive')->header = array('MyTrack_Plugin', 'header'); return _t('插件已激活'); } /** * 禁用插件方法 * * @access public * @return void */ public static function deactivate() { // 移除管理菜单 - 使用与activate()相同的索引 Helper::removePanel(3, 'MyTrack/Manage.php'); // 同时从数据库中清理相关记录 try { $db = Typecho_Db::get(); // 清理options表中的面板记录 $db->query($db->delete('table.options') ->where('name = ?', 'panelTable:MyTrack/Manage.php')); } catch (Exception $e) { // 忽略错误 } Helper::removeAction('track'); Helper::removeRoute('track_action'); return _t('插件已禁用'); } /** * 获取插件配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form 配置面板 * @return void */ public static function config(Typecho_Widget_Helper_Form $form) { // 高德地图JS API密钥 $jsApiKey = new Typecho_Widget_Helper_Form_Element_Text('jsApiKey', null, '', _t('高德地图JS API密钥 *'), _t('用于前端地图显示的API密钥,必填项')); $jsApiKey->addRule('required', _t('JS API密钥不能为空')); $form->addInput($jsApiKey); // 高德地图Web服务API密钥 $webApiKey = new Typecho_Widget_Helper_Form_Element_Text('webApiKey', null, '', _t('高德地图Web服务API密钥 *'), _t('用于后台地址搜索的API密钥,必填项')); $webApiKey->addRule('required', _t('Web服务API密钥不能为空')); $form->addInput($webApiKey); // 默认缩放级别 $zoomLevel = new Typecho_Widget_Helper_Form_Element_Text('zoomLevel', null, '15', _t('默认缩放级别 *'), _t('设置地图默认缩放级别,范围3-18,默认为15,必填项')); $zoomLevel->addRule('required', _t('缩放级别不能为空')) ->addRule(function($value) { return is_numeric($value) && $value >= 3 && $value <= 18; }, _t('缩放级别必须是3-18之间的数字')); $form->addInput($zoomLevel); // 默认视图模式 $viewMode = new Typecho_Widget_Helper_Form_Element_Radio('viewMode', array( '2D' => '2D视图', '3D' => '3D视图' ), '2D', _t('默认视图模式')); $form->addInput($viewMode); // 默认地图主题 $mapTheme = new Typecho_Widget_Helper_Form_Element_Select('mapTheme', array( 'normal' => '标准', 'dark' => '幻影黑', 'light' => '月光银', 'whitesmoke' => '远山黛', 'fresh' => '草色青', 'grey' => '雅士灰', 'graffiti' => '涂鸦', 'macaron' => '马卡龙', 'blue' => '靛青蓝', 'darkblue' => '极夜蓝', 'wine' => '酱籽' ), 'normal', _t('默认地图主题')); $form->addInput($mapTheme); // 前台显示设置 $enableDisplay = new Typecho_Widget_Helper_Form_Element_Radio('enableDisplay', array( 1 => '是', 0 => '否' ), 1, _t('显示前台足迹地图'), _t('关闭后前台将不显示足迹地图,但后台管理功能仍可使用')); $form->addInput($enableDisplay); // 缓存时间设置 $cacheExpire = new Typecho_Widget_Helper_Form_Element_Text('cacheExpire', null, '7', _t('缓存时间(天)*'), _t('设置数据缓存的有效期,单位为天,默认为7天,0表示关闭缓存')); $cacheExpire->addRule('required', _t('缓存时间不能为空')) ->addRule(function($value) { return is_numeric($value) && $value >= 0 && is_int($value + 0); }, _t('缓存时间必须是一个大于等于0的整数')); $form->addInput($cacheExpire); // 点聚合功能设置 $enableCluster = new Typecho_Widget_Helper_Form_Element_Radio('enableCluster', array( 1 => '是', 0 => '否' ), 0, _t('启用点聚合功能'), _t('启用后,当地图上标记点较多时会自动聚合,提高性能和可读性')); $form->addInput($enableCluster); // 空内容标记点颜色设置 $emptyMarkerColor = new Typecho_Widget_Helper_Form_Element_Text('emptyMarkerColor', null, '#2196F3', _t('空内容标记点颜色'), _t('设置空内容标记点的颜色')); $emptyMarkerColor->input->setAttribute('type', 'color'); $form->addInput($emptyMarkerColor); // 有内容标记点颜色设置 $contentMarkerColor = new Typecho_Widget_Helper_Form_Element_Text('contentMarkerColor', null, '#4CAF50', _t('有内容标记点颜色'), _t('设置有内容标记点的颜色')); $contentMarkerColor->input->setAttribute('type', 'color'); $form->addInput($contentMarkerColor); // 聚合标记点颜色设置 $clusterMarkerColor1 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor1', null, '#2196F3', _t('聚合标记点颜色1'), _t('设置小规模聚合标记点的颜色')); $clusterMarkerColor1->input->setAttribute('type', 'color'); $form->addInput($clusterMarkerColor1); $clusterMarkerColor2 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor2', null, '#FF9800', _t('聚合标记点颜色2'), _t('设置中等规模聚合标记点的颜色')); $clusterMarkerColor2->input->setAttribute('type', 'color'); $form->addInput($clusterMarkerColor2); $clusterMarkerColor3 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor3', null, '#FF5722', _t('聚合标记点颜色3'), _t('设置大规模聚合标记点的颜色')); $clusterMarkerColor3->input->setAttribute('type', 'color'); $form->addInput($clusterMarkerColor3); // 短代码地图卡片样式设置 $cardMapWidth = new Typecho_Widget_Helper_Form_Element_Text('cardMapWidth', null, '40%', _t('短代码地图宽度'), _t('设置短代码地图卡片的宽度,例如:40% 或 300px')); $form->addInput($cardMapWidth); $cardMapHeight = new Typecho_Widget_Helper_Form_Element_Text('cardMapHeight', null, '320px', _t('短代码地图高度'), _t('设置短代码地图卡片的高度')); $form->addInput($cardMapHeight); $cardMapZoom = new Typecho_Widget_Helper_Form_Element_Text('cardMapZoom', null, '16', _t('短代码地图缩放级别'), _t('设置短代码地图的缩放级别,范围3-18')); $form->addInput($cardMapZoom); } /** * 个人用户的配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form * @return void */ public static function personalConfig(Typecho_Widget_Helper_Form $form) {} /** * 输出CSS到页面头部 * * @access public */ public static function header() { if (!self::$cssAdded) { echo ''; self::$cssAdded = true; } } /** * 输出JS到页面底部 * * @access public */ public static function footer() { if (!empty(self::$mapConfigs) && !self::$jsAdded) { echo self::getMapCardScripts(self::$mapConfigs); self::$jsAdded = true; } } /** * 初始化数据库路径 * 优先使用已存在的数据库文件,如果没有则生成新的随机名称 * * @access private * @return void */ private static function initDbPath() { $dbDir = __DIR__ . '/db'; // 确保目录存在 if (!is_dir($dbDir)) { mkdir($dbDir, 0755, true); } // 检查是否已存在.db文件(优先使用已存在的) $dbFiles = glob($dbDir . '/track_*.db'); if (!empty($dbFiles)) { // 使用第一个找到的数据库文件(保持一致性) self::$dbPath = $dbFiles[0]; } else { // 没有找到现有数据库文件,生成新的随机名称 $randomStr = substr(md5(uniqid(rand(), true)), 0, 10); self::$dbPath = $dbDir . '/track_' . $randomStr . '.db'; } } /** * 初始化数据库 * * @access private * @return void */ private static function initDatabase() { // 确保数据库路径已初始化 if (empty(self::$dbPath)) { self::initDbPath(); } try { // 创建SQLite数据库连接 $db = new PDO('sqlite:' . self::$dbPath); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 读取SQL初始化文件 $sqlFile = __DIR__ . '/db/init.sql'; if (file_exists($sqlFile)) { $sql = file_get_contents($sqlFile); $db->exec($sql); } else { // 如果没有SQL文件,手动创建表 $db->exec("CREATE TABLE IF NOT EXISTS plugin_track_footprint ( id INTEGER PRIMARY KEY AUTOINCREMENT, latitude REAL NOT NULL, longitude REAL NOT NULL, name TEXT NOT NULL, address TEXT, location_type TEXT, rating_level INTEGER, categories TEXT, review TEXT, description TEXT, article_cid INTEGER, urlLabel TEXT, url TEXT, photos TEXT, tags TEXT, date DATETIME, markerColor TEXT, related_articles TEXT, highlights TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )"); // 创建更新时间触发器 $db->exec("CREATE TRIGGER IF NOT EXISTS update_footprint_time AFTER UPDATE ON plugin_track_footprint BEGIN UPDATE plugin_track_footprint SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END"); } $db = null; } catch (PDOException $e) { throw new Typecho_Plugin_Exception('数据库初始化失败: ' . $e->getMessage()); } } /** * 获取数据库连接 * * @access public * @return PDO */ public static function getDbConnection() { // 确保数据库路径已初始化 if (empty(self::$dbPath)) { self::initDbPath(); } // 如果数据库文件不存在,初始化数据库 if (!file_exists(self::$dbPath)) { self::initDatabase(); } try { $db = new PDO('sqlite:' . self::$dbPath); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $db; } catch (PDOException $e) { throw new Typecho_Plugin_Exception('数据库连接失败: ' . $e->getMessage()); } } /** * 通过文章CID获取文章信息和图片 * * @access public * @param integer $cid 文章CID * @return array */ public static function getArticleInfo($cid) { $result = array( 'title' => '', 'link' => '', 'text' => '', 'images' => array(), 'tags' => array(), 'created' => '' ); if (empty($cid)) { return $result; } try { // 获取文章信息,包括内容 $db = Typecho_Db::get(); $article = $db->fetchRow($db->select('title', 'slug', 'created', 'text') ->from('table.contents') ->where('cid = ?', $cid) ->where('type = ?', 'post') ->where('status = ?', 'publish') ->limit(1)); if ($article) { $result['title'] = $article['title']; $result['link'] = self::getPostUrlByCid($cid); $result['text'] = $article['text']; $result['created'] = $article['created']; // 从文章内容中提取图片(限制4张) if (!empty($article['text'])) { $result['images'] = self::getPostImagesByCid($cid); } // 获取文章标签 $result['tags'] = self::getPostTagsByCid($cid); } } catch (Exception $e) { error_log('MyTrack: 获取文章信息失败: ' . $e->getMessage()); } return $result; } /** * 通过文章CID获取文章URL * * @access public * @param integer $cid 文章CID * @return string|false */ public static function getPostUrlByCid($cid) { try { // 获取文章信息,包括日期和分类 $db = Typecho_Db::get(); $row = $db->fetchRow($db->select('slug', 'type', 'created') ->from('table.contents') ->where('cid = ?', $cid) ->where('status = ?', 'publish')); if (!$row) { return false; } // 获取文章分类 $category = ''; $categories = $db->fetchAll($db->select('slug') ->from('table.metas') ->join('table.relationships', 'table.metas.mid = table.relationships.mid') ->where('table.relationships.cid = ?', $cid) ->where('table.metas.type = ?', 'category') ->order('table.metas.order', Typecho_Db::SORT_ASC)); if (!empty($categories)) { $category = $categories[0]['slug']; } // 准备URL参数 $date = getdate($row['created']); $params = array( 'cid' => $cid, 'slug' => $row['slug'], 'category' => $category, 'directory' => $category, 'year' => $date['year'], 'month' => str_pad($date['mon'], 2, '0', STR_PAD_LEFT), 'day' => str_pad($date['mday'], 2, '0', STR_PAD_LEFT) ); // 获取自定义路径设置 $options = Typecho_Widget::widget('Widget_Options'); // 尝试获取不同的永久链接设置 $permalinkStructure = ''; // 方法1: 直接从options获取 if (isset($options->permalink)) { $permalinkStructure = $options->permalink; } // 方法2: 尝试从routingTable获取 if (empty($permalinkStructure) && isset($options->routingTable)) { $routingTable = $options->routingTable; if (isset($routingTable['post'])) { $permalinkStructure = $routingTable['post']['url']; } } // 方法3: 尝试从Typecho_Router获取 if (empty($permalinkStructure)) { $router = Typecho_Router::get(); $routes = $router->getRoutes(); if (isset($routes['post'])) { $permalinkStructure = $routes['post']['url']; } } // 如果没有找到自定义路径设置,使用默认方式 if (empty($permalinkStructure)) { return Typecho_Router::url($row['type'], $row, $options->index); } // 替换路径中的占位符 $url = $permalinkStructure; // 处理Typecho特殊格式的占位符 $url = preg_replace('/\[year:digital:4\]/', $params['year'], $url); $url = preg_replace('/\[month:digital:2\]/', $params['month'], $url); $url = preg_replace('/\[day:digital:2\]/', $params['day'], $url); $url = preg_replace('/\[slug\]/', $params['slug'], $url); $url = preg_replace('/\[cid\]/', $params['cid'], $url); $url = preg_replace('/\[category\]/', $params['category'], $url); $url = preg_replace('/\[directory\]/', $params['directory'], $url); // 替换标准占位符 foreach ($params as $key => $value) { $url = str_replace('{' . $key . '}', $value, $url); } // 确保URL以/开头 if (strpos($url, '/') !== 0) { $url = '/' . $url; } // 拼接完整URL $fullUrl = rtrim($options->siteUrl, '/') . $url; return $fullUrl; } catch (Exception $e) { error_log("MyTrack: 获取文章 URL 失败: " . $e->getMessage()); } return false; } /** * 通过文章CID获取文章中的图片 * * @access public * @param integer $cid 文章CID * @return array */ public static function getPostImagesByCid($cid) { $db = Typecho_Db::get(); $content = $db->fetchRow($db->select('text') ->from('table.contents') ->where('cid = ?', $cid) ->where('type = ?', 'post') ->where('status = ?', 'publish')); if (!$content) { return array(); } // 尝试多种图片匹配模式 $images = array(); $text = $content['text']; // 模式1: 标准img标签 preg_match_all('/]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $text, $matches1); if (isset($matches1[1]) && !empty($matches1[1])) { foreach($matches1[1] as $imageUrl) { $images[] = self::processImageUrl($imageUrl); } } // 模式2: markdown图片语法 ![alt](url) preg_match_all('/!\[[^\]]*\]\(([^)]+)\)/i', $text, $matches2); if (isset($matches2[1]) && !empty($matches2[1])) { foreach($matches2[1] as $imageUrl) { $images[] = self::processImageUrl($imageUrl); } } // 模式3: 直接匹配图片URL preg_match_all('/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/i', $text, $matches3); if (isset($matches3[0]) && !empty($matches3[0])) { foreach($matches3[0] as $imageUrl) { $images[] = self::processImageUrl($imageUrl); } } // 模式4: 匹配相对路径的图片URL preg_match_all('/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/i', $text, $matches4); if (isset($matches4[0]) && !empty($matches4[0])) { foreach($matches4[0] as $imageUrl) { if (strpos($imageUrl, 'http') !== 0) { $images[] = self::processImageUrl($imageUrl); } } } // 去重并过滤空值 $images = array_filter(array_unique($images)); // 限制最多4张图片 $images = array_slice($images, 0, 4); return $images; } /** * 通过文章CID获取文章标签 * * @access public * @param integer $cid 文章CID * @return array 标签数组 */ public static function getPostTagsByCid($cid) { try { $db = Typecho_Db::get(); // 查询文章的标签信息 $tags = $db->fetchAll($db->select('table.metas.name', 'table.metas.slug') ->from('table.metas') ->join('table.relationships', 'table.metas.mid = table.relationships.mid') ->where('table.relationships.cid = ?', $cid) ->where('table.metas.type = ?', 'tag') ->order('table.metas.order', Typecho_Db::SORT_ASC)); $tagNames = array(); if (!empty($tags)) { foreach ($tags as $tag) { $tagNames[] = $tag['name']; } } return $tagNames; } catch (Exception $e) { error_log('MyTrack: 获取文章标签失败: ' . $e->getMessage()); return array(); } } /** * 处理图片URL,将相对路径转换为绝对路径 * * @access private * @param string $imageUrl 原始图片URL * @return string 处理后的图片URL */ private static function processImageUrl($imageUrl) { // 处理相对路径 if (strpos($imageUrl, 'http') !== 0) { $options = Typecho_Widget::widget('Widget_Options'); $imageUrl = Typecho_Common::url($imageUrl, $options->siteUrl); } return $imageUrl; } /** * 数据库迁移 * 更新数据库字段,添加新字段 * * @access private * @return void */ private static function migrateDatabase() { try { // 确保数据库路径已初始化 if (empty(self::$dbPath)) { self::initDbPath(); } $db = new PDO('sqlite:' . self::$dbPath); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 检查表结构 $stmt = $db->prepare("PRAGMA table_info(plugin_track_footprint)"); $stmt->execute(); $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); $hasName = false; $hasDate = false; $hasUrl = false; $hasUrlLabel = false; $hasPhotos = false; $hasCategories = false; $hasMarkerColor = false; $hasRelatedArticles = false; $hasHighlights = false; // 检查字段是否存在 foreach ($columns as $column) { if ($column['name'] === 'name') { $hasName = true; } if ($column['name'] === 'date') { $hasDate = true; } if ($column['name'] === 'url') { $hasUrl = true; } if ($column['name'] === 'urlLabel') { $hasUrlLabel = true; } if ($column['name'] === 'photos') { $hasPhotos = true; } if ($column['name'] === 'categories') { $hasCategories = true; } if ($column['name'] === 'markerColor') { $hasMarkerColor = true; } if ($column['name'] === 'related_articles') { $hasRelatedArticles = true; } if ($column['name'] === 'highlights') { $hasHighlights = true; } } // 重命名字段:location 改为 name if (!$hasName) { // 检查是否存在 location 字段 $hasLocation = false; foreach ($columns as $column) { if ($column['name'] === 'location') { $hasLocation = true; break; } } if ($hasLocation) { // 重命名 location 为 name $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN location TO name"); } else { // 直接添加 name 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN name TEXT"); } } // 重命名字段:travel_time 改为 date if (!$hasDate) { // 检查是否存在 travel_time 字段 $hasTravelTime = false; foreach ($columns as $column) { if ($column['name'] === 'travel_time') { $hasTravelTime = true; break; } } if ($hasTravelTime) { // 重命名 travel_time 为 date $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN travel_time TO date"); } else { // 直接添加 date 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN date DATETIME"); } } // 重命名字段:article_link 改为 url if (!$hasUrl) { // 检查是否存在 article_link 字段 $hasArticleLink = false; foreach ($columns as $column) { if ($column['name'] === 'article_link') { $hasArticleLink = true; break; } } if ($hasArticleLink) { // 重命名 article_link 为 url $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN article_link TO url"); } else { // 直接添加 url 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN url TEXT"); } } // 重命名字段:article_title 改为 urlLabel if (!$hasUrlLabel) { // 检查是否存在 article_title 字段 $hasArticleTitle = false; foreach ($columns as $column) { if ($column['name'] === 'article_title') { $hasArticleTitle = true; break; } } if ($hasArticleTitle) { // 重命名 article_title 为 urlLabel $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN article_title TO urlLabel"); } else { // 直接添加 urlLabel 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN urlLabel TEXT"); } } // 重命名字段:image_links 改为 photos if (!$hasPhotos) { // 检查是否存在 image_links 字段 $hasImageLinks = false; foreach ($columns as $column) { if ($column['name'] === 'image_links') { $hasImageLinks = true; break; } } if ($hasImageLinks) { // 重命名 image_links 为 photos $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN image_links TO photos"); } else { // 直接添加 photos 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN photos TEXT"); } } // 重命名字段:status 改为 categories if (!$hasCategories) { // 检查是否存在 status 字段 $hasStatus = false; foreach ($columns as $column) { if ($column['name'] === 'status') { $hasStatus = true; break; } } if ($hasStatus) { // 重命名 status 为 categories $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN status TO categories"); } else { // 直接添加 categories 字段 $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN categories TEXT"); } } // 添加新字段:markerColor if (!$hasMarkerColor) { $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN markerColor TEXT"); } // 添加新字段:related_articles if (!$hasRelatedArticles) { $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN related_articles TEXT"); } // 添加新字段:highlights if (!$hasHighlights) { $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN highlights TEXT"); } $db = null; } catch (PDOException $e) { error_log('MyTrack数据库迁移失败: ' . $e->getMessage()); } } /** * 获取数据库文件路径 * * @access public * @return string */ public static function getDbPath() { if (empty(self::$dbPath)) { self::initDbPath(); } return self::$dbPath; } /** * 通过足迹ID获取足迹信息 * * @access public * @param integer $id 足迹ID * @return array|null 足迹信息或null */ public static function getFootprintById($id) { if (empty($id)) { return null; } try { $db = self::getDbConnection(); $stmt = $db->prepare("SELECT * FROM plugin_track_footprint WHERE id = ?"); $stmt->execute(array($id)); $footprint = $stmt->fetch(PDO::FETCH_ASSOC); if ($footprint) { // 处理分类字段 if (!empty($footprint['categories'])) { $footprint['categories'] = explode(',', $footprint['categories']); } else { $footprint['categories'] = array(); } // 处理亮点字段 if (!empty($footprint['highlights'])) { $footprint['highlights'] = explode(',', $footprint['highlights']); } else { $footprint['highlights'] = array(); } // 获取文章信息(如果有关联文章) if (!empty($footprint['article_cid'])) { $articleInfo = self::getArticleInfo($footprint['article_cid']); if (!empty($articleInfo['title']) && empty($footprint['urlLabel'])) { $footprint['urlLabel'] = $articleInfo['title']; } if (!empty($articleInfo['link']) && empty($footprint['url'])) { $footprint['url'] = $articleInfo['link']; } } // 处理关联文章信息 if (!empty($footprint['related_articles'])) { $footprint['related_articles_info'] = self::getRelatedArticlesInfo($footprint['related_articles']); } return $footprint; } } catch (Exception $e) { error_log('MyTrack: 获取足迹信息失败: ' . $e->getMessage()); } return null; } /** * 获取关联文章信息 * * @access public * @param string $relatedArticles 关联文章ID字符串(逗号分隔) * @return array 关联文章信息数组 */ public static function getRelatedArticlesInfo($relatedArticles) { $result = array(); if (empty($relatedArticles)) { return $result; } $articleIds = explode(',', $relatedArticles); $articleIds = array_map('trim', $articleIds); $articleIds = array_filter($articleIds); foreach ($articleIds as $cid) { if (is_numeric($cid)) { $articleInfo = self::getArticleInfo($cid); if ($articleInfo) { $result[$cid] = array( 'title' => $articleInfo['title'], 'link' => $articleInfo['link'], 'cid' => $cid ); } } } return $result; } /** * 解析文章内容中的地图短代码 * * @access public * @param string $content 文章内容 * @param Widget_Abstract_Contents $widget 小部件对象 * @param array $lastResult 上次解析结果 * @return string 处理后的内容 */ public static function parseMapShortcode($content, $widget, $lastResult) { $content = empty($lastResult) ? $content : $lastResult; // 匹配 {map-数字} 格式的短代码 $pattern = '/\{map-(\d+)\}/i'; if (preg_match_all($pattern, $content, $matches)) { // 获取当前文章的CID $currentCid = isset($widget->cid) ? $widget->cid : 0; // 收集所有需要渲染的地图ID $mapIds = $matches[1]; $mapIndex = 0; foreach ($mapIds as $index => $mapId) { // 渲染单个地图卡片 list($mapHtml, $config) = self::renderSingleMapCard($mapId, $mapIndex); if ($mapHtml) { $content = str_replace($matches[0][$index], $mapHtml, $content); if ($config) { self::$mapConfigs[] = $config; } // 在短代码解析时同步更新关联文章 if ($currentCid > 0) { self::syncRelatedArticles($mapId, $currentCid); } } else { $content = str_replace($matches[0][$index], '
足迹ID ' . $mapId . ' 不存在
', $content); } $mapIndex++; } } return $content; } /** * 渲染单个地图卡片HTML * * @access private * @param integer $mapId 足迹ID * @param integer $index 索引 * @return array [HTML字符串, 配置数组] */ private static function renderSingleMapCard($mapId, $index) { // 获取足迹信息 $footprint = self::getFootprintById($mapId); if (!$footprint) { return array(null, null); } // 获取插件配置 $options = Typecho_Widget::widget('Widget_Options')->plugin('MyTrack'); // 获取配置 $cardMapWidth = isset($options->cardMapWidth) ? $options->cardMapWidth : '40%'; $cardMapHeight = isset($options->cardMapHeight) ? $options->cardMapHeight : '350px'; // 处理分类显示 $categoriesHtml = ''; if (!empty($footprint['categories'])) { foreach ($footprint['categories'] as $category) { $category = trim($category); $class = ''; $text = ''; switch($category) { case 'visited': $class = 'mytrack-categories-visited-badge'; $text = '已玩'; break; case 'want': $class = 'mytrack-categories-want-badge'; $text = '向往'; break; case 'plan': $class = 'mytrack-categories-plan-badge'; $text = '计划'; break; default: $class = 'mytrack-categories-badge'; $text = $category; } $categoriesHtml .= '' . $text . ''; } } // 处理星级显示 $ratingHtml = ''; if (!empty($footprint['rating_level']) && $footprint['rating_level'] > 0) { $ratingHtml = '
'; for ($i = 1; $i <= 5; $i++) { if ($i <= $footprint['rating_level']) { $ratingHtml .= ''; } else { $ratingHtml .= ''; } } $ratingHtml .= '
'; } // 处理亮点显示 $highlightsHtml = ''; if (!empty($footprint['highlights'])) { $highlightsHtml = '
亮点:'; foreach ($footprint['highlights'] as $highlight) { $highlight = trim($highlight); if ($highlight) { $highlightsHtml .= '' . htmlspecialchars($highlight) . ' '; } } $highlightsHtml .= '
'; } // 处理关联文章显示 $relatedArticlesHtml = ''; if (!empty($footprint['related_articles_info'])) { $relatedArticlesHtml .= ''; } // 生成唯一ID $uniqueId = 'map_' . $mapId . '_' . $index; $mapCardId = 'mytrack-card-' . $uniqueId; $mapContainerId = $mapCardId . '-map'; // 构建HTML $html = '

' . htmlspecialchars($footprint['name']) . '

'; if ($footprint['address']) { $html .= '
地址:' . htmlspecialchars($footprint['address']) . '
'; } if ($footprint['location_type']) { $html .= '
类型:' . htmlspecialchars($footprint['location_type']) . '
'; } if ($categoriesHtml) { $html .= '
分类:' . $categoriesHtml . '
'; } if ($ratingHtml) { $html .= '
推荐:' . $ratingHtml . '
'; } $html .= $highlightsHtml; if ($footprint['review']) { $html .= '
简评:' . nl2br(htmlspecialchars($footprint['review'])) . '
'; } $html .= $relatedArticlesHtml; $html .= '
'; // 准备地图配置 $apiKey = isset($options->jsApiKey) ? $options->jsApiKey : ''; $cardMapZoom = isset($options->cardMapZoom) ? intval($options->cardMapZoom) : 16; $mapTheme = isset($options->mapTheme) ? $options->mapTheme : 'normal'; // 修复:正确获取markerColor和对应的颜色值 $markerColor = isset($footprint['markerColor']) && !empty($footprint['markerColor']) ? trim($footprint['markerColor']) : ''; $colorValue = self::getMarkerColorValue($markerColor); $config = array( 'containerId' => $mapContainerId, 'mapId' => $mapId, 'name' => $footprint['name'], 'latitude' => floatval($footprint['latitude']), 'longitude' => floatval($footprint['longitude']), 'markerColor' => $markerColor, 'markerColorValue' => $colorValue, 'zoom' => $cardMapZoom, 'theme' => $mapTheme, 'apiKey' => $apiKey ); return array($html, $config); } /** * 根据markerColor标识获取具体的颜色值 * * @access private * @param string $markerColor 标记颜色标识 * @return string 具体的颜色值 */ private static function getMarkerColorValue($markerColor) { // 如果markerColor为空,返回默认红色 if (empty($markerColor) || !is_string($markerColor)) { return '#ff0000'; } $markerColor = trim($markerColor); if ($markerColor === '') { return '#ff0000'; } // 颜色映射表 $colorMap = array( 'sunset' => 'linear-gradient(135deg, rgb(255, 179, 71), rgb(255, 111, 97))', 'ocean' => 'linear-gradient(135deg, rgb(6, 190, 182), rgb(72, 177, 191))', 'forest' => 'linear-gradient(135deg, rgb(94, 231, 223), rgb(57, 163, 124))', 'amber' => 'linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133))', 'violet' => 'linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235))', 'citrus' => 'linear-gradient(135deg, rgb(253, 251, 143), rgb(161, 255, 206))' ); // 如果找到对应的颜色,返回具体的颜色值,否则返回默认的红色 if (isset($colorMap[$markerColor])) { return $colorMap[$markerColor]; } return '#ff0000'; // 默认红色 } /** * 获取地图卡片CSS样式 - 修复标点颜色问题 * * @access private * @return string CSS样式 */ private static function getMapCardStyles() { return ' /* MyTrack 插件专用样式 - 防止与其他插件冲突 */ .mytrack-card { border: 1px solid #e8e8e8; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background: white; margin: 20px 0; } .mytrack-card-content { display: flex; } .mytrack-card-info { flex: 1; padding: 20px; min-width: 0; } .mytrack-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } .mytrack-card-title { margin: 0; font-size: 18px; font-weight: 600; color: #333 !important; /* 修复标题颜色,确保显示 */ } .mytrack-card-categories { margin-top: 10px; display: flex; flex-wrap: wrap; } .mytrack-categories-badge { font-size: 14px; background: #f0f0f0; color: #666; } .mytrack-categories-visited-badge { background: #fff; color: #666; } .mytrack-categories-want-badge { background: #e3f2fd; color: #1565c0; } .mytrack-categories-plan-badge { background: #fff3e0; color: #ef6c00; } .mytrack-card-info > div { margin-bottom: 10px; font-size: 14px; line-height: 1.5; } .mytrack-card-info strong { color: #555; font-weight: 600; min-width: 40px; display: inline-block; } .mytrack-card-address, .mytrack-card-type, .mytrack-card-review { color: #666; } .mytrack-card-highlights { color: #666; } .mytrack-highlight-badge { background-color: #e8f5e9; color: #2e7d32; padding: 2px 8px; border-radius: 12px; font-size: 12px; margin-right: 5px; display: inline-block; } .mytrack-card-rating { display: flex; align-items: center; } .mytrack-rating-display { display: inline-flex; align-items: center; } .mytrack-rating-star { color: #ffc107; font-size: 14px; margin-right: 2px; } .mytrack-rating-empty-star { color: #ddd; font-size: 14px; margin-right: 2px; } .mytrack-related-articles-section { margin-top: 15px; padding-top: 10px; border-top: 1px dashed #eee; } .mytrack-related-articles-list { margin: 5px 0 0 0; padding: 0; } .mytrack-related-article-item { margin-bottom: 5px; font-size: 13px; } .mytrack-related-article-item a { color: #3b82f6; text-decoration: none; } .mytrack-related-article-item a:hover { text-decoration: underline; } .mytrack-card-map { position: relative; background: #f5f5f5; } /* 修复地图标记点样式 - 移除有问题的inherit规则 */ .mytrack-marker-custom { width: 20px !important; height: 20px !important; border-radius: 50% !important; box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; cursor: pointer !important; transition: transform 0.2s ease-out !important; border: 2px solid #fff !important; position: relative !important; z-index: 100 !important; display: block !important; background: #ff0000 !important; /* 默认红色,会被JS内联样式覆盖 */ } .mytrack-marker-custom:hover { transform: scale(1.05) !important; } /* 自定义标记颜色类 */ .mytrack-marker-sunset { background: linear-gradient(135deg, rgb(255, 179, 71), rgb(255, 111, 97)) !important; } .mytrack-marker-ocean { background: linear-gradient(135deg, rgb(6, 190, 182), rgb(72, 177, 191)) !important; } .mytrack-marker-forest { background: linear-gradient(135deg, rgb(94, 231, 223), rgb(57, 163, 124)) !important; } .mytrack-marker-amber { background: linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133)) !important; } .mytrack-marker-violet { background: linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235)) !important; } .mytrack-marker-citrus { background: linear-gradient(135deg, rgb(253, 251, 143), rgb(161, 255, 206)) !important; } /* 隐藏高德地图版权信息 */ .mytrack-card-map .amap-logo, .mytrack-card-map .amap-copyright { display: none !important; visibility: hidden !important; opacity: 0 !important; } /* 确保所有地图容器中的版权信息都被隐藏 */ .mytrack-card-map div[style*="amap"] .amap-logo, .mytrack-card-map div[style*="amap"] .amap-copyright, .mytrack-card-map .amap-container .amap-logo, .mytrack-card-map .amap-container .amap-copyright { display: none !important; visibility: hidden !important; opacity: 0 !important; } .mytrack-error { padding: 20px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; text-align: center; } /* 深色模式适配 */ .dark .mytrack-card { background: #1a1a1a; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .dark .mytrack-card-title { color: #fff !important; /* 修复深色模式标题颜色 */ } .dark .mytrack-card-info strong { color: #b5b5b5; } .dark .mytrack-card-address, .dark .mytrack-card-type, .dark .mytrack-card-review, .dark .mytrack-card-highlights { color: #ccc; } .dark .mytrack-categories-badge { background: #1a1a1a; color: #d1d5db; } .dark .mytrack-highlight-badge { color: #d1d5db; background:#dc2626; } .dark .mytrack-related-articles-section { border-top-color: rgba(255, 255, 255, 0.1); } /* 深色模式下标记点边框调整为深色 */ .dark .mytrack-marker-custom { border: 2px solid #fff !important; } /* 响应式设计 */ @media (max-width: 768px) { .mytrack-card-content { flex-direction: column; } .mytrack-card-map { width: 100% !important; height: 300px !important; border-top: 1px solid #e8e8e8; } .dark .mytrack-card-map { border-top-color: rgba(255, 255, 255, 0.1); } } '; } /** * 获取地图卡片JS脚本 - 修复标点颜色问题 * * @access private * @param array $mapConfigs 地图配置数组 * @return string JS脚本 */ private static function getMapCardScripts($mapConfigs) { if (empty($mapConfigs)) { return ''; } $configsJson = json_encode($mapConfigs); $apiKey = !empty($mapConfigs[0]['apiKey']) ? $mapConfigs[0]['apiKey'] : ''; // 如果API密钥为空,直接返回错误提示 if (empty($apiKey)) { return ' '; } return ' '; } /** * 原来的renderMapCard方法(保持兼容性)- 修复主题模板调用时的标记颜色问题 * * @access public * @param integer $mapId 足迹ID * @return string 地图卡片HTML */ public static function renderMapCard($mapId) { static $index = 0; $currentIndex = $index++; // 直接调用renderSingleMapCard来确保配置正确 list($mapHtml, $config) = self::renderSingleMapCard($mapId, $currentIndex); if (!$mapHtml) { return '
足迹ID ' . $mapId . ' 不存在
'; } // 确保配置正确添加到mapConfigs数组中 if ($config) { self::$mapConfigs[] = $config; } return $mapHtml; } /** * 同步关联文章 * * @access private * @param int $footprintId 足迹ID * @param int $articleCid 文章CID * @return bool 是否成功 */ private static function syncRelatedArticles($footprintId, $articleCid) { try { $db = self::getDbConnection(); // 获取当前的关联文章 $stmt = $db->prepare("SELECT related_articles FROM plugin_track_footprint WHERE id = ?"); $stmt->execute(array($footprintId)); $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result) { $currentRelatedArticles = $result['related_articles']; $relatedArticlesArray = array(); // 解析当前的关联文章 if (!empty($currentRelatedArticles)) { $relatedArticlesArray = explode(',', $currentRelatedArticles); $relatedArticlesArray = array_map('trim', $relatedArticlesArray); $relatedArticlesArray = array_filter($relatedArticlesArray); } // 添加新的文章CID(如果不存在) if (!in_array($articleCid, $relatedArticlesArray)) { $relatedArticlesArray[] = $articleCid; $newRelatedArticles = implode(',', $relatedArticlesArray); // 更新数据库 $updateStmt = $db->prepare("UPDATE plugin_track_footprint SET related_articles = ? WHERE id = ?"); $updateStmt->execute(array($newRelatedArticles, $footprintId)); return true; } } } catch (Exception $e) { error_log('MyTrack: 同步关联文章失败: ' . $e->getMessage()); } return false; } /** * 文章保存时的处理 * * @access public * @param array $content 文章数据 * @return array */ public static function onPostSave($content) { // 获取当前文章CID $cid = isset($content['cid']) ? $content['cid'] : 0; if ($cid && isset($content['text'])) { // 解析文章内容中的地图短代码,并验证足迹ID是否存在 $pattern = '/\{map-(\d+)\}/i'; if (preg_match_all($pattern, $content['text'], $matches)) { foreach ($matches[1] as $mapId) { $footprint = self::getFootprintById($mapId); if ($footprint) { // 更新足迹的关联文章字段 self::syncRelatedArticles($mapId, $cid); } } } } return $content; } /** * 文章写入时的处理 * * @access public * @param array $content 文章数据 * @return array */ public static function onPostWrite($content) { return self::onPostSave($content); } }