Files
MyTrack/Widget.php

2330 lines
76 KiB
PHP
Raw Permalink Normal View History

2026-02-23 19:45:59 +08:00
<?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);
}
}