1855 lines
66 KiB
PHP
1855 lines
66 KiB
PHP
<?php
|
||
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
||
|
||
/**
|
||
* 地点卡片&足迹地图
|
||
*
|
||
* @package MyTrack
|
||
* @author 石头厝
|
||
* @version 1.1.3
|
||
* @link https://www.shitoucuo.com/
|
||
*/
|
||
class MyTrack_Plugin implements Typecho_Plugin_Interface
|
||
{
|
||
/**
|
||
* 数据库文件路径
|
||
*/
|
||
private static $dbPath;
|
||
|
||
/**
|
||
* 是否已添加CSS样式
|
||
*/
|
||
private static $cssAdded = false;
|
||
|
||
/**
|
||
* 是否已添加JS脚本
|
||
*/
|
||
private static $jsAdded = false;
|
||
|
||
/**
|
||
* 地图配置数组
|
||
*/
|
||
private static $mapConfigs = array();
|
||
|
||
/**
|
||
* 获取插件版本号
|
||
*
|
||
* @access public
|
||
* @return string 版本号
|
||
*/
|
||
public static function getVersion()
|
||
{
|
||
// 读取插件文件头部注释中的版本信息
|
||
$pluginFile = __FILE__;
|
||
$content = file_get_contents($pluginFile);
|
||
|
||
// 使用正则表达式匹配@version标签
|
||
if (preg_match('/@version\s+([0-9]+\.[0-9]+\.[0-9]+)/', $content, $matches)) {
|
||
return $matches[1];
|
||
}
|
||
|
||
// 如果无法从注释中获取,返回默认版本
|
||
return '1.1.3';
|
||
}
|
||
|
||
/**
|
||
* 激活插件方法
|
||
*
|
||
* @access public
|
||
* @return void
|
||
* @throws Typecho_Plugin_Exception
|
||
*/
|
||
public static function activate()
|
||
{
|
||
// 初始化数据库路径
|
||
self::initDbPath();
|
||
|
||
// 创建数据库表
|
||
self::initDatabase();
|
||
|
||
// 检查并执行数据库迁移
|
||
self::migrateDatabase();
|
||
|
||
// 添加后台管理菜单 - 使用类型1(管理菜单)与UserCard插件保持一致
|
||
Helper::addPanel(3, 'MyTrack/Manage.php', '足迹管理', '足迹管理', 'administrator');
|
||
|
||
// 添加动作处理
|
||
Helper::addAction('track', 'MyTrack_Action');
|
||
|
||
// 注册路由
|
||
Helper::addRoute('track_action', '/action/track', 'MyTrack_Action', 'action');
|
||
|
||
// 注册短代码解析
|
||
Typecho_Plugin::factory('Widget_Abstract_Contents')->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 '<style>' . self::getMapCardStyles() . '</style>';
|
||
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('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $text, $matches1);
|
||
if (isset($matches1[1]) && !empty($matches1[1])) {
|
||
foreach($matches1[1] as $imageUrl) {
|
||
$images[] = self::processImageUrl($imageUrl);
|
||
}
|
||
}
|
||
|
||
// 模式2: markdown图片语法 
|
||
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], '<div class="mytrack-error">足迹ID ' . $mapId . ' 不存在</div>', $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 .= '<span class="mytrack-categories-badge ' . $class . '">' . $text . '</span>';
|
||
}
|
||
}
|
||
|
||
// 处理星级显示
|
||
$ratingHtml = '';
|
||
if (!empty($footprint['rating_level']) && $footprint['rating_level'] > 0) {
|
||
$ratingHtml = '<div class="mytrack-rating-display">';
|
||
for ($i = 1; $i <= 5; $i++) {
|
||
if ($i <= $footprint['rating_level']) {
|
||
$ratingHtml .= '<span class="mytrack-rating-star">★</span>';
|
||
} else {
|
||
$ratingHtml .= '<span class="mytrack-rating-empty-star">☆</span>';
|
||
}
|
||
}
|
||
$ratingHtml .= '</div>';
|
||
}
|
||
|
||
// 处理亮点显示
|
||
$highlightsHtml = '';
|
||
if (!empty($footprint['highlights'])) {
|
||
$highlightsHtml = '<div class="mytrack-card-highlights"><strong>亮点:</strong>';
|
||
foreach ($footprint['highlights'] as $highlight) {
|
||
$highlight = trim($highlight);
|
||
if ($highlight) {
|
||
$highlightsHtml .= '<span class="mytrack-highlight-badge">' . htmlspecialchars($highlight) . '</span> ';
|
||
}
|
||
}
|
||
$highlightsHtml .= '</div>';
|
||
}
|
||
|
||
// 处理关联文章显示
|
||
$relatedArticlesHtml = '';
|
||
if (!empty($footprint['related_articles_info'])) {
|
||
$relatedArticlesHtml .= '<div class="mytrack-related-articles-section">';
|
||
$relatedArticlesHtml .= '<strong>这些文章也提到了本地点:</strong><div class="mytrack-related-articles-list">';
|
||
foreach ($footprint['related_articles_info'] as $articleInfo) {
|
||
if (!empty($articleInfo['title']) && !empty($articleInfo['link'])) {
|
||
$relatedArticlesHtml .= '<div class="mytrack-related-article-item"><a href="' . $articleInfo['link'] . '" target="_blank">' . htmlspecialchars($articleInfo['title']) . '</a></div>';
|
||
}
|
||
}
|
||
$relatedArticlesHtml .= '</div></div>';
|
||
}
|
||
|
||
// 生成唯一ID
|
||
$uniqueId = 'map_' . $mapId . '_' . $index;
|
||
$mapCardId = 'mytrack-card-' . $uniqueId;
|
||
$mapContainerId = $mapCardId . '-map';
|
||
|
||
// 构建HTML
|
||
$html = '
|
||
<div class="mytrack-card" id="' . $mapCardId . '">
|
||
<div class="mytrack-card-content">
|
||
<div class="mytrack-card-info">
|
||
<div class="mytrack-card-header">
|
||
<h3 class="mytrack-card-title">' . htmlspecialchars($footprint['name']) . '</h3>
|
||
</div>';
|
||
|
||
if ($footprint['address']) {
|
||
$html .= '<div class="mytrack-card-address"><strong>地址:</strong>' . htmlspecialchars($footprint['address']) . '</div>';
|
||
}
|
||
|
||
if ($footprint['location_type']) {
|
||
$html .= '<div class="mytrack-card-type"><strong>类型:</strong>' . htmlspecialchars($footprint['location_type']) . '</div>';
|
||
}
|
||
|
||
if ($categoriesHtml) {
|
||
$html .= '<div class="mytrack-card-categories"><strong>分类:</strong>' . $categoriesHtml . '</div>';
|
||
}
|
||
|
||
if ($ratingHtml) {
|
||
$html .= '<div class="mytrack-card-rating"><strong>推荐:</strong>' . $ratingHtml . '</div>';
|
||
}
|
||
|
||
$html .= $highlightsHtml;
|
||
|
||
if ($footprint['review']) {
|
||
$html .= '<div class="mytrack-card-review"><strong>简评:</strong>' . nl2br(htmlspecialchars($footprint['review'])) . '</div>';
|
||
}
|
||
|
||
$html .= $relatedArticlesHtml;
|
||
|
||
$html .= '</div>
|
||
<div class="mytrack-card-map" id="' . $mapContainerId . '" data-map-id="' . $uniqueId . '" style="width: ' . $cardMapWidth . '; height: ' . $cardMapHeight . ';"></div>
|
||
</div>
|
||
</div>';
|
||
|
||
// 准备地图配置
|
||
$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 '
|
||
<script>
|
||
(function() {
|
||
var configs = ' . $configsJson . ';
|
||
for (var i = 0; i < configs.length; i++) {
|
||
var mapElement = document.getElementById(configs[i].containerId);
|
||
if (mapElement) {
|
||
mapElement.innerHTML = \'<div style="padding:20px;color:#721c24;background:#f8d7da;text-align:center;">请在高德地图官网申请API密钥并配置</div>\';
|
||
}
|
||
}
|
||
})();
|
||
</script>';
|
||
}
|
||
|
||
return '
|
||
<script>
|
||
(function() {
|
||
// 等待页面完全加载
|
||
function initMyTrackMaps() {
|
||
var configs = ' . $configsJson . ';
|
||
|
||
if (!configs || configs.length === 0) return;
|
||
|
||
// 加载高德地图API
|
||
if (typeof AMap === "undefined") {
|
||
var script = document.createElement("script");
|
||
script.src = "https://webapi.amap.com/maps?v=2.0&key=' . $apiKey . '&plugin=AMap.Scale";
|
||
script.onload = function() {
|
||
setTimeout(createMaps, 100);
|
||
};
|
||
script.onerror = function() {
|
||
showAllErrors("地图API加载失败,请检查API密钥");
|
||
};
|
||
document.head.appendChild(script);
|
||
} else {
|
||
setTimeout(createMaps, 100);
|
||
}
|
||
|
||
function createMaps() {
|
||
if (typeof AMap === "undefined") {
|
||
showAllErrors("地图API未加载");
|
||
return;
|
||
}
|
||
|
||
for (var i = 0; i < configs.length; i++) {
|
||
createMap(configs[i]);
|
||
}
|
||
}
|
||
|
||
function createMap(config) {
|
||
var mapElement = document.getElementById(config.containerId);
|
||
if (!mapElement) return;
|
||
|
||
try {
|
||
// 检查容器尺寸
|
||
if (mapElement.offsetWidth === 0 || mapElement.offsetHeight === 0) {
|
||
setTimeout(function() { createMap(config); }, 100);
|
||
return;
|
||
}
|
||
|
||
// 确定主题
|
||
var isDark = document.documentElement.classList.contains("dark");
|
||
var mapStyle = isDark ? "amap://styles/dark" : "amap://styles/" + (config.theme || "normal");
|
||
|
||
// 创建地图时隐藏logo和版权信息
|
||
var map = new AMap.Map(mapElement, {
|
||
zoom: config.zoom || 16,
|
||
center: [config.longitude, config.latitude],
|
||
mapStyle: mapStyle,
|
||
resizeEnable: true,
|
||
logo: false // 禁用logo
|
||
});
|
||
|
||
// 添加标记点 - 修复:优先使用后台设置的markerColor
|
||
var markerContent = \'\';
|
||
if (config.markerColor && config.markerColor.trim() !== \'\') {
|
||
// 有自定义标记颜色,使用对应的颜色类
|
||
markerContent = \'<div class="mytrack-marker-custom mytrack-marker-\' + config.markerColor + \'"></div>\';
|
||
} else {
|
||
// 没有自定义颜色,使用默认红色
|
||
markerContent = \'<div class="mytrack-marker-custom" style="background: \' + (config.markerColorValue || \'#ff0000\') + \' !important;"></div>\';
|
||
}
|
||
|
||
var marker = new AMap.Marker({
|
||
position: [config.longitude, config.latitude],
|
||
content: markerContent,
|
||
title: config.name || "地点",
|
||
offset: new AMap.Pixel(-10, -10)
|
||
});
|
||
|
||
map.add(marker);
|
||
|
||
// 为深色模式调整标记点边框
|
||
if (isDark) {
|
||
try {
|
||
var markerElement = mapElement.querySelector(".mytrack-marker-custom");
|
||
if (markerElement) {
|
||
markerElement.style.border = "2px solid #333 !important";
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 创建后强制隐藏版权信息
|
||
hideCopyrightInfo(mapElement);
|
||
|
||
// 定期检查并隐藏版权信息
|
||
var hideInterval = setInterval(function() {
|
||
hideCopyrightInfo(mapElement);
|
||
}, 500);
|
||
|
||
// 5秒后停止检查
|
||
setTimeout(function() {
|
||
clearInterval(hideInterval);
|
||
}, 5000);
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener("resize", function() {
|
||
setTimeout(function() {
|
||
map && map.resize();
|
||
}, 100);
|
||
});
|
||
|
||
// 监听主题变化
|
||
if (typeof MutationObserver !== "undefined") {
|
||
var observer = new MutationObserver(function() {
|
||
var isDarkNow = document.documentElement.classList.contains("dark");
|
||
var newMapStyle = isDarkNow ? "amap://styles/dark" : "amap://styles/" + (config.theme || "normal");
|
||
try {
|
||
map.setMapStyle(newMapStyle);
|
||
// 主题变化时调整标记点边框
|
||
var markerElement = mapElement.querySelector(".mytrack-marker-custom");
|
||
if (markerElement) {
|
||
markerElement.style.border = isDarkNow ? "2px solid #333 !important" : "2px solid #fff !important";
|
||
}
|
||
} catch (e) {}
|
||
});
|
||
|
||
observer.observe(document.documentElement, { attributes: true });
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error("MyTrack: 创建地图失败", e);
|
||
if (mapElement) {
|
||
mapElement.innerHTML = \'<div style="padding:20px;color:#721c24;background:#f8d7da;text-align:center;">地图加载失败</div>\';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 隐藏版权信息的函数
|
||
function hideCopyrightInfo(mapElement) {
|
||
if (!mapElement) return;
|
||
|
||
try {
|
||
// 方法1: 直接查找并隐藏
|
||
var logos = mapElement.querySelectorAll(".amap-logo, .amap-copyright");
|
||
logos.forEach(function(logo) {
|
||
if (logo) {
|
||
logo.style.display = "none";
|
||
logo.style.visibility = "hidden";
|
||
logo.style.opacity = "0";
|
||
}
|
||
});
|
||
|
||
// 方法2: 查找所有可能包含版权信息的元素
|
||
var allElements = mapElement.querySelectorAll("*");
|
||
allElements.forEach(function(el) {
|
||
if (el.innerHTML && (el.innerHTML.includes("高德") || el.innerHTML.includes("Amap"))) {
|
||
el.style.display = "none";
|
||
el.style.visibility = "hidden";
|
||
el.style.opacity = "0";
|
||
}
|
||
});
|
||
|
||
// 方法3: 查找具有特定样式的元素
|
||
var styledElements = mapElement.querySelectorAll("[style*=\'position: absolute\']");
|
||
styledElements.forEach(function(el) {
|
||
var style = el.getAttribute("style") || "";
|
||
if (style.includes("bottom") && (style.includes("0px") || style.includes("right"))) {
|
||
el.style.display = "none";
|
||
el.style.visibility = "hidden";
|
||
el.style.opacity = "0";
|
||
}
|
||
});
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
}
|
||
|
||
function showAllErrors(message) {
|
||
for (var i = 0; i < configs.length; i++) {
|
||
var mapElement = document.getElementById(configs[i].containerId);
|
||
if (mapElement) {
|
||
mapElement.innerHTML = \'<div style="padding:20px;color:#721c24;background:#f8d7da;text-align:center;">\' + message + \'</div>\';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", initMyTrackMaps);
|
||
} else {
|
||
setTimeout(initMyTrackMaps, 300);
|
||
}
|
||
})();
|
||
</script>';
|
||
}
|
||
|
||
/**
|
||
* 原来的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 '<div class="mytrack-error">足迹ID ' . $mapId . ' 不存在</div>';
|
||
}
|
||
|
||
// 确保配置正确添加到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);
|
||
}
|
||
} |