Files
MyTrack/Widget.php
2026-02-23 19:45:59 +08:00

2330 lines
76 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
* MyTrack 前台足迹地图小部件
*
* @package MyTrack
*/
class MyTrack_Widget extends Typecho_Widget implements Widget_Interface_Do
{
/**
* 数据库连接
*
* @access private
* @var PDO
*/
private $db;
/**
* 插件配置
*
* @access private
* @var Typecho_Config
*/
private $options;
/**
* 缓存目录
*
* @access private
* @var string
*/
private $cacheDir;
/**
* 缓存过期时间(秒)
*
* @access private
* @var int
*/
private $cacheExpire;
/**
* 构造函数
*
* @access public
* @param mixed $request request对象
* @param mixed $response response对象
* @param mixed $params 参数列表
* @return void
*/
public function __construct($request, $response, $params = NULL)
{
parent::__construct($request, $response, $params);
$this->db = MyTrack_Plugin::getDbConnection();
$this->options = Typecho_Widget::widget('Widget_Options')->plugin('MyTrack');
// 初始化缓存相关属性
$this->initCache();
}
/**
* 初始化缓存
*
* @access private
* @return void
*/
private function initCache()
{
$this->cacheDir = __DIR__ . '/cache';
// 确保缓存目录存在
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
// 从插件配置中获取缓存时间默认为7天
try {
if ($this->options && isset($this->options->cacheExpire)) {
$cacheDays = $this->options->cacheExpire;
// 确保是有效的数字如果不是则使用默认值7天
$cacheDays = is_numeric($cacheDays) && $cacheDays >= 0 ? intval($cacheDays) : 7;
// 将天数转换为秒数0表示关闭缓存
$this->cacheExpire = $cacheDays * 24 * 60 * 60;
} else {
// 如果获取配置失败使用默认值7天
$this->cacheExpire = 7 * 24 * 60 * 60;
}
} catch (Exception $e) {
// 如果获取配置失败使用默认值7天
$this->cacheExpire = 7 * 24 * 60 * 60;
}
}
/**
* 获取缓存键名
*
* @access private
* @param string $key 缓存键
* @return string 完整的缓存文件路径
*/
private function getCacheFile($key)
{
return $this->cacheDir . '/' . md5($key) . '.cache';
}
/**
* 获取缓存数据
*
* @access private
* @param string $key 缓存键
* @return mixed|null 缓存数据或null
*/
private function getCache($key)
{
// 如果缓存时间为0表示关闭缓存
if ($this->cacheExpire === 0) {
return null;
}
$cacheFile = $this->getCacheFile($key);
if (!file_exists($cacheFile)) {
return null;
}
// 检查缓存是否过期
if (filemtime($cacheFile) + $this->cacheExpire < time()) {
unlink($cacheFile);
return null;
}
$data = file_get_contents($cacheFile);
return unserialize($data);
}
/**
* 设置缓存数据
*
* @access private
* @param string $key 缓存键
* @param mixed $data 要缓存的数据
* @return bool 是否成功
*/
private function setCache($key, $data)
{
// 如果缓存时间为0表示关闭缓存
if ($this->cacheExpire === 0) {
return false;
}
$cacheFile = $this->getCacheFile($key);
$data = serialize($data);
return file_put_contents($cacheFile, $data) !== false;
}
/**
* 清除缓存
*
* @access private
* @param string|null $key 特定缓存键null表示清除所有缓存
* @return bool 是否成功
*/
private function clearCache($key = null)
{
if ($key === null) {
// 清除所有缓存
$files = glob($this->cacheDir . '/*.cache');
foreach ($files as $file) {
unlink($file);
}
return true;
} else {
// 清除特定缓存
$cacheFile = $this->getCacheFile($key);
if (file_exists($cacheFile)) {
return unlink($cacheFile);
}
return true;
}
}
/**
* 执行函数
*
* @access public
* @return void
*/
public function execute()
{
// 检查是否启用前台显示
if (!$this->options || !$this->options->enableDisplay) {
return;
}
}
/**
* 动作处理
*
* @access public
* @return void
*/
public function action()
{
// 处理前台API请求
$action = $this->request->get('do');
if ($action) {
switch ($action) {
case 'getAll':
$this->getAll();
break;
case 'clearCache':
$this->clearAllCache();
break;
default:
$this->response->throwJson(array(
'success' => false,
'message' => '未知操作'
));
}
} else {
$this->response->throwJson(array(
'success' => false,
'message' => '缺少操作参数'
));
}
}
/**
* 清除所有缓存
*
* @access private
* @return void
*/
private function clearAllCache()
{
try {
$this->clearCache();
$this->response->throwJson(array(
'success' => true,
'message' => '缓存已清除'
));
} catch (Exception $e) {
$this->response->throwJson(array(
'success' => false,
'message' => '清除缓存失败: ' . $e->getMessage()
));
}
}
/**
* 输出足迹地图
*
* @access public
* @param string|null $externalTheme 外部传入的主题参数
* @return void
*/
public function render($externalTheme = null)
{
// 检查是否启用前台显示
if (!$this->options || !$this->options->enableDisplay) {
return;
}
// 获取API密钥 - 使用JS API密钥
$apiKey = isset($this->options->jsApiKey) ? $this->options->jsApiKey : '';
if (empty($apiKey)) {
echo '<div class="mytrack-error">请先在插件设置中配置高德地图JS API密钥</div>';
return;
}
// 获取配置
$zoomLevel = $this->options->zoomLevel ?: '15';
$viewMode = $this->options->viewMode ?: '2D';
// 优先使用外部传入的主题参数,如果没有则使用后台设置
$mapTheme = $externalTheme ?: ($this->options->mapTheme ?: 'normal');
$enableCluster = isset($this->options->enableCluster) ? $this->options->enableCluster : 0;
$emptyMarkerColor = $this->options->emptyMarkerColor ?: '#2196F3';
$contentMarkerColor = $this->options->contentMarkerColor ?: '#4CAF50';
$clusterMarkerColor1 = $this->options->clusterMarkerColor1 ?: '#2196F3';
$clusterMarkerColor2 = $this->options->clusterMarkerColor2 ?: '#FF9800';
$clusterMarkerColor3 = $this->options->clusterMarkerColor3 ?: '#FF5722';
// 输出地图容器和筛选器容器
echo <<<HTML
<div id="mytrack-map-container" class="mytrack-map-container mytrack-theme-{$mapTheme}">
<!-- 分类筛选器 -->
<div id="mytrack-filters" class="mytrack-filters"></div>
<!-- 地图容器 -->
<div id="mytrack-map" class="mytrack-map"></div>
<!-- 右侧工具栏 -->
<div id="mytrack-toolbar" class="mytrack-toolbar">
<div class="mytrack-toolbar-group">
<button id="mytrack-fullscreen-btn" class="mytrack-toolbar-btn" title="全屏">
<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
</button>
<button id="mytrack-reset-btn" class="mytrack-toolbar-btn" title="重置视图">
<svg viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
</button>
</div>
<div class="mytrack-toolbar-group">
<button id="mytrack-zoom-in-btn" class="mytrack-toolbar-btn" title="放大">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
<button id="mytrack-zoom-out-btn" class="mytrack-toolbar-btn" title="缩小">
<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>
</button>
</div>
</div>
<!-- 集群开关 -->
<div id="mytrack-cluster-toggle" class="mytrack-cluster-toggle">
<span class="mytrack-cluster-label">分散聚合</span>
<button id="mytrack-cluster-switch" class="mytrack-cluster-switch is-on">
<span class="mytrack-cluster-knob"></span>
</button>
</div>
</div>
<div id="mytrack-info-panel" class="mytrack-info-panel mytrack-theme-{$mapTheme}" style="display: none;">
<div class="mytrack-info-header">
<h3 id="mytrack-info-title" class="mytrack-card-title"></h3>
<button id="mytrack-info-close" class="mytrack-close-btn">&times;</button>
</div>
<div class="mytrack-info-content">
<div id="mytrack-info-meta" class="mytrack-card-info"></div>
<div id="mytrack-info-review" class="mytrack-card-review" style="display:none;"></div>
<div id="mytrack-info-images" class="mytrack-info-images" style="display:none;"></div>
</div>
</div>
HTML;
// 输出JavaScript
$this->outputMapScript($apiKey, $zoomLevel, $viewMode, $mapTheme, $enableCluster, $emptyMarkerColor, $contentMarkerColor, $clusterMarkerColor1, $clusterMarkerColor2, $clusterMarkerColor3);
}
/**
* 获取所有足迹数据
*
* @access public
* @return void
*/
public function getAll()
{
try {
// 先检查缓存
$cacheKey = 'mytrack_footprints_data';
$cachedData = $this->getCache($cacheKey);
if ($cachedData !== null) {
// 缓存命中,直接返回缓存数据
$this->response->throwJson(array(
'success' => true,
'data' => $cachedData,
'from_cache' => true
));
return;
}
// 缓存未命中,查询数据库
$stmt = $this->db->query("SELECT
id,
latitude,
longitude,
name,
address,
location_type,
rating_level,
categories,
review,
description,
article_cid,
urlLabel,
url,
photos,
tags,
date,
markerColor,
related_articles,
highlights,
created_at,
updated_at
FROM plugin_track_footprint
ORDER BY date ASC, created_at ASC");
$footprints = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 过滤掉经纬度无效的数据
$validFootprints = array_filter($footprints, function($footprint) {
$longitude = is_numeric($footprint['longitude']) ? floatval($footprint['longitude']) : null;
$latitude = is_numeric($footprint['latitude']) ? floatval($footprint['latitude']) : null;
return $longitude !== null && $latitude !== null &&
!is_nan($longitude) && !is_nan($latitude) &&
$longitude >= -180 && $longitude <= 180 &&
$latitude >= -90 && $latitude <= 90;
});
// 重新索引数组
$validFootprints = array_values($validFootprints);
// 处理每个足迹
foreach ($validFootprints as &$footprint) {
if (!empty($footprint['article_cid'])) {
$articleInfo = MyTrack_Plugin::getArticleInfo($footprint['article_cid']);
if ($articleInfo) {
if (empty($footprint['urlLabel']) && !empty($articleInfo['title'])) {
$footprint['urlLabel'] = $articleInfo['title'];
}
if (empty($footprint['url']) && !empty($articleInfo['link'])) {
$footprint['url'] = $articleInfo['link'];
}
}
}
if (!empty($footprint['related_articles'])) {
$relatedArticlesInfo = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
if (!empty($relatedArticlesInfo)) {
$simpleArticles = array();
foreach ($relatedArticlesInfo as $cid => $info) {
$simpleArticles[] = array(
'cid' => $cid,
'title' => $info['title'] ?? '',
'link' => $info['link'] ?? ''
);
}
$footprint['related_articles_info'] = $simpleArticles;
} else {
$footprint['related_articles_info'] = array();
}
} else {
$footprint['related_articles_info'] = array();
}
// 确保字段有默认值
if (!isset($footprint['address'])) $footprint['address'] = '';
if (!isset($footprint['location_type'])) $footprint['location_type'] = '';
if (!isset($footprint['rating_level'])) $footprint['rating_level'] = 0;
if (!isset($footprint['categories'])) $footprint['categories'] = '';
if (!isset($footprint['review'])) $footprint['review'] = '';
if (!isset($footprint['markerColor'])) $footprint['markerColor'] = '';
if (!isset($footprint['related_articles'])) $footprint['related_articles'] = '';
if (!isset($footprint['highlights'])) $footprint['highlights'] = '';
$footprint['rating_level'] = intval($footprint['rating_level']);
// 重要保持categories为英文数组不翻译
if (!empty($footprint['categories'])) {
$categoriesArray = explode(',', $footprint['categories']);
$footprint['categories'] = array_map('trim', $categoriesArray);
} else {
$footprint['categories'] = array();
}
}
// 将数据存入缓存
if ($this->cacheExpire > 0) {
$this->setCache($cacheKey, $validFootprints);
}
$this->response->throwJson(array(
'success' => true,
'data' => $validFootprints,
'from_cache' => false
));
} catch (Exception $e) {
$this->response->throwJson(array(
'success' => false,
'message' => '查询失败: ' . $e->getMessage()
));
}
}
/**
* 将分类从英文翻译为中文
*
* @access private
* @param string $category 英文分类
* @return string 中文分类
*/
private function translateCategoryToChinese($category)
{
$translations = array(
'plan' => '计划',
'Plan' => '计划',
'want' => '向往',
'Want' => '向往',
'visited' => '已玩',
'Visited' => '已玩',
'wish' => '想去',
'Wish' => '想去',
'todo' => '待做',
'Todo' => '待做',
'done' => '完成',
'Done' => '完成',
'travel' => '旅行',
'Travel' => '旅行',
'food' => '美食',
'Food' => '美食',
'shopping' => '购物',
'Shopping' => '购物',
'entertainment' => '娱乐',
'Entertainment' => '娱乐',
'culture' => '文化',
'Culture' => '文化',
'nature' => '自然',
'Nature' => '自然',
'cafe' => '咖啡',
'Cafe' => '咖啡',
'restaurant' => '餐厅',
'Restaurant' => '餐厅',
'hotel' => '酒店',
'Hotel' => '酒店',
'shop' => '商店',
'Shop' => '商店',
'attraction' => '景点',
'Attraction' => '景点',
'park' => '公园',
'Park' => '公园',
'museum' => '博物馆',
'Museum' => '博物馆'
);
$categoryLower = strtolower(trim($category));
foreach ($translations as $key => $value) {
if (strtolower($key) === $categoryLower) {
return $value;
}
}
return $category;
}
/**
* 输出内容
*
* @access public
* @param string|null $externalTheme 外部传入的主题参数
* @return void
*/
public static function output($externalTheme = null)
{
$widget = new self(
Typecho_Widget::widget('Widget_Options')->request,
Typecho_Widget::widget('Widget_Options')->response
);
$widget->render($externalTheme);
}
/**
* 获取足迹数据(服务器端获取)
*
* @access private
* @return array 足迹数据数组
*/
private function getFootprintsData()
{
try {
$cacheKey = 'mytrack_footprints_data';
$cachedData = $this->getCache($cacheKey);
if ($cachedData !== null) {
return array(
'success' => true,
'data' => $cachedData,
'from_cache' => true
);
}
$stmt = $this->db->query("SELECT
id,
latitude,
longitude,
name,
address,
location_type,
rating_level,
categories,
review,
description,
article_cid,
urlLabel,
url,
photos,
tags,
date,
markerColor,
related_articles,
highlights,
created_at,
updated_at
FROM plugin_track_footprint
ORDER BY date ASC, created_at ASC");
$footprints = $stmt->fetchAll(PDO::FETCH_ASSOC);
$validFootprints = array_filter($footprints, function($footprint) {
$longitude = is_numeric($footprint['longitude']) ? floatval($footprint['longitude']) : null;
$latitude = is_numeric($footprint['latitude']) ? floatval($footprint['latitude']) : null;
return $longitude !== null && $latitude !== null &&
!is_nan($longitude) && !is_nan($latitude) &&
$longitude >= -180 && $longitude <= 180 &&
$latitude >= -90 && $latitude <= 90;
});
$validFootprints = array_values($validFootprints);
foreach ($validFootprints as &$footprint) {
if (!empty($footprint['article_cid'])) {
$articleInfo = MyTrack_Plugin::getArticleInfo($footprint['article_cid']);
if ($articleInfo) {
if (empty($footprint['urlLabel']) && !empty($articleInfo['title'])) {
$footprint['urlLabel'] = $articleInfo['title'];
}
if (empty($footprint['url']) && !empty($articleInfo['link'])) {
$footprint['url'] = $articleInfo['link'];
}
}
}
if (!empty($footprint['related_articles'])) {
$relatedArticlesInfo = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
if (!empty($relatedArticlesInfo)) {
$simpleArticles = array();
foreach ($relatedArticlesInfo as $cid => $info) {
$simpleArticles[] = array(
'cid' => $cid,
'title' => $info['title'] ?? '',
'link' => $info['link'] ?? ''
);
}
$footprint['related_articles_info'] = $simpleArticles;
} else {
$footprint['related_articles_info'] = array();
}
} else {
$footprint['related_articles_info'] = array();
}
if (!isset($footprint['address'])) $footprint['address'] = '';
if (!isset($footprint['location_type'])) $footprint['location_type'] = '';
if (!isset($footprint['rating_level'])) $footprint['rating_level'] = 0;
if (!isset($footprint['categories'])) $footprint['categories'] = '';
if (!isset($footprint['review'])) $footprint['review'] = '';
if (!isset($footprint['markerColor'])) $footprint['markerColor'] = '';
if (!isset($footprint['related_articles'])) $footprint['related_articles'] = '';
if (!isset($footprint['highlights'])) $footprint['highlights'] = '';
$footprint['rating_level'] = intval($footprint['rating_level']);
// 重要保持categories为英文数组不翻译
if (!empty($footprint['categories'])) {
$categoriesArray = explode(',', $footprint['categories']);
$footprint['categories'] = array_map('trim', $categoriesArray);
} else {
$footprint['categories'] = array();
}
}
if ($this->cacheExpire > 0) {
$this->setCache($cacheKey, $validFootprints);
}
return array(
'success' => true,
'data' => $validFootprints,
'from_cache' => false
);
} catch (Exception $e) {
return array(
'success' => false,
'message' => '查询失败: ' . $e->getMessage(),
'data' => array(),
'from_cache' => false
);
}
}
/**
* 输出地图脚本
*
* @access private
* @param string $apiKey API密钥
* @param string $zoomLevel 缩放级别
* @param string $viewMode 视图模式
* @param string $mapTheme 地图主题
* @param int $enableCluster 是否启用点聚合
* @param string $emptyMarkerColor 空内容标记点颜色
* @param string $contentMarkerColor 有内容标记点颜色
* @param string $clusterMarkerColor1 聚合标记点颜色1
* @param string $clusterMarkerColor2 聚合标记点颜色2
* @param string $clusterMarkerColor3 聚合标记点颜色3
* @return void
*/
private function outputMapScript($apiKey, $zoomLevel, $viewMode, $mapTheme, $enableCluster, $emptyMarkerColor, $contentMarkerColor, $clusterMarkerColor1, $clusterMarkerColor2, $clusterMarkerColor3)
{
$version = MyTrack_Plugin::getVersion();
$emptyMarkerRgb = $this->hexToRgb($emptyMarkerColor);
$contentMarkerRgb = $this->hexToRgb($contentMarkerColor);
$clusterMarkerRgb1 = $this->hexToRgb($clusterMarkerColor1);
$clusterMarkerRgb2 = $this->hexToRgb($clusterMarkerColor2);
$clusterMarkerRgb3 = $this->hexToRgb($clusterMarkerColor3);
$footprintsData = $this->getFootprintsData();
$footprintsJson = json_encode($footprintsData, JSON_UNESCAPED_UNICODE);
// 输出CSS - 移植深色模式适配
echo <<<HTML
<style>
/* AMap Overrides */
.amap-logo, .amap-copyright {
display: none !important;
}
/* Reset InfoWindow Styles */
.amap-info-content,
.amap-info-outer,
.amap-info-inner {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
}
.amap-info-sharp,
.amap-info-shadow {
display: none !important;
}
.amap-scale-text,
.amap-scalecontrol {
background: none !important;
border: none !important;
box-shadow: none !important;
}
/* 主容器 */
.mytrack-map-container {
width: 100%;
min-height: 420px;
border-radius: 12px;
overflow: hidden;
margin: 1.5rem 0;
position: relative;
background: #fff;
z-index: 1;
}
.mytrack-map {
width: 100%;
height: 100%;
outline: none;
}
.mytrack-map-container.is-fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 99999 !important;
margin: 0 !important;
border-radius: 0 !important;
background: #fff;
}
/* 分类筛选器样式 - 重构 */
.mytrack-filters {
position: absolute;
top: 6px;
left: 6px;
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
z-index: 3;
max-width: calc(100% - 24px);
}
.mytrack-filter-btn {
border: none;
background: transparent;
color: #333;
font-size: 0.85rem;
padding: 6px 12px;
border-radius: 18px;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
white-space: nowrap;
}
.mytrack-filter-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.mytrack-filter-btn.is-active {
background: #f15a22;
color: #fff;
}
/* 右侧工具栏样式 - 优化 */
.mytrack-toolbar {
position: absolute;
top: 20px;
right: 20px;
z-index: 9;
display: flex;
flex-direction: column;
gap: 12px;
}
.mytrack-toolbar-group {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mytrack-toolbar-btn {
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
}
.mytrack-toolbar-btn:hover {
background: #f5f5f5;
border-color: #ccc;
transform: translateY(-1px);
}
.mytrack-toolbar-btn svg {
width: 20px;
height: 20px;
fill: #666;
}
.mytrack-toolbar-btn:hover svg {
fill: #333;
}
/* 集群开关样式 */
.mytrack-cluster-toggle {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-size: 13px;
user-select: none;
transition: all 0.3s ease;
}
.mytrack-cluster-label {
font-weight: 500;
color: #333;
transition: color 0.3s ease;
}
.mytrack-cluster-switch {
position: relative;
width: 44px;
height: 24px;
border: none;
border-radius: 12px;
cursor: pointer;
transition: background 0.3s ease;
outline: none;
background: #f15a22;
padding: 0;
}
.mytrack-cluster-switch.is-off {
background: #ccc;
}
.mytrack-cluster-knob {
position: absolute;
top: 2px;
left: 22px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: left 0.3s ease;
}
.mytrack-cluster-switch.is-off .mytrack-cluster-knob {
left: 2px;
}
/* 信息面板样式 */
.mytrack-info-tags{display:none;}
.mytrack-map-container { width: 100%; height: 500px; margin: 20px 0; position: relative; }
.mytrack-map { width: 100%; height: 100%; border-radius:10px;}
.mytrack-info-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-width: 90%; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 1000; }
.mytrack-info-header { padding: 25px 20px 5px 20px; display: flex; justify-content: space-between; align-items: center; }
.mytrack-info-header h3 { margin: 0; font-size: 18px; font-weight: 600; color: #333; flex: 1; }
.mytrack-close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #999; }
.mytrack-info-content { padding:0 20px 20px; max-height: 400px; overflow-y: auto; }
.mytrack-card-info {
flex: 1;
min-width: 0;
padding:0 20px!important;
}
.mytrack-card-info > div {
margin-bottom: 10px;
font-size: 14px;
line-height: 1.5;
}
.dark .mytrack-card-highlights{
color:#ccc;
}
.mytrack-card-info strong {
color: #555;
font-weight: 600;
min-width: 40px;
display: inline-block;
}
.mytrack-card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
flex: 1;
margin-left:20px!important;
}
.mytrack-card-address {
color: #666;
}
.mytrack-card-type {
color: #666;
}
.mytrack-card-categories {
display: flex;
flex-wrap: wrap;
}
.mytrack-categories-badge {
display: inline-block;
font-size: 14px;
margin-right: 5px;
margin-bottom: 5px;
color:#666;
}
.mytrack-categories-cafe-badge {
background-color: #e8f4fd;
color: #0d6efd;
border: 1px solid #b3d7ff;
}
.dark .mytrack-categories-badge{
color:#ccc;
background: #333;
}
.mytrack-categories-restaurant-badge {
background-color: #f8f9fa;
color: #6c757d;
border: 1px solid #dee2e6;
}
.mytrack-categories-hotel-badge {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.mytrack-categories-shop-badge {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.mytrack-categories-attraction-badge {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.mytrack-categories-food-badge {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.mytrack-categories-travel-badge {
background-color: #cce5ff;
color: #004085;
border: 1px solid #b8daff;
}
.mytrack-card-rating {
display: flex;
align-items: center;
}
.mytrack-stars-inline {
display: inline-flex;
align-items: center;
}
.mytrack-rating-display {
display: inline-flex;
align-items: center;
}
.mytrack-rating-star {
color: #ffc107;
font-size: 14px;
}
.mytrack-rating-empty-star {
color: #e4e5e9;
font-size: 14px;
}
.mytrack-related-articles-section {
margin-top: 10px;
}
.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-review {
color: #444;
border-radius: 6px;
}
.mytrack-info-images {
display: none !important;
}
.mytrack-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; display: none; }
.mytrack-lightbox { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1001; display: none; justify-content: center; align-items: center; cursor: pointer; }
.mytrack-lightbox img { max-width: 90%; max-height: 90%; object-fit: contain; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
.mytrack-lightbox-close { position: absolute; top: 20px; right: 30px; color: white; font-size: 40px; font-weight: bold; cursor: pointer; z-index: 1002; transition: color 0.3s ease; }
.mytrack-lightbox-close:hover { color: #ccc; }
.mytrack-lightbox-nav { position: absolute; top: 50%; transform: translateY(-50%); color: white; font-size: 30px; font-weight: bold; cursor: pointer; padding: 15px; transition: background-color 0.3s ease; border-radius: 50%; width: 30px; height: 30px;line-height: 23px;text-align: center;}
.mytrack-lightbox-nav:hover { background-color: rgba(255,255,255,0.1); }
.mytrack-lightbox-prev { left: 20px; }
.mytrack-lightbox-next { right: 20px; }
.mytrack-info-panel.is-hidden { display: none !important; }
.mytrack-error { padding: 15px; background: #f8d7da; color: #721c24; border-radius: 4px; margin: 20px 0; }
.mytrack-marker-custom {
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
cursor: pointer;
transition: transform 0.2s ease-out;
border: 2px solid #fff;
}
.mytrack-marker-custom:hover {
transform: scale(2.05);
}
.mytrack-marker-sunset { background: linear-gradient(135deg, rgb(255, 179, 71), rgb(255, 111, 97)); }
.mytrack-marker-ocean { background: linear-gradient(135deg, rgb(6, 190, 182), rgb(72, 177, 191)); }
.mytrack-marker-forest { background: linear-gradient(135deg, rgb(94, 231, 223), rgb(57, 163, 124)); }
.mytrack-marker-amber { background: linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133)); }
.mytrack-marker-violet { background: linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235)); }
.mytrack-marker-citrus { background: linear-gradient(135deg, rgb(253, 251, 143), rgb(161, 255, 206)); }
.mytrack-marker-empty {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: rgba({$emptyMarkerRgb['r']}, {$emptyMarkerRgb['g']}, {$emptyMarkerRgb['b']}, 0.7);
border: 2px solid {$emptyMarkerColor};
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
cursor: pointer;
}
.mytrack-marker-with-content {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(180deg, #cf6609, #c00);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
cursor: pointer;
transition: transform 0.2s ease-out;
border: 2px solid #fff;
}
.mytrack-marker-with-content:hover {
transform: scale(2.05);
}
/* ==========================================================================
深色模式适配 - 移植自正常工作版本
========================================================================== */
.dark .mytrack-filters {
background: rgba(40, 40, 40, 0.95);
border-color: #555;
color: #e0e0e0;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.dark .mytrack-filter-btn {
background: transparent;
color: #999;
border: none;
}
.dark .mytrack-filter-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.dark .mytrack-filter-btn.is-active {
background:#f15a22;;
color: #fff;
}
.dark .mytrack-toolbar-group {
background: rgba(40, 40, 40, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.dark .mytrack-toolbar-btn {
background: #333;
border-color: #555;
}
.dark .mytrack-toolbar-btn:hover {
background: #444;
border-color: #666;
}
.dark .mytrack-toolbar-btn svg {
fill: #ccc;
}
.dark .mytrack-toolbar-btn:hover svg {
fill: white;
}
.dark .mytrack-cluster-toggle {
background: rgba(40, 40, 40, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.dark .mytrack-cluster-label {
color: #e0e0e0;
}
.dark .mytrack-cluster-switch {
background: #555;
}
.dark .mytrack-cluster-switch.is-off {
background: #444;
}
.dark .mytrack-cluster-switch.is-on {
background: #f15a22;
}
/* 信息面板深色模式 */
.dark .mytrack-info-panel {
background-color: #333;
}
.dark .mytrack-info-panel .mytrack-card-title {
color: #fff;
}
.dark .mytrack-info-panel strong {
color: #b5b5b5;
}
.dark .mytrack-card-review{
color: #ccc;}
/**.dark .mytrack-info-panel .mytrack-c {
background: #444;
color: #ddd;
border-left-color: #4a90e2;
}**/
.dark .mytrack-info-panel .mytrack-card-address,
.dark .mytrack-info-panel .mytrack-card-type {
color: #ccc;
}
.dark .mytrack-info-panel .mytrack-related-article-item a {
color: #64b5f6;
}
/* 分类徽章深色模式适配 */
.dark .mytrack-info-panel .mytrack-categories-cafe-badge {
background-color: rgba(232, 244, 253, 0.2);
color: #b3d7ff;
border-color: rgba(179, 215, 255, 0.3);
}
/* 移动端适配 */
@media (max-width: 640px) {
.mytrack-filters {
top: 10px;
left: 10px;
max-width: calc(100% - 100px);
max-height: 120px;
overflow-y: auto;
border-radius: 12px;
padding: 6px;
gap: 6px;
}
.mytrack-filter-btn {
padding: 4px 12px;
font-size: 12px;
}
.mytrack-toolbar {
top: 10px;
right: 10px;
gap: 8px;
}
.mytrack-toolbar-group {
padding: 6px;
gap: 6px;
}
.mytrack-toolbar-btn {
width: 32px;
height: 32px;
}
.mytrack-toolbar-btn svg {
width: 18px;
height: 18px;
}
.mytrack-cluster-toggle {
bottom: 10px;
padding: 6px 12px;
font-size: 12px;
}
.mytrack-cluster-switch {
width: 40px;
height: 22px;
}
.mytrack-cluster-knob {
width: 18px;
height: 18px;
top: 2px;
}
.mytrack-cluster-switch .mytrack-cluster-knob {
left: 20px;
}
.mytrack-cluster-switch.is-off .mytrack-cluster-knob {
left: 2px;
}
}
</style>
HTML;
echo '<div id="mytrack-overlay" class="mytrack-overlay"></div>';
echo '<div id="mytrack-lightbox" class="mytrack-lightbox">
<span class="mytrack-lightbox-close">&times;</span>
<span class="mytrack-lightbox-nav mytrack-lightbox-prev">&#8249;</span>
<span class="mytrack-lightbox-nav mytrack-lightbox-next">&#8250;</span>
<img id="mytrack-lightbox-img" src="" alt="大图预览">
</div>';
// 输出JavaScript - 添加地图主题切换功能
echo <<<HTML
<script>
(function() {
var mapContainer = document.getElementById("mytrack-map");
if (!mapContainer) return;
// 全局变量
var allFootprints = [];
var currentFilter = 'all';
var markers = [];
var cluster = null;
var allCategories = [];
var markersCache = {};
var clusterEnabled = {$enableCluster} ? true : false;
var themeObserver = null;
var registeredMaps = new Set();
// 从之前正常的代码中移植的:获取当前主题
function getCurrentTheme() {
var lightTheme = 'amap://styles/whitesmoke';
var darkTheme = 'amap://styles/dark';
return document.documentElement.classList.contains('dark') ? darkTheme : lightTheme;
}
// 从之前正常的代码中移植的:注册主题同步
function registerThemeSync(map) {
registeredMaps.add(map);
if (themeObserver) return;
themeObserver = new MutationObserver(() => {
var style = getCurrentTheme();
registeredMaps.forEach(m => {
try { m.setMapStyle(style); } catch (e) {}
});
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
// 加载高德地图API
function loadAMapScript() {
if (window.AMap) {
initMap();
return;
}
if (typeof AMapLoader !== 'undefined') {
var plugins = ['AMap.Scale', 'AMap.ToolBar', 'AMap.ControlBar'];
if (clusterEnabled) {
plugins.push('AMap.MarkerCluster', 'AMap.Adaptor');
}
AMapLoader.load({
key: '{$apiKey}',
version: '2.0',
plugins: plugins
}).then((AMap) => {
initMap();
}).catch((e) => {
console.error("加载高德地图API失败:", e);
showError("加载高德地图API失败请检查网络连接或API密钥配置");
});
} else {
var loaderScript = document.createElement("script");
loaderScript.type = "text/javascript";
loaderScript.src = "https://webapi.amap.com/loader.js";
loaderScript.onload = function() {
loadAMapScript();
};
loaderScript.onerror = function() {
console.error("加载AMapLoader失败");
showError("加载地图加载器失败,请检查网络连接");
};
document.head.appendChild(loaderScript);
}
}
loadAMapScript();
function initMap() {
try {
// 修改使用getCurrentTheme()设置地图样式
window.mytrackMap = new AMap.Map("mytrack-map", {
zoom: parseInt({$zoomLevel}),
viewMode: "{$viewMode}",
mapStyle: getCurrentTheme(), // 添加这一行
resizeEnable: true
});
// 添加:注册主题同步
registerThemeSync(window.mytrackMap);
// 初始化工具栏事件
initToolbarEvents();
// 初始化集群开关
initClusterToggle();
// 等待地图完全加载
window.mytrackMap.on('complete', function() {
processFootprintsData();
console.info('%cMyTrack v{$version}%chttps://wangdaodao.com/', 'color: #013821; background: #43bb88; padding:5px; font-size: 12px; font-weight: bold;','color: #fadfa3; background: #030307; padding:5px; font-size: 12px; font-weight: bold;');
});
} catch (error) {
console.error("初始化地图失败:", error);
}
}
function initToolbarEvents() {
var fullscreenBtn = document.getElementById("mytrack-fullscreen-btn");
var resetBtn = document.getElementById("mytrack-reset-btn");
var zoomInBtn = document.getElementById("mytrack-zoom-in-btn");
var zoomOutBtn = document.getElementById("mytrack-zoom-out-btn");
if (fullscreenBtn) fullscreenBtn.addEventListener("click", toggleFullscreen);
if (resetBtn) resetBtn.addEventListener("click", resetView);
if (zoomInBtn) zoomInBtn.addEventListener("click", zoomIn);
if (zoomOutBtn) zoomOutBtn.addEventListener("click", zoomOut);
}
function initClusterToggle() {
var clusterSwitch = document.getElementById("mytrack-cluster-switch");
if (!clusterSwitch) return;
// 设置初始状态
if (!clusterEnabled) {
clusterSwitch.classList.remove("is-on");
clusterSwitch.classList.add("is-off");
}
clusterSwitch.addEventListener("click", function() {
clusterEnabled = !clusterEnabled;
if (clusterEnabled) {
clusterSwitch.classList.remove("is-off");
clusterSwitch.classList.add("is-on");
} else {
clusterSwitch.classList.remove("is-on");
clusterSwitch.classList.add("is-off");
}
// 更新地图显示
updateClusters();
});
}
function toggleFullscreen() {
var container = document.getElementById("mytrack-map-container");
if (!container) return;
var isFullscreen = container.classList.contains("is-fullscreen");
if (!isFullscreen) {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestfullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestfullscreen();
}
container.classList.add("is-fullscreen");
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
container.classList.remove("is-fullscreen");
}
setTimeout(function() {
if (window.mytrackMap) {
window.mytrackMap.resize();
}
}, 100);
}
function resetView() {
if (window.mytrackMap && markers && markers.length > 0) {
try {
window.mytrackMap.setFitView(markers, false, [80, 80, 80, 120]);
} catch (e) {
console.error("重置视图失败:", e);
}
}
}
function zoomIn() {
if (window.mytrackMap) {
var currentZoom = window.mytrackMap.getZoom();
window.mytrackMap.setZoom(currentZoom + 1);
}
}
function zoomOut() {
if (window.mytrackMap) {
var currentZoom = window.mytrackMap.getZoom();
window.mytrackMap.setZoom(currentZoom - 1);
}
}
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
function handleFullscreenChange() {
var container = document.getElementById("mytrack-map-container");
if (!container) return;
var isFullscreen = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
if (isFullscreen) {
container.classList.add("is-fullscreen");
} else {
container.classList.remove("is-fullscreen");
}
setTimeout(function() {
if (window.mytrackMap) {
window.mytrackMap.resize();
}
}, 100);
}
function processFootprintsData() {
try {
var serverData = {$footprintsJson};
console.log('足迹数据:', serverData);
if (serverData.success) {
allFootprints = serverData.data;
console.log('处理后的足迹数据:', allFootprints.length, '条');
// 提取所有分类 - 使用之前的逻辑
extractCategories(allFootprints);
console.log('提取到的分类:', allCategories);
// 渲染筛选按钮 - 使用之前的逻辑
renderFilters();
// 显示所有足迹
displayFootprints(allFootprints);
} else {
console.error("获取足迹数据失败:", serverData.message);
showError("获取足迹数据失败: " + serverData.message);
}
} catch (error) {
console.error("处理足迹数据出错:", error);
showError("处理足迹数据出错: " + error.message);
}
}
// 提取所有分类 - 之前的逻辑
function extractCategories(footprints) {
var categoriesSet = new Set();
footprints.forEach(function(footprint) {
if (footprint.categories && Array.isArray(footprint.categories)) {
footprint.categories.forEach(function(category) {
category = category.trim();
if (category) {
categoriesSet.add(category);
}
});
}
});
allCategories = Array.from(categoriesSet).sort();
}
// 渲染筛选按钮 - 修改:在显示时翻译为中文
function renderFilters() {
var filtersContainer = document.getElementById("mytrack-filters");
if (!filtersContainer) return;
console.log('渲染筛选器,分类数量:', allCategories.length);
// 只有当有分类时才显示筛选器
if (allCategories.length === 0) {
console.log('没有分类,隐藏筛选器');
filtersContainer.style.display = 'none';
return;
}
// 清空容器
filtersContainer.innerHTML = '';
// 创建"全部"按钮
var allButton = createFilterButton('全部足迹', 'all', true);
filtersContainer.appendChild(allButton);
// 创建分类按钮 - 显示时翻译为中文
allCategories.forEach(function(category) {
// 在显示时翻译为中文,但内部逻辑仍使用英文分类
var displayName = translateCategoryToChinese(category);
var button = createFilterButton(displayName, category, false);
filtersContainer.appendChild(button);
});
console.log('筛选器渲染完成,按钮数量:', filtersContainer.children.length);
}
// 创建筛选按钮 - 之前的逻辑
function createFilterButton(label, value, isActive) {
var button = document.createElement("button");
button.type = "button";
button.className = "mytrack-filter-btn";
if (isActive) {
button.classList.add("is-active");
}
button.textContent = label;
button.dataset.filter = value;
button.addEventListener("click", function() {
if (button.classList.contains("is-active")) return;
// 更新活动状态
document.querySelectorAll(".mytrack-filter-btn").forEach(function(btn) {
btn.classList.remove("is-active");
});
button.classList.add("is-active");
// 更新筛选 - 注意:这里使用的是英文分类值
currentFilter = value;
filterFootprints();
});
return button;
}
// 筛选足迹 - 之前的逻辑
function filterFootprints() {
var filteredFootprints = [];
if (currentFilter === 'all') {
filteredFootprints = allFootprints;
} else {
filteredFootprints = allFootprints.filter(function(footprint) {
if (!footprint.categories || !Array.isArray(footprint.categories)) {
return false;
}
return footprint.categories.some(function(category) {
return category.trim() === currentFilter;
});
});
}
console.log('筛选结果:', currentFilter, '=>', filteredFootprints.length, '条');
// 显示筛选后的足迹
displayFootprints(filteredFootprints);
}
function showError(message) {
var mapContainer = document.getElementById("mytrack-map");
if (mapContainer) {
var errorDiv = document.createElement("div");
errorDiv.className = "mytrack-error";
errorDiv.textContent = message;
errorDiv.style.position = "absolute";
errorDiv.style.top = "50%";
errorDiv.style.left = "50%";
errorDiv.style.transform = "translate(-50%, -50%)";
errorDiv.style.zIndex = "10";
errorDiv.style.maxWidth = "80%";
mapContainer.appendChild(errorDiv);
}
}
// 显示足迹 - 修复集群问题
function displayFootprints(footprints) {
if (!footprints || footprints.length === 0) {
clearMap();
return;
}
// 清空地图
clearMap();
var validFootprints = [];
var newMarkers = [];
var hasContentCache = {};
footprints.forEach(function(footprint) {
var longitude = parseFloat(footprint.longitude);
var latitude = parseFloat(footprint.latitude);
if (isNaN(longitude) || isNaN(latitude) ||
longitude < -180 || longitude > 180 ||
latitude < -90 || latitude > 90) {
console.warn("发现无效的经纬度数据:", footprint);
return;
}
var hasAddress = !!(footprint.address && footprint.address.trim());
var hasLocationType = !!(footprint.location_type && footprint.location_type.trim());
var hasRatingLevel = !!(footprint.rating_level && footprint.rating_level > 0);
var hasCategories = !!(footprint.categories && footprint.categories.length > 0);
var hasReview = !!(footprint.review && footprint.review.trim());
var hasHighlights = !!(footprint.highlights && footprint.highlights.trim());
var hasRelatedArticles = false;
if (footprint.related_articles_info && Array.isArray(footprint.related_articles_info)) {
hasRelatedArticles = footprint.related_articles_info.length > 0;
}
var hasContent = hasAddress || hasLocationType || hasRatingLevel || hasCategories || hasReview || hasHighlights || hasRelatedArticles;
hasContentCache[footprint.id || JSON.stringify(footprint)] = {
hasAddress: hasAddress,
hasLocationType: hasLocationType,
hasRatingLevel: hasRatingLevel,
hasCategories: hasCategories,
hasReview: hasReview,
hasHighlights: hasHighlights,
hasRelatedArticles: hasRelatedArticles,
hasContent: hasContent
};
validFootprints.push(footprint);
var position = new AMap.LngLat(longitude, latitude);
try {
var marker = new AMap.Marker({
position: position,
title: footprint.name || "足迹点",
animation: "AMAP_ANIMATION_DROP",
anchor: 'bottom-center'
});
marker.setExtData({
footprint: footprint,
hasAddress: hasAddress,
hasLocationType: hasLocationType,
hasRatingLevel: hasRatingLevel,
hasCategories: hasCategories,
hasReview: hasReview,
hasHighlights: hasHighlights,
hasRelatedArticles: hasRelatedArticles,
hasContent: hasContent
});
var markerContent = '';
if (footprint.markerColor) {
markerContent = '<div class="mytrack-marker-custom mytrack-marker-' + footprint.markerColor + '"></div>';
} else if (hasContent) {
markerContent = '<div class="mytrack-marker-with-content"></div>';
} else {
markerContent = '<div class="mytrack-marker-empty"></div>';
}
marker.setContent(markerContent);
marker.on("click", function(e) {
var data = e.target.getExtData();
if (data.hasContent) {
showFootprintInfo(data.footprint);
}
});
newMarkers.push(marker);
markersCache[footprint.id || JSON.stringify(footprint)] = marker;
} catch (error) {
console.error("创建标记点失败:", error, "数据:", footprint);
}
});
markers = newMarkers;
// 更新集群显示 - 简化逻辑
updateClusters();
if (validFootprints.length > 0) {
if (markers && markers.length > 0) {
try {
window.mytrackMap.setFitView(markers, false, [80, 80, 80, 120]);
} catch (e) {
console.error("自动调整视图失败:", e);
var firstFootprint = validFootprints[0];
var centerPosition = new AMap.LngLat(
parseFloat(firstFootprint.longitude),
parseFloat(firstFootprint.latitude)
);
window.mytrackMap.setCenter(centerPosition);
}
} else {
var firstFootprint = validFootprints[0];
var centerPosition = new AMap.LngLat(
parseFloat(firstFootprint.longitude),
parseFloat(firstFootprint.latitude)
);
window.mytrackMap.setCenter(centerPosition);
}
} else {
console.warn("没有有效的足迹数据可以显示");
}
}
// 更新集群显示 - 简化版本
function updateClusters() {
// 移除现有的集群
if (cluster) {
try {
cluster.setMap(null);
} catch (e) {
console.error("移除集群时出错:", e);
}
cluster = null;
}
// 移除现有的标记点
if (markers && markers.length > 0) {
try {
window.mytrackMap.remove(markers);
} catch (e) {
console.error("移除标记点时出错:", e);
}
}
// 如果启用了点聚合,创建聚合对象
if (clusterEnabled && markers.length > 0) {
try {
if (typeof AMap.MarkerCluster !== 'undefined') {
cluster = new AMap.MarkerCluster(window.mytrackMap, markers, {
gridSize: 80,
maxZoom: 10,
averageCenter: true,
minClusterSize: 2,
styles: [
{
url: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="18" fill="{$clusterMarkerColor1}" fill-opacity="0.7" stroke="{$clusterMarkerColor1}" stroke-width="2"/></svg>'),
size: new AMap.Size(40, 40),
offset: new AMap.Pixel(-20, -20),
textColor: 'white',
textSize: 14
},
{
url: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45" viewBox="0 0 45 45"><circle cx="22.5" cy="22.5" r="20.5" fill="{$clusterMarkerColor2}" fill-opacity="0.7" stroke="{$clusterMarkerColor2}" stroke-width="2"/></svg>'),
size: new AMap.Size(45, 45),
offset: new AMap.Pixel(-22.5, -22.5),
textColor: 'white',
textSize: 16
},
{
url: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50"><circle cx="25" cy="25" r="23" fill="{$clusterMarkerColor3}" fill-opacity="0.7" stroke="{$clusterMarkerColor3}" stroke-width="2"/></svg>'),
size: new AMap.Size(50, 50),
offset: new AMap.Pixel(-25, -25),
textColor: 'white',
textSize: 16
}
]
});
cluster.on('click', function(e) {
var clusterData = e.clusterData;
if (clusterData && clusterData.length > 1) {
var currentZoom = window.mytrackMap.getZoom();
var newZoom = currentZoom + 3;
newZoom = Math.min(newZoom, 18);
window.mytrackMap.setZoom(newZoom);
window.mytrackMap.setCenter(e.lnglat);
return;
}
var marker = e.target;
if (marker && marker.getExtData) {
var data = marker.getExtData();
if (data.hasContent) {
showFootprintInfo(data.footprint);
}
}
});
console.log('集群已启用,标记点数量:', markers.length);
} else {
console.warn("AMap.MarkerCluster未加载使用普通标记点");
window.mytrackMap.add(markers);
}
} catch (error) {
console.error("创建点聚合失败:", error);
window.mytrackMap.add(markers);
}
} else if (markers.length > 0) {
window.mytrackMap.add(markers);
console.log('集群已禁用,使用普通标记点,数量:', markers.length);
}
}
function clearMap() {
if (cluster) {
cluster.setMap(null);
cluster = null;
}
if (markers && markers.length > 0) {
window.mytrackMap.remove(markers);
markers = [];
}
markersCache = {};
}
function showFootprintInfo(footprint) {
var infoPanel = document.getElementById("mytrack-info-panel");
var overlay = document.getElementById("mytrack-overlay");
var metaDiv = document.getElementById("mytrack-info-meta");
var reviewDiv = document.getElementById("mytrack-info-review");
var imagesDiv = document.getElementById("mytrack-info-images");
var titleElement = document.getElementById("mytrack-info-title");
var hasAddress = !!(footprint.address && footprint.address.trim());
var hasLocationType = !!(footprint.location_type && footprint.location_type.trim());
var hasRatingLevel = !!(footprint.rating_level && footprint.rating_level > 0);
var hasCategories = !!(footprint.categories && footprint.categories.length > 0);
var hasReview = !!(footprint.review && footprint.review.trim());
var hasHighlights = !!(footprint.highlights && footprint.highlights.trim());
var hasRelatedArticles = false;
var relatedArticlesInfo = null;
if (footprint.related_articles_info && Array.isArray(footprint.related_articles_info)) {
relatedArticlesInfo = footprint.related_articles_info;
hasRelatedArticles = relatedArticlesInfo.length > 0;
}
if (!hasAddress && !hasLocationType && !hasRatingLevel &&
!hasCategories && !hasReview && !hasHighlights && !hasRelatedArticles) {
return;
}
metaDiv.innerHTML = '';
reviewDiv.style.display = 'none';
imagesDiv.style.display = 'none';
titleElement.textContent = footprint.name || "足迹信息";
// 1. 地址信息
if (hasAddress) {
var addressDiv = document.createElement("div");
addressDiv.className = "mytrack-card-address";
addressDiv.innerHTML = '<strong>地址:</strong>' + footprint.address;
metaDiv.appendChild(addressDiv);
}
// 2. 类型信息
if (hasLocationType) {
var typeDiv = document.createElement("div");
typeDiv.className = "mytrack-card-type";
typeDiv.innerHTML = '<strong>类型:</strong>' + footprint.location_type;
metaDiv.appendChild(typeDiv);
}
// 3. 分类信息 - 翻译为中文显示
if (hasCategories) {
var categoriesDiv = document.createElement("div");
categoriesDiv.className = "mytrack-card-categories";
categoriesDiv.innerHTML = '<strong>分类:</strong>';
if (Array.isArray(footprint.categories)) {
footprint.categories.forEach(function(category) {
category = category.trim();
var className = getCategoryClass(category);
var text = translateCategoryToChinese(category);
var badge = document.createElement("span");
badge.className = 'mytrack-categories-badge ' + className;
badge.textContent = text;
categoriesDiv.appendChild(badge);
});
}
metaDiv.appendChild(categoriesDiv);
}
// 4. 推荐星级
if (hasRatingLevel) {
var ratingDiv = document.createElement("div");
ratingDiv.className = "mytrack-card-rating";
ratingDiv.innerHTML = '<strong>推荐:</strong><span class="mytrack-stars-inline">';
var starsContainer = document.createElement("span");
starsContainer.className = "mytrack-rating-display";
for (var i = 1; i <= 5; i++) {
var star = document.createElement("span");
if (i <= footprint.rating_level) {
star.className = "mytrack-rating-star";
star.textContent = "★";
} else {
star.className = "mytrack-rating-empty-star";
star.textContent = "☆";
}
starsContainer.appendChild(star);
}
ratingDiv.appendChild(starsContainer);
metaDiv.appendChild(ratingDiv);
}
// 5. 亮点信息(新增)
if (hasHighlights) {
var highlightsDiv = document.createElement("div");
highlightsDiv.className = "mytrack-card-highlights";
highlightsDiv.innerHTML = '<strong>亮点:</strong>';
// 按逗号分隔亮点
var highlights = footprint.highlights.split(',').map(function(highlight) {
return highlight.trim();
}).filter(function(highlight) {
return highlight !== '';
});
if (highlights.length > 0) {
highlights.forEach(function(highlight, index) {
if (index > 0) {
highlightsDiv.appendChild(document.createTextNode(', '));
}
var highlightSpan = document.createElement("span");
highlightSpan.className = "mytrack-highlight-tag";
highlightSpan.textContent = highlight;
highlightsDiv.appendChild(highlightSpan);
});
}
metaDiv.appendChild(highlightsDiv);
}
// 6. 简评
if (hasReview) {
var reviewDivInMeta = document.createElement("div");
reviewDivInMeta.className = "mytrack-card-review";
reviewDivInMeta.innerHTML = '<strong>简评:</strong>' + footprint.review;
metaDiv.appendChild(reviewDivInMeta);
}
// 7. 关联文章
if (hasRelatedArticles && relatedArticlesInfo) {
var relatedArticlesDiv = document.createElement("div");
relatedArticlesDiv.className = "mytrack-related-articles-section";
relatedArticlesDiv.innerHTML = '<strong>这些文章提到了本地点:</strong>';
var articlesList = document.createElement("div");
articlesList.className = "mytrack-related-articles-list";
relatedArticlesInfo.forEach(function(articleInfo) {
if (articleInfo && articleInfo.title && articleInfo.link) {
var articleItem = document.createElement("div");
articleItem.className = "mytrack-related-article-item";
var link = document.createElement("a");
link.href = articleInfo.link;
link.target = "_blank";
link.textContent = articleInfo.title;
articleItem.appendChild(link);
articlesList.appendChild(articleItem);
}
});
if (articlesList.children.length > 0) {
relatedArticlesDiv.appendChild(articlesList);
metaDiv.appendChild(relatedArticlesDiv);
}
}
infoPanel.style.display = "block";
overlay.style.display = "block";
}
function getCategoryClass(category) {
var categoryLower = category.toLowerCase();
if (categoryLower === 'visited' || categoryLower === '已玩') {
return 'mytrack-categories-visited-badge';
} else if (categoryLower === 'want' || categoryLower === '向往') {
return 'mytrack-categories-want-badge';
} else if (categoryLower === 'plan' || categoryLower === '计划') {
return 'mytrack-categories-plan-badge';
} else if (categoryLower === 'cafe' || categoryLower === '咖啡') {
return 'mytrack-categories-cafe-badge';
} else if (categoryLower === 'restaurant' || categoryLower === '餐厅') {
return 'mytrack-categories-restaurant-badge';
} else if (categoryLower === 'hotel' || categoryLower === '酒店') {
return 'mytrack-categories-hotel-badge';
} else if (categoryLower === 'shop' || categoryLower === '商店') {
return 'mytrack-categories-shop-badge';
} else if (categoryLower === 'attraction' || categoryLower === '景点') {
return 'mytrack-categories-attraction-badge';
} else if (categoryLower === 'food' || categoryLower === '美食') {
return 'mytrack-categories-food-badge';
} else if (categoryLower === 'travel' || categoryLower === '旅行') {
return 'mytrack-categories-travel-badge';
}
return 'mytrack-categories-badge';
}
// 翻译分类为中文(用于显示)
function translateCategoryToChinese(category) {
var translations = {
'plan': '计划',
'Plan': '计划',
'want': '向往',
'Want': '向往',
'visited': '已玩',
'Visited': '已玩',
'wish': '想去',
'Wish': '想去',
'todo': '待做',
'Todo': '待做',
'done': '完成',
'Done': '完成',
'travel': '旅行',
'Travel': '旅行',
'food': '美食',
'Food': '美食',
'shopping': '购物',
'Shopping': '购物',
'entertainment': '娱乐',
'Entertainment': '娱乐',
'culture': '文化',
'Culture': '文化',
'nature': '自然',
'Nature': '自然',
'cafe': '咖啡',
'Cafe': '咖啡',
'restaurant': '餐厅',
'Restaurant': '餐厅',
'hotel': '酒店',
'Hotel': '酒店',
'shop': '商店',
'Shop': '商店',
'attraction': '景点',
'Attraction': '景点',
'park': '公园',
'Park': '公园',
'museum': '博物馆',
'Museum': '博物馆'
};
var categoryLower = category.toLowerCase();
for (var key in translations) {
if (key.toLowerCase() === categoryLower) {
return translations[key];
}
}
return category;
}
function closeInfoPanel() {
var infoPanel = document.getElementById("mytrack-info-panel");
var overlay = document.getElementById("mytrack-overlay");
if (infoPanel) infoPanel.style.display = "none";
if (overlay) overlay.style.display = "none";
}
var currentImageIndex = 0;
var currentImages = [];
function openLightbox(imageSrc, images, index) {
currentImages = images;
currentImageIndex = index;
var lightbox = document.getElementById("mytrack-lightbox");
var lightboxImg = document.getElementById("mytrack-lightbox-img");
var infoPanel = document.getElementById("mytrack-info-panel");
if (lightboxImg) lightboxImg.src = imageSrc;
if (lightbox) lightbox.style.display = "flex";
if (infoPanel) {
infoPanel.classList.add("is-hidden");
}
updateNavigationButtons();
}
function closeLightbox() {
var lightbox = document.getElementById("mytrack-lightbox");
var infoPanel = document.getElementById("mytrack-info-panel");
if (lightbox) lightbox.style.display = "none";
if (infoPanel) {
infoPanel.classList.remove("is-hidden");
}
}
function updateNavigationButtons() {
var prevBtn = document.querySelector(".mytrack-lightbox-prev");
var nextBtn = document.querySelector(".mytrack-lightbox-next");
if (!prevBtn || !nextBtn) return;
if (currentImages.length <= 1) {
prevBtn.style.display = "none";
nextBtn.style.display = "none";
} else {
prevBtn.style.display = currentImageIndex === 0 ? "none" : "block";
nextBtn.style.display = currentImageIndex === currentImages.length - 1 ? "none" : "block";
}
}
function showPrevImage() {
if (currentImageIndex > 0) {
currentImageIndex--;
var lightboxImg = document.getElementById("mytrack-lightbox-img");
if (lightboxImg) lightboxImg.src = currentImages[currentImageIndex];
updateNavigationButtons();
}
}
function showNextImage() {
if (currentImageIndex < currentImages.length - 1) {
currentImageIndex++;
var lightboxImg = document.getElementById("mytrack-lightbox-img");
if (lightboxImg) lightboxImg.src = currentImages[currentImageIndex];
updateNavigationButtons();
}
}
var infoCloseBtn = document.getElementById("mytrack-info-close");
var overlay = document.getElementById("mytrack-overlay");
if (infoCloseBtn) infoCloseBtn.addEventListener("click", closeInfoPanel);
if (overlay) overlay.addEventListener("click", closeInfoPanel);
var lightbox = document.getElementById("mytrack-lightbox");
if (lightbox) {
lightbox.addEventListener("click", function(e) {
if (e.target === this) {
closeLightbox();
}
});
}
var lightboxClose = document.querySelector(".mytrack-lightbox-close");
var lightboxPrev = document.querySelector(".mytrack-lightbox-prev");
var lightboxNext = document.querySelector(".mytrack-lightbox-next");
if (lightboxClose) lightboxClose.addEventListener("click", closeLightbox);
if (lightboxPrev) lightboxPrev.addEventListener("click", function(e) {
e.stopPropagation();
showPrevImage();
});
if (lightboxNext) lightboxNext.addEventListener("click", function(e) {
e.stopPropagation();
showNextImage();
});
document.addEventListener("keydown", function(e) {
var lightbox = document.getElementById("mytrack-lightbox");
if (lightbox && lightbox.style.display === "flex") {
if (e.key === "Escape") {
closeLightbox();
} else if (e.key === "ArrowLeft") {
showPrevImage();
} else if (e.key === "ArrowRight") {
showNextImage();
}
}
});
})();
</script>
HTML;
}
/**
* 将十六进制颜色转换为RGB格式
*
* @access private
* @param string $hex 十六进制颜色值
* @return array RGB值数组
*/
private function hexToRgb($hex)
{
$hex = ltrim($hex, '#');
if (strlen($hex) == 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
return array('r' => $r, 'g' => $g, 'b' => $b);
}
}