From 945b2a104cb5db458c7c031045fecf9f5939688a Mon Sep 17 00:00:00 2001
From: XIGE <710062962@qq.com>
Date: Mon, 23 Feb 2026 19:45:59 +0800
Subject: [PATCH] 1.0
---
Action.php | 1133 ++++++++
Manage.php | 2735 ++++++++++++++++++
Plugin.php | 1855 ++++++++++++
Widget.php | 2330 +++++++++++++++
assets/admin.css | 439 +++
cache/bac86e675f18b6df00448f832069f769.cache | 1 +
db/track_e670fbc4f2.db | Bin 0 -> 73728 bytes
7 files changed, 8493 insertions(+)
create mode 100644 Action.php
create mode 100644 Manage.php
create mode 100644 Plugin.php
create mode 100644 Widget.php
create mode 100644 assets/admin.css
create mode 100644 cache/bac86e675f18b6df00448f832069f769.cache
create mode 100644 db/track_e670fbc4f2.db
diff --git a/Action.php b/Action.php
new file mode 100644
index 0000000..78a9f45
--- /dev/null
+++ b/Action.php
@@ -0,0 +1,1133 @@
+db = MyTrack_Plugin::getDbConnection();
+ }
+
+ /**
+ * 动作入口
+ *
+ * @access public
+ * @return void
+ */
+ public function action()
+ {
+ $this->widget('Widget_User')->pass('administrator');
+ $this->on($this->request->is('do=add'))->add();
+ $this->on($this->request->is('do=update'))->update();
+ $this->on($this->request->is('do=delete'))->delete();
+ $this->on($this->request->is('do=get'))->get();
+ $this->on($this->request->is('do=getAll'))->getAll();
+ $this->on($this->request->is('do=getOne'))->get();
+ $this->on($this->request->is('do=list'))->list();
+ $this->on($this->request->is('do=batchDelete'))->batchDelete();
+ $this->on($this->request->is('do=import'))->import();
+ $this->on($this->request->is('do=export'))->export();
+ $this->on($this->request->is('do=exportSimple'))->exportSimple();
+ $this->on($this->request->is('do=getArticleImages'))->getArticleImages();
+ $this->on($this->request->is('do=getById'))->getById();
+ $this->on($this->request->is('do=getRelatedArticlesInfo'))->getRelatedArticlesInfo();
+
+ // 默认返回列表
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '无效的操作'
+ ));
+ }
+
+ /**
+ * 检测重复数据(基于经纬度)
+ *
+ * @access private
+ * @param float $latitude 纬度
+ * @param float $longitude 经度
+ * @param int $excludeId 要排除的ID(更新时使用)
+ * @return array|false 返回重复的数据信息,false表示无重复
+ */
+ private function checkDuplicate($latitude, $longitude, $excludeId = null)
+ {
+ try {
+ // 根据经纬度进行检测,使用较高精度(0.000001度约等于0.1米)
+ $sql = "SELECT id, name FROM plugin_track_footprint
+ WHERE ABS(latitude - ?) < 0.000001
+ AND ABS(longitude - ?) < 0.000001";
+
+ $params = array($latitude, $longitude);
+
+ if ($excludeId !== null) {
+ $sql .= " AND id != ?";
+ $params[] = $excludeId;
+ }
+
+ $sql .= " LIMIT 1";
+
+ $stmt = $this->db->prepare($sql);
+ $stmt->execute($params);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ return $result ? $result : false;
+ } catch (PDOException $e) {
+ error_log('检测重复数据失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ public function add()
+ {
+ // 原有的参数获取
+ $latitude = $this->request->get('latitude');
+ $longitude = $this->request->get('longitude');
+ $name = $this->request->get('name');
+ $description = $this->request->get('description');
+ $tags = $this->request->get('tags');
+ $article_cid = $this->request->get('article_cid');
+ $urlLabel = $this->request->get('urlLabel');
+ $url = $this->request->get('url');
+ $photos = $this->request->get('photos');
+ $date = $this->request->get('date');
+
+ // 新增字段
+ $address = $this->request->get('address');
+ $location_type = $this->request->get('location_type');
+ $rating_level = $this->request->get('rating_level', 0);
+ $categories = $this->request->get('categories');
+ $review = $this->request->get('review');
+ $markerColor = $this->request->get('markerColor');
+ $related_articles = $this->request->get('related_articles');
+ $highlights = $this->request->get('highlights');
+
+ // 如果提供了日期,确保格式为 YYYY-MM-DD HH:MM:SS
+ if (!empty($date)) {
+ $date = date('Y-m-d H:i:s', strtotime($date));
+ }
+
+ // 验证必填字段
+ if (empty($latitude) || empty($longitude) || empty($name)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '经纬度和地点名称不能为空'
+ ));
+ }
+
+ // 检查重复数据(基于经纬度)
+ $duplicate = $this->checkDuplicate($latitude, $longitude);
+ if ($duplicate !== false && isset($duplicate['name']) && isset($duplicate['id'])) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => "该位置已存在地点:{$duplicate['name']}(ID: {$duplicate['id']}),请勿重复添加"
+ ));
+ }
+
+ try {
+ // 如果提供了文章CID,获取文章信息和图片
+ if (!empty($article_cid)) {
+ $articleInfo = MyTrack_Plugin::getArticleInfo($article_cid);
+
+ // 如果没有提供图片链接,尝试从文章中提取(限制4张)
+ if (empty($photos) && !empty($articleInfo['images'])) {
+ $limitedImages = array_slice($articleInfo['images'], 0, 4);
+ $photos = implode(',', $limitedImages);
+ }
+
+ // 如果没有提供文章标题,尝试从文章中获取
+ if (empty($urlLabel) && !empty($articleInfo['title'])) {
+ $urlLabel = $articleInfo['title'];
+ }
+
+ // 如果没有提供文章链接,尝试从文章中获取
+ if (empty($url) && !empty($articleInfo['link'])) {
+ $url = $articleInfo['link'];
+ }
+
+ // 如果没有提供标签,尝试从文章中获取
+ if (empty($tags) && !empty($articleInfo['tags'])) {
+ $tags = implode(',', $articleInfo['tags']);
+ }
+ }
+
+ $stmt = $this->db->prepare("INSERT INTO plugin_track_footprint
+ (latitude, longitude, name, address, location_type, rating_level, categories, review, description, tags, article_cid, urlLabel, url, photos, date, markerColor, related_articles, highlights)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+ $stmt->execute(array(
+ $latitude,
+ $longitude,
+ $name,
+ $address,
+ $location_type,
+ $rating_level,
+ $categories,
+ $review,
+ $description,
+ $tags,
+ $article_cid,
+ $urlLabel,
+ $url,
+ $photos,
+ $date,
+ $markerColor,
+ $related_articles,
+ $highlights
+ ));
+
+ $id = $this->db->lastInsertId();
+
+ // 清除足迹缓存
+ $this->clearFootprintCache();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '足迹添加成功',
+ 'data' => array(
+ 'id' => $id
+ )
+ ));
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '添加失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ public function update()
+ {
+ $id = $this->request->get('id');
+ $latitude = $this->request->get('latitude');
+ $longitude = $this->request->get('longitude');
+ $name = $this->request->get('name');
+ $description = $this->request->get('description');
+ $tags = $this->request->get('tags');
+ $article_cid = $this->request->get('article_cid');
+ $urlLabel = $this->request->get('urlLabel');
+ $url = $this->request->get('url');
+ $photos = $this->request->get('photos');
+ $date = $this->request->get('date');
+
+ // 新增字段
+ $address = $this->request->get('address');
+ $location_type = $this->request->get('location_type');
+ $rating_level = $this->request->get('rating_level', 0);
+ $categories = $this->request->get('categories');
+ $review = $this->request->get('review');
+ $markerColor = $this->request->get('markerColor');
+ $related_articles = $this->request->get('related_articles');
+ $highlights = $this->request->get('highlights');
+
+ // 验证必填字段
+ if (empty($id) || empty($latitude) || empty($longitude) || empty($name)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => 'ID、经纬度和地点名称不能为空'
+ ));
+ }
+
+ // 检查重复数据(基于经纬度,排除自身)
+ $duplicate = $this->checkDuplicate($latitude, $longitude, $id);
+ if ($duplicate !== false && isset($duplicate['name']) && isset($duplicate['id'])) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => "该位置已存在其他地点:{$duplicate['name']}(ID: {$duplicate['id']}),请修改经纬度"
+ ));
+ }
+
+ try {
+ // 如果提供了文章CID,获取文章信息和图片
+ if (!empty($article_cid)) {
+ $articleInfo = MyTrack_Plugin::getArticleInfo($article_cid);
+
+ if (empty($photos) && !empty($articleInfo['images'])) {
+ $limitedImages = array_slice($articleInfo['images'], 0, 4);
+ $photos = implode(',', $limitedImages);
+ }
+
+ if (empty($urlLabel) && !empty($articleInfo['title'])) {
+ $urlLabel = $articleInfo['title'];
+ }
+
+ if (empty($url) && !empty($articleInfo['link'])) {
+ $url = $articleInfo['link'];
+ }
+
+ if (empty($tags) && !empty($articleInfo['tags'])) {
+ $tags = implode(',', $articleInfo['tags']);
+ }
+ }
+
+ $stmt = $this->db->prepare("UPDATE plugin_track_footprint
+ SET latitude = ?, longitude = ?, name = ?, address = ?, location_type = ?, rating_level = ?, categories = ?, review = ?, description = ?, tags = ?, article_cid = ?,
+ urlLabel = ?, url = ?, photos = ?, date = ?, markerColor = ?, related_articles = ?, highlights = ?
+ WHERE id = ?");
+
+ $result = $stmt->execute(array(
+ $latitude,
+ $longitude,
+ $name,
+ $address,
+ $location_type,
+ $rating_level,
+ $categories,
+ $review,
+ $description,
+ $tags,
+ $article_cid,
+ $urlLabel,
+ $url,
+ $photos,
+ $date,
+ $markerColor,
+ $related_articles,
+ $highlights,
+ $id
+ ));
+
+ if ($result) {
+ // 清除足迹缓存
+ $this->clearFootprintCache();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '足迹更新成功'
+ ));
+ } else {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '足迹不存在或更新失败'
+ ));
+ }
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '更新失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取关联文章信息
+ *
+ * @access public
+ * @return void
+ */
+ public function getRelatedArticlesInfo()
+ {
+ $related_articles = $this->request->get('related_articles');
+
+ if (empty($related_articles)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '关联文章不能为空'
+ ));
+ }
+
+ try {
+ $result = MyTrack_Plugin::getRelatedArticlesInfo($related_articles);
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $result
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取关联文章信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 批量删除足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function batchDelete()
+ {
+ $ids = $this->request->filter('int')->getArray('footprint');
+
+ if (empty($ids)) {
+ $rawIds = $this->request->get('footprint');
+ if (!empty($rawIds)) {
+ $ids = is_array($rawIds) ? $rawIds : array($rawIds);
+ $ids = array_map('intval', $ids);
+ }
+ }
+
+ if (empty($ids) || !is_array($ids)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '请选择要删除的足迹'
+ ));
+ }
+
+ $ids = array_filter($ids, function($id) {
+ return is_numeric($id) && $id > 0;
+ });
+
+ if (empty($ids)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '无效的足迹ID'
+ ));
+ }
+
+ try {
+ $placeholders = implode(',', array_fill(0, count($ids), '?'));
+ $stmt = $this->db->prepare("DELETE FROM plugin_track_footprint WHERE id IN ($placeholders)");
+ $result = $stmt->execute($ids);
+
+ if ($result) {
+ $this->clearFootprintCache();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '足迹删除成功'
+ ));
+ } else {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '删除失败,请检查足迹是否存在'
+ ));
+ }
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '删除失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 删除足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function delete()
+ {
+ $id = $this->request->get('id');
+
+ if (empty($id)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => 'ID不能为空'
+ ));
+ }
+
+ try {
+ $stmt = $this->db->prepare("DELETE FROM plugin_track_footprint WHERE id = ?");
+ $result = $stmt->execute(array($id));
+
+ if ($result) {
+ $this->clearFootprintCache();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '足迹删除成功'
+ ));
+ } else {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '足迹不存在或删除失败'
+ ));
+ }
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '删除失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取单个足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function get()
+ {
+ $id = $this->request->get('id');
+
+ if (empty($id)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => 'ID不能为空'
+ ));
+ }
+
+ try {
+ $stmt = $this->db->prepare("SELECT * FROM plugin_track_footprint WHERE id = ?");
+ $stmt->execute(array($id));
+ $footprint = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($footprint) {
+ if (!empty($footprint['related_articles'])) {
+ $footprint['related_articles_info'] = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
+ }
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $footprint
+ ));
+ } else {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '足迹不存在'
+ ));
+ }
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 通过ID获取单个足迹(公开接口)
+ *
+ * @access public
+ * @return void
+ */
+ public function getById()
+ {
+ $id = $this->request->get('id');
+
+ if (empty($id)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => 'ID不能为空'
+ ));
+ }
+
+ try {
+ $stmt = $this->db->prepare("SELECT * FROM plugin_track_footprint WHERE id = ?");
+ $stmt->execute(array($id));
+ $footprint = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($footprint) {
+ if (!empty($footprint['categories'])) {
+ $footprint['categories'] = explode(',', $footprint['categories']);
+ } else {
+ $footprint['categories'] = array();
+ }
+
+ if (!empty($footprint['highlights'])) {
+ $footprint['highlights'] = explode(',', $footprint['highlights']);
+ } else {
+ $footprint['highlights'] = array();
+ }
+
+ if (!empty($footprint['article_cid'])) {
+ $articleInfo = MyTrack_Plugin::getArticleInfo($footprint['article_cid']);
+ if (!empty($articleInfo['title']) && empty($footprint['urlLabel'])) {
+ $footprint['urlLabel'] = $articleInfo['title'];
+ }
+ if (!empty($articleInfo['link']) && empty($footprint['url'])) {
+ $footprint['url'] = $articleInfo['link'];
+ }
+ }
+
+ if (!empty($footprint['related_articles'])) {
+ $footprint['related_articles_info'] = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
+ }
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $footprint
+ ));
+ } else {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '足迹不存在'
+ ));
+ }
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取足迹列表
+ *
+ * @access public
+ * @return void
+ */
+ public function list()
+ {
+ $page = $this->request->get('page', 1);
+ $size = $this->request->get('size', 10);
+ $offset = ($page - 1) * $size;
+
+ try {
+ $countStmt = $this->db->query("SELECT COUNT(*) FROM plugin_track_footprint");
+ $total = $countStmt->fetchColumn();
+
+ $stmt = $this->db->prepare("SELECT * FROM plugin_track_footprint
+ ORDER BY date DESC, created_at DESC
+ LIMIT ? OFFSET ?");
+ $stmt->execute(array($size, $offset));
+ $footprints = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($footprints as &$footprint) {
+ if (!empty($footprint['related_articles'])) {
+ $footprint['related_articles_info'] = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
+ }
+ }
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => array(
+ 'total' => $total,
+ 'page' => $page,
+ 'size' => $size,
+ 'pages' => ceil($total / $size),
+ 'items' => $footprints
+ )
+ ));
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 导入足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function import()
+ {
+ if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '请上传有效的文件'
+ ));
+ }
+
+ $file = $_FILES['file'];
+ $fileType = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+
+ if (!in_array($fileType, array('json'))) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '只支持JSON格式的文件'
+ ));
+ }
+
+ try {
+ $content = file_get_contents($file['tmp_name']);
+
+ $encoding = mb_detect_encoding($content, array('UTF-8', 'GBK', 'GB2312', 'ASCII'), true);
+ if ($encoding !== 'UTF-8' && $encoding !== false) {
+ $content = mb_convert_encoding($content, 'UTF-8', $encoding);
+ }
+
+ $data = json_decode($content, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new Exception('JSON格式错误: ' . json_last_error_msg());
+ }
+
+ if (!is_array($data)) {
+ throw new Exception('JSON格式不正确,应该是一个数组');
+ }
+
+ $successCount = 0;
+ $errorCount = 0;
+ $errors = array();
+
+ $this->db->beginTransaction();
+
+ foreach ($data as $index => $item) {
+ try {
+ if (empty($item['latitude']) || empty($item['longitude']) || empty($item['name'])) {
+ $errors[] = "第" . ($index + 1) . "行: 缺少经纬度或地点名称";
+ $errorCount++;
+ continue;
+ }
+
+ // 导入时检查重复数据(基于经纬度)
+ $duplicate = $this->checkDuplicate($item['latitude'], $item['longitude']);
+ if ($duplicate !== false && isset($duplicate['name']) && isset($duplicate['id'])) {
+ $errors[] = "第" . ($index + 1) . "行: 该位置已存在地点:{$duplicate['name']}(ID: {$duplicate['id']}),跳过导入";
+ $errorCount++;
+ continue;
+ }
+
+ $categories = '';
+ if (isset($item['categories'])) {
+ if (is_array($item['categories'])) {
+ $categories = implode(',', $item['categories']);
+ } else {
+ $categories = $item['categories'];
+ }
+ }
+
+ $highlights = '';
+ if (isset($item['highlights'])) {
+ if (is_array($item['highlights'])) {
+ $highlights = implode(',', $item['highlights']);
+ } else {
+ $highlights = $item['highlights'];
+ }
+ }
+
+ $photos = '';
+ if (isset($item['photos'])) {
+ if (is_array($item['photos'])) {
+ $photos = implode(',', $item['photos']);
+ } else {
+ $photos = $item['photos'];
+ }
+ }
+
+ $related_articles = '';
+ if (isset($item['related_articles'])) {
+ if (is_array($item['related_articles'])) {
+ $related_articles = implode(',', $item['related_articles']);
+ } else {
+ $related_articles = $item['related_articles'];
+ }
+ }
+
+ $stmt = $this->db->prepare("INSERT INTO plugin_track_footprint
+ (latitude, longitude, name, address, location_type, rating_level, categories, review,
+ description, tags, article_cid, urlLabel, url, photos, date, markerColor,
+ related_articles, highlights, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+ $result = $stmt->execute(array(
+ $item['latitude'] ?? null,
+ $item['longitude'] ?? null,
+ $item['name'] ?? '',
+ $item['address'] ?? null,
+ $item['location_type'] ?? null,
+ $item['rating_level'] ?? 0,
+ $categories,
+ $item['review'] ?? null,
+ $item['description'] ?? null,
+ $item['tags'] ?? null,
+ $item['article_cid'] ?? null,
+ $item['urlLabel'] ?? null,
+ $item['url'] ?? null,
+ $photos,
+ $item['date'] ?? null,
+ $item['markerColor'] ?? null,
+ $related_articles,
+ $highlights,
+ $item['created_at'] ?? date('Y-m-d H:i:s'),
+ $item['updated_at'] ?? date('Y-m-d H:i:s')
+ ));
+
+ if ($result) {
+ $successCount++;
+ } else {
+ $errors[] = "第" . ($index + 1) . "行: 插入失败";
+ $errorCount++;
+ }
+ } catch (PDOException $e) {
+ $errorCount++;
+ $errors[] = "第" . ($index + 1) . "行: " . $e->getMessage();
+ }
+ }
+
+ $this->db->commit();
+
+ $this->clearFootprintCache();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => "导入完成,成功: {$successCount},失败: {$errorCount}",
+ 'data' => array(
+ 'success' => $successCount,
+ 'error' => $errorCount,
+ 'errors' => $errors
+ )
+ ));
+ } catch (Exception $e) {
+ $this->db->rollBack();
+
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '导入失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 导出足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function export()
+ {
+ $format = $this->request->get('format', 'json');
+
+ if (!in_array($format, array('json'))) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '不支持的导出格式'
+ ));
+ }
+
+ try {
+ $stmt = $this->db->query("SELECT
+ id,
+ latitude,
+ longitude,
+ name,
+ address,
+ location_type,
+ rating_level,
+ categories,
+ review,
+ description,
+ tags,
+ article_cid,
+ urlLabel,
+ url,
+ photos,
+ date,
+ markerColor,
+ related_articles,
+ highlights,
+ created_at,
+ updated_at
+ FROM plugin_track_footprint
+ ORDER BY date DESC, created_at DESC");
+
+ $footprints = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (empty($footprints)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '没有数据可导出'
+ ));
+ }
+
+ $filename = 'mytrack_footprints_' . date('Ymd_His') . '.json';
+ $content = json_encode($footprints, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+ header('Content-Type: application/json');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($content));
+ header('Cache-Control: no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+ header('Expires: 0');
+
+ echo $content;
+ exit;
+
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '导出失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 精简导出足迹
+ *
+ * @access public
+ * @return void
+ */
+ public function exportSimple()
+ {
+ $format = $this->request->get('format', 'json');
+
+ if (!in_array($format, array('json'))) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '不支持的导出格式'
+ ));
+ }
+
+ try {
+ $stmt = $this->db->query("SELECT
+ name,
+ latitude,
+ longitude,
+ date,
+ url,
+ urlLabel,
+ categories,
+ markerColor,
+ photos,
+ related_articles,
+ highlights
+ FROM plugin_track_footprint
+ ORDER BY date DESC");
+
+ $footprints = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (empty($footprints)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '没有数据可导出'
+ ));
+ }
+
+ $locations = array();
+ foreach ($footprints as $footprint) {
+ $coordinates = $footprint['longitude'] . ',' . $footprint['latitude'];
+
+ $categories = array();
+ if (!empty($footprint['categories'])) {
+ $categoryArray = explode(',', $footprint['categories']);
+ foreach ($categoryArray as $category) {
+ $category = trim($category);
+ switch($category) {
+ case 'visited':
+ $categories[] = '去过';
+ break;
+ case 'want':
+ $categories[] = '想去';
+ break;
+ case 'plan':
+ $categories[] = '计划';
+ break;
+ default:
+ $categories[] = $category;
+ }
+ }
+ }
+
+ $highlights = !empty($footprint['highlights']) ? explode(',', $footprint['highlights']) : array();
+ $photos = !empty($footprint['photos']) ? explode(',', $footprint['photos']) : array();
+ $relatedArticles = !empty($footprint['related_articles']) ? explode(',', $footprint['related_articles']) : array();
+
+ $locations[] = array(
+ 'name' => $footprint['name'] ?? '',
+ 'coordinates' => $coordinates,
+ 'date' => $footprint['date'] ?? '',
+ 'url' => $footprint['url'] ?? '',
+ 'urlLabel' => $footprint['urlLabel'] ?? '',
+ 'categories' => $categories,
+ 'highlights' => $highlights,
+ 'markerColor' => $footprint['markerColor'] ?? '',
+ 'photos' => $photos,
+ 'relatedArticles' => $relatedArticles
+ );
+ }
+
+ $exportData = array('locations' => $locations);
+ $filename = 'mytrack_simple_' . date('Ymd_His') . '.json';
+ $content = json_encode($exportData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+
+ header('Content-Type: application/json');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($content));
+ header('Cache-Control: no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+ header('Expires: 0');
+
+ echo $content;
+ exit;
+
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '导出失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取所有足迹(用于前台地图显示)
+ *
+ * @access public
+ * @return void
+ */
+ public function getAll()
+ {
+ try {
+ $options = Typecho_Widget::widget('Widget_Options')->plugin('MyTrack');
+ $cacheExpire = isset($options->cacheExpire) && is_numeric($options->cacheExpire) && $options->cacheExpire >= 0
+ ? intval($options->cacheExpire) * 24 * 60 * 60
+ : 7 * 24 * 60 * 60;
+
+ $cacheKey = 'mytrack_footprints_data';
+
+ if ($cacheExpire > 0) {
+ $cacheFile = __DIR__ . '/cache/' . md5($cacheKey) . '.cache';
+
+ if (file_exists($cacheFile) && filemtime($cacheFile) + $cacheExpire > time()) {
+ $cachedData = unserialize(file_get_contents($cacheFile));
+ if ($cachedData !== false) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $cachedData,
+ 'from_cache' => true
+ ));
+ return;
+ }
+ }
+ }
+
+ $stmt = $this->db->query("SELECT * 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['related_articles'])) {
+ $footprint['related_articles_info'] = MyTrack_Plugin::getRelatedArticlesInfo($footprint['related_articles']);
+ }
+ }
+
+ if ($cacheExpire > 0) {
+ $cacheDir = __DIR__ . '/cache';
+ if (!is_dir($cacheDir)) {
+ mkdir($cacheDir, 0755, true);
+ }
+
+ $cacheFile = $cacheDir . '/' . md5($cacheKey) . '.cache';
+ file_put_contents($cacheFile, serialize($validFootprints));
+ }
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $validFootprints,
+ 'from_cache' => false
+ ));
+ } catch (PDOException $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取文章图片
+ *
+ * @access public
+ * @return void
+ */
+ public function getArticleImages()
+ {
+ $article_cid = $this->request->get('article_cid');
+
+ if (empty($article_cid)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '文章CID不能为空'
+ ));
+ }
+
+ try {
+ $articleInfo = MyTrack_Plugin::getArticleInfo($article_cid);
+
+ if (!empty($articleInfo)) {
+ $response = array(
+ 'success' => true,
+ 'message' => '获取文章信息成功',
+ 'images' => $articleInfo['images'] ?? array(),
+ 'text' => $articleInfo['text'] ?? ''
+ );
+
+ if (!empty($articleInfo['title'])) {
+ $response['title'] = $articleInfo['title'];
+ }
+
+ if (!empty($articleInfo['link'])) {
+ $response['link'] = $articleInfo['link'];
+ }
+
+ if (!empty($articleInfo['tags'])) {
+ $response['tags'] = $articleInfo['tags'];
+ }
+
+ if (!empty($articleInfo['created'])) {
+ $response['created'] = $articleInfo['created'];
+ }
+
+ $this->response->throwJson($response);
+ } else {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '该文章中没有找到内容',
+ 'images' => array(),
+ 'text' => ''
+ ));
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取文章信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 清除足迹缓存
+ *
+ * @access private
+ * @return void
+ */
+ private function clearFootprintCache()
+ {
+ try {
+ $cacheDir = __DIR__ . '/cache';
+
+ if (!is_dir($cacheDir)) {
+ return;
+ }
+
+ $files = glob($cacheDir . '/*.cache');
+ foreach ($files as $file) {
+ if (is_file($file)) {
+ unlink($file);
+ }
+ }
+ } catch (Exception $e) {
+ error_log('清除MyTrack缓存失败: ' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Manage.php b/Manage.php
new file mode 100644
index 0000000..0f83fa0
--- /dev/null
+++ b/Manage.php
@@ -0,0 +1,2735 @@
+plugin('MyTrack');
+$webApiKey = isset($pluginOptions->webApiKey) ? $pluginOptions->webApiKey : '';
+
+// 检查是否有搜索参数
+$searchKeyword = isset($_GET['search']) ? trim($_GET['search']) : '';
+$searchResults = array();
+$searchPerformed = false;
+$currentPage = isset($_GET['page']) ? intval($_GET['page']) : 1;
+
+// 如果有关键词,执行搜索
+if ($searchKeyword) {
+ $searchPerformed = true;
+ try {
+ $db = MyTrack_Plugin::getDbConnection();
+
+ // 构建SQL查询条件 - 只搜索地名
+ $whereConditions = array();
+ $params = array();
+
+ // 仅搜索名称(地名)
+ if (!empty($searchKeyword)) {
+ $whereConditions[] = "name LIKE ?";
+ $searchParam = '%' . $searchKeyword . '%';
+ $params = array($searchParam);
+ }
+
+ // 构建完整的WHERE子句
+ $whereSql = '';
+ if (!empty($whereConditions)) {
+ $whereSql = 'WHERE ' . implode(' AND ', $whereConditions);
+ }
+
+ // 获取分页信息
+ $pageSize = 10;
+ $offset = ($currentPage - 1) * $pageSize;
+
+ // 获取总记录数
+ $countSql = "SELECT COUNT(*) FROM plugin_track_footprint " . $whereSql;
+ $countStmt = $db->prepare($countSql);
+ $countStmt->execute($params);
+ $total = $countStmt->fetchColumn();
+ $totalPages = ceil($total / $pageSize);
+
+ // 获取搜索结果
+ $sql = "SELECT * FROM plugin_track_footprint
+ $whereSql
+ ORDER BY date DESC, created_at DESC
+ LIMIT ? OFFSET ?";
+ $stmt = $db->prepare($sql);
+ $limitParams = array_merge($params, array($pageSize, $offset));
+ $stmt->execute($limitParams);
+ $searchResults = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ } catch (Exception $e) {
+ // 搜索出错,不显示结果
+ $searchPerformed = false;
+ }
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugin.php b/Plugin.php
new file mode 100644
index 0000000..b0c1d84
--- /dev/null
+++ b/Plugin.php
@@ -0,0 +1,1855 @@
+contentEx = array('MyTrack_Plugin', 'parseMapShortcode');
+ Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('MyTrack_Plugin', 'parseMapShortcode');
+
+ // 注册文章保存时的处理
+ Typecho_Plugin::factory('Widget_Abstract_Contents')->save = array('MyTrack_Plugin', 'onPostSave');
+ Typecho_Plugin::factory('Widget_Abstract_Contents')->write = array('MyTrack_Plugin', 'onPostWrite');
+
+ // 注册footer钩子来输出JS
+ Typecho_Plugin::factory('Widget_Archive')->footer = array('MyTrack_Plugin', 'footer');
+
+ // 注册header钩子来输出CSS
+ Typecho_Plugin::factory('Widget_Archive')->header = array('MyTrack_Plugin', 'header');
+
+ return _t('插件已激活');
+ }
+
+ /**
+ * 禁用插件方法
+ *
+ * @access public
+ * @return void
+ */
+ public static function deactivate()
+ {
+ // 移除管理菜单 - 使用与activate()相同的索引
+ Helper::removePanel(3, 'MyTrack/Manage.php');
+
+ // 同时从数据库中清理相关记录
+ try {
+ $db = Typecho_Db::get();
+ // 清理options表中的面板记录
+ $db->query($db->delete('table.options')
+ ->where('name = ?', 'panelTable:MyTrack/Manage.php'));
+ } catch (Exception $e) {
+ // 忽略错误
+ }
+
+ Helper::removeAction('track');
+ Helper::removeRoute('track_action');
+
+ return _t('插件已禁用');
+ }
+
+ /**
+ * 获取插件配置面板
+ *
+ * @access public
+ * @param Typecho_Widget_Helper_Form $form 配置面板
+ * @return void
+ */
+ public static function config(Typecho_Widget_Helper_Form $form)
+ {
+ // 高德地图JS API密钥
+ $jsApiKey = new Typecho_Widget_Helper_Form_Element_Text('jsApiKey', null, '',
+ _t('高德地图JS API密钥 *'), _t('用于前端地图显示的API密钥,必填项'));
+ $jsApiKey->addRule('required', _t('JS API密钥不能为空'));
+ $form->addInput($jsApiKey);
+
+ // 高德地图Web服务API密钥
+ $webApiKey = new Typecho_Widget_Helper_Form_Element_Text('webApiKey', null, '',
+ _t('高德地图Web服务API密钥 *'), _t('用于后台地址搜索的API密钥,必填项'));
+ $webApiKey->addRule('required', _t('Web服务API密钥不能为空'));
+ $form->addInput($webApiKey);
+
+ // 默认缩放级别
+ $zoomLevel = new Typecho_Widget_Helper_Form_Element_Text('zoomLevel', null, '15',
+ _t('默认缩放级别 *'), _t('设置地图默认缩放级别,范围3-18,默认为15,必填项'));
+ $zoomLevel->addRule('required', _t('缩放级别不能为空'))
+ ->addRule(function($value) {
+ return is_numeric($value) && $value >= 3 && $value <= 18;
+ }, _t('缩放级别必须是3-18之间的数字'));
+ $form->addInput($zoomLevel);
+
+ // 默认视图模式
+ $viewMode = new Typecho_Widget_Helper_Form_Element_Radio('viewMode', array(
+ '2D' => '2D视图',
+ '3D' => '3D视图'
+ ), '2D', _t('默认视图模式'));
+ $form->addInput($viewMode);
+
+ // 默认地图主题
+ $mapTheme = new Typecho_Widget_Helper_Form_Element_Select('mapTheme', array(
+ 'normal' => '标准',
+ 'dark' => '幻影黑',
+ 'light' => '月光银',
+ 'whitesmoke' => '远山黛',
+ 'fresh' => '草色青',
+ 'grey' => '雅士灰',
+ 'graffiti' => '涂鸦',
+ 'macaron' => '马卡龙',
+ 'blue' => '靛青蓝',
+ 'darkblue' => '极夜蓝',
+ 'wine' => '酱籽'
+ ), 'normal', _t('默认地图主题'));
+ $form->addInput($mapTheme);
+
+ // 前台显示设置
+ $enableDisplay = new Typecho_Widget_Helper_Form_Element_Radio('enableDisplay', array(
+ 1 => '是',
+ 0 => '否'
+ ), 1, _t('显示前台足迹地图'), _t('关闭后前台将不显示足迹地图,但后台管理功能仍可使用'));
+ $form->addInput($enableDisplay);
+
+ // 缓存时间设置
+ $cacheExpire = new Typecho_Widget_Helper_Form_Element_Text('cacheExpire', null, '7',
+ _t('缓存时间(天)*'), _t('设置数据缓存的有效期,单位为天,默认为7天,0表示关闭缓存'));
+ $cacheExpire->addRule('required', _t('缓存时间不能为空'))
+ ->addRule(function($value) {
+ return is_numeric($value) && $value >= 0 && is_int($value + 0);
+ }, _t('缓存时间必须是一个大于等于0的整数'));
+ $form->addInput($cacheExpire);
+
+ // 点聚合功能设置
+ $enableCluster = new Typecho_Widget_Helper_Form_Element_Radio('enableCluster', array(
+ 1 => '是',
+ 0 => '否'
+ ), 0, _t('启用点聚合功能'), _t('启用后,当地图上标记点较多时会自动聚合,提高性能和可读性'));
+ $form->addInput($enableCluster);
+
+ // 空内容标记点颜色设置
+ $emptyMarkerColor = new Typecho_Widget_Helper_Form_Element_Text('emptyMarkerColor', null, '#2196F3',
+ _t('空内容标记点颜色'), _t('设置空内容标记点的颜色'));
+ $emptyMarkerColor->input->setAttribute('type', 'color');
+ $form->addInput($emptyMarkerColor);
+
+ // 有内容标记点颜色设置
+ $contentMarkerColor = new Typecho_Widget_Helper_Form_Element_Text('contentMarkerColor', null, '#4CAF50',
+ _t('有内容标记点颜色'), _t('设置有内容标记点的颜色'));
+ $contentMarkerColor->input->setAttribute('type', 'color');
+ $form->addInput($contentMarkerColor);
+
+ // 聚合标记点颜色设置
+ $clusterMarkerColor1 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor1', null, '#2196F3',
+ _t('聚合标记点颜色1'), _t('设置小规模聚合标记点的颜色'));
+ $clusterMarkerColor1->input->setAttribute('type', 'color');
+ $form->addInput($clusterMarkerColor1);
+
+ $clusterMarkerColor2 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor2', null, '#FF9800',
+ _t('聚合标记点颜色2'), _t('设置中等规模聚合标记点的颜色'));
+ $clusterMarkerColor2->input->setAttribute('type', 'color');
+ $form->addInput($clusterMarkerColor2);
+
+ $clusterMarkerColor3 = new Typecho_Widget_Helper_Form_Element_Text('clusterMarkerColor3', null, '#FF5722',
+ _t('聚合标记点颜色3'), _t('设置大规模聚合标记点的颜色'));
+ $clusterMarkerColor3->input->setAttribute('type', 'color');
+ $form->addInput($clusterMarkerColor3);
+
+ // 短代码地图卡片样式设置
+ $cardMapWidth = new Typecho_Widget_Helper_Form_Element_Text('cardMapWidth', null, '40%',
+ _t('短代码地图宽度'), _t('设置短代码地图卡片的宽度,例如:40% 或 300px'));
+ $form->addInput($cardMapWidth);
+
+ $cardMapHeight = new Typecho_Widget_Helper_Form_Element_Text('cardMapHeight', null, '320px',
+ _t('短代码地图高度'), _t('设置短代码地图卡片的高度'));
+ $form->addInput($cardMapHeight);
+
+ $cardMapZoom = new Typecho_Widget_Helper_Form_Element_Text('cardMapZoom', null, '16',
+ _t('短代码地图缩放级别'), _t('设置短代码地图的缩放级别,范围3-18'));
+ $form->addInput($cardMapZoom);
+ }
+
+ /**
+ * 个人用户的配置面板
+ *
+ * @access public
+ * @param Typecho_Widget_Helper_Form $form
+ * @return void
+ */
+ public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
+
+ /**
+ * 输出CSS到页面头部
+ *
+ * @access public
+ */
+ public static function header()
+ {
+ if (!self::$cssAdded) {
+ echo '';
+ self::$cssAdded = true;
+ }
+ }
+
+ /**
+ * 输出JS到页面底部
+ *
+ * @access public
+ */
+ public static function footer()
+ {
+ if (!empty(self::$mapConfigs) && !self::$jsAdded) {
+ echo self::getMapCardScripts(self::$mapConfigs);
+ self::$jsAdded = true;
+ }
+ }
+
+ /**
+ * 初始化数据库路径
+ * 优先使用已存在的数据库文件,如果没有则生成新的随机名称
+ *
+ * @access private
+ * @return void
+ */
+ private static function initDbPath()
+ {
+ $dbDir = __DIR__ . '/db';
+
+ // 确保目录存在
+ if (!is_dir($dbDir)) {
+ mkdir($dbDir, 0755, true);
+ }
+
+ // 检查是否已存在.db文件(优先使用已存在的)
+ $dbFiles = glob($dbDir . '/track_*.db');
+
+ if (!empty($dbFiles)) {
+ // 使用第一个找到的数据库文件(保持一致性)
+ self::$dbPath = $dbFiles[0];
+ } else {
+ // 没有找到现有数据库文件,生成新的随机名称
+ $randomStr = substr(md5(uniqid(rand(), true)), 0, 10);
+ self::$dbPath = $dbDir . '/track_' . $randomStr . '.db';
+ }
+ }
+
+ /**
+ * 初始化数据库
+ *
+ * @access private
+ * @return void
+ */
+ private static function initDatabase()
+ {
+ // 确保数据库路径已初始化
+ if (empty(self::$dbPath)) {
+ self::initDbPath();
+ }
+
+ try {
+ // 创建SQLite数据库连接
+ $db = new PDO('sqlite:' . self::$dbPath);
+ $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // 读取SQL初始化文件
+ $sqlFile = __DIR__ . '/db/init.sql';
+ if (file_exists($sqlFile)) {
+ $sql = file_get_contents($sqlFile);
+ $db->exec($sql);
+ } else {
+ // 如果没有SQL文件,手动创建表
+ $db->exec("CREATE TABLE IF NOT EXISTS plugin_track_footprint (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ latitude REAL NOT NULL,
+ longitude REAL NOT NULL,
+ name TEXT NOT NULL,
+ address TEXT,
+ location_type TEXT,
+ rating_level INTEGER,
+ categories TEXT,
+ review TEXT,
+ description TEXT,
+ article_cid INTEGER,
+ urlLabel TEXT,
+ url TEXT,
+ photos TEXT,
+ tags TEXT,
+ date DATETIME,
+ markerColor TEXT,
+ related_articles TEXT,
+ highlights TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )");
+
+ // 创建更新时间触发器
+ $db->exec("CREATE TRIGGER IF NOT EXISTS update_footprint_time
+ AFTER UPDATE ON plugin_track_footprint
+ BEGIN
+ UPDATE plugin_track_footprint SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
+ END");
+ }
+
+ $db = null;
+ } catch (PDOException $e) {
+ throw new Typecho_Plugin_Exception('数据库初始化失败: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取数据库连接
+ *
+ * @access public
+ * @return PDO
+ */
+ public static function getDbConnection()
+ {
+ // 确保数据库路径已初始化
+ if (empty(self::$dbPath)) {
+ self::initDbPath();
+ }
+
+ // 如果数据库文件不存在,初始化数据库
+ if (!file_exists(self::$dbPath)) {
+ self::initDatabase();
+ }
+
+ try {
+ $db = new PDO('sqlite:' . self::$dbPath);
+ $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ return $db;
+ } catch (PDOException $e) {
+ throw new Typecho_Plugin_Exception('数据库连接失败: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 通过文章CID获取文章信息和图片
+ *
+ * @access public
+ * @param integer $cid 文章CID
+ * @return array
+ */
+ public static function getArticleInfo($cid)
+ {
+ $result = array(
+ 'title' => '',
+ 'link' => '',
+ 'text' => '',
+ 'images' => array(),
+ 'tags' => array(),
+ 'created' => ''
+ );
+
+ if (empty($cid)) {
+ return $result;
+ }
+
+ try {
+ // 获取文章信息,包括内容
+ $db = Typecho_Db::get();
+ $article = $db->fetchRow($db->select('title', 'slug', 'created', 'text')
+ ->from('table.contents')
+ ->where('cid = ?', $cid)
+ ->where('type = ?', 'post')
+ ->where('status = ?', 'publish')
+ ->limit(1));
+
+ if ($article) {
+ $result['title'] = $article['title'];
+ $result['link'] = self::getPostUrlByCid($cid);
+ $result['text'] = $article['text'];
+ $result['created'] = $article['created'];
+
+ // 从文章内容中提取图片(限制4张)
+ if (!empty($article['text'])) {
+ $result['images'] = self::getPostImagesByCid($cid);
+ }
+
+ // 获取文章标签
+ $result['tags'] = self::getPostTagsByCid($cid);
+ }
+ } catch (Exception $e) {
+ error_log('MyTrack: 获取文章信息失败: ' . $e->getMessage());
+ }
+
+ return $result;
+ }
+
+ /**
+ * 通过文章CID获取文章URL
+ *
+ * @access public
+ * @param integer $cid 文章CID
+ * @return string|false
+ */
+ public static function getPostUrlByCid($cid)
+ {
+ try {
+ // 获取文章信息,包括日期和分类
+ $db = Typecho_Db::get();
+ $row = $db->fetchRow($db->select('slug', 'type', 'created')
+ ->from('table.contents')
+ ->where('cid = ?', $cid)
+ ->where('status = ?', 'publish'));
+
+ if (!$row) {
+ return false;
+ }
+
+ // 获取文章分类
+ $category = '';
+ $categories = $db->fetchAll($db->select('slug')
+ ->from('table.metas')
+ ->join('table.relationships', 'table.metas.mid = table.relationships.mid')
+ ->where('table.relationships.cid = ?', $cid)
+ ->where('table.metas.type = ?', 'category')
+ ->order('table.metas.order', Typecho_Db::SORT_ASC));
+
+ if (!empty($categories)) {
+ $category = $categories[0]['slug'];
+ }
+
+ // 准备URL参数
+ $date = getdate($row['created']);
+ $params = array(
+ 'cid' => $cid,
+ 'slug' => $row['slug'],
+ 'category' => $category,
+ 'directory' => $category,
+ 'year' => $date['year'],
+ 'month' => str_pad($date['mon'], 2, '0', STR_PAD_LEFT),
+ 'day' => str_pad($date['mday'], 2, '0', STR_PAD_LEFT)
+ );
+
+ // 获取自定义路径设置
+ $options = Typecho_Widget::widget('Widget_Options');
+
+ // 尝试获取不同的永久链接设置
+ $permalinkStructure = '';
+
+ // 方法1: 直接从options获取
+ if (isset($options->permalink)) {
+ $permalinkStructure = $options->permalink;
+ }
+
+ // 方法2: 尝试从routingTable获取
+ if (empty($permalinkStructure) && isset($options->routingTable)) {
+ $routingTable = $options->routingTable;
+ if (isset($routingTable['post'])) {
+ $permalinkStructure = $routingTable['post']['url'];
+ }
+ }
+
+ // 方法3: 尝试从Typecho_Router获取
+ if (empty($permalinkStructure)) {
+ $router = Typecho_Router::get();
+ $routes = $router->getRoutes();
+ if (isset($routes['post'])) {
+ $permalinkStructure = $routes['post']['url'];
+ }
+ }
+
+ // 如果没有找到自定义路径设置,使用默认方式
+ if (empty($permalinkStructure)) {
+ return Typecho_Router::url($row['type'], $row, $options->index);
+ }
+
+ // 替换路径中的占位符
+ $url = $permalinkStructure;
+
+ // 处理Typecho特殊格式的占位符
+ $url = preg_replace('/\[year:digital:4\]/', $params['year'], $url);
+ $url = preg_replace('/\[month:digital:2\]/', $params['month'], $url);
+ $url = preg_replace('/\[day:digital:2\]/', $params['day'], $url);
+ $url = preg_replace('/\[slug\]/', $params['slug'], $url);
+ $url = preg_replace('/\[cid\]/', $params['cid'], $url);
+ $url = preg_replace('/\[category\]/', $params['category'], $url);
+ $url = preg_replace('/\[directory\]/', $params['directory'], $url);
+
+ // 替换标准占位符
+ foreach ($params as $key => $value) {
+ $url = str_replace('{' . $key . '}', $value, $url);
+ }
+
+ // 确保URL以/开头
+ if (strpos($url, '/') !== 0) {
+ $url = '/' . $url;
+ }
+
+ // 拼接完整URL
+ $fullUrl = rtrim($options->siteUrl, '/') . $url;
+
+ return $fullUrl;
+
+ } catch (Exception $e) {
+ error_log("MyTrack: 获取文章 URL 失败: " . $e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * 通过文章CID获取文章中的图片
+ *
+ * @access public
+ * @param integer $cid 文章CID
+ * @return array
+ */
+ public static function getPostImagesByCid($cid)
+ {
+ $db = Typecho_Db::get();
+ $content = $db->fetchRow($db->select('text')
+ ->from('table.contents')
+ ->where('cid = ?', $cid)
+ ->where('type = ?', 'post')
+ ->where('status = ?', 'publish'));
+
+ if (!$content) {
+ return array();
+ }
+
+ // 尝试多种图片匹配模式
+ $images = array();
+ $text = $content['text'];
+
+ // 模式1: 标准img标签
+ preg_match_all('/
]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $text, $matches1);
+ if (isset($matches1[1]) && !empty($matches1[1])) {
+ foreach($matches1[1] as $imageUrl) {
+ $images[] = self::processImageUrl($imageUrl);
+ }
+ }
+
+ // 模式2: markdown图片语法 
+ preg_match_all('/!\[[^\]]*\]\(([^)]+)\)/i', $text, $matches2);
+ if (isset($matches2[1]) && !empty($matches2[1])) {
+ foreach($matches2[1] as $imageUrl) {
+ $images[] = self::processImageUrl($imageUrl);
+ }
+ }
+
+ // 模式3: 直接匹配图片URL
+ preg_match_all('/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/i', $text, $matches3);
+ if (isset($matches3[0]) && !empty($matches3[0])) {
+ foreach($matches3[0] as $imageUrl) {
+ $images[] = self::processImageUrl($imageUrl);
+ }
+ }
+
+ // 模式4: 匹配相对路径的图片URL
+ preg_match_all('/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/i', $text, $matches4);
+ if (isset($matches4[0]) && !empty($matches4[0])) {
+ foreach($matches4[0] as $imageUrl) {
+ if (strpos($imageUrl, 'http') !== 0) {
+ $images[] = self::processImageUrl($imageUrl);
+ }
+ }
+ }
+
+ // 去重并过滤空值
+ $images = array_filter(array_unique($images));
+
+ // 限制最多4张图片
+ $images = array_slice($images, 0, 4);
+
+ return $images;
+ }
+
+ /**
+ * 通过文章CID获取文章标签
+ *
+ * @access public
+ * @param integer $cid 文章CID
+ * @return array 标签数组
+ */
+ public static function getPostTagsByCid($cid)
+ {
+ try {
+ $db = Typecho_Db::get();
+
+ // 查询文章的标签信息
+ $tags = $db->fetchAll($db->select('table.metas.name', 'table.metas.slug')
+ ->from('table.metas')
+ ->join('table.relationships', 'table.metas.mid = table.relationships.mid')
+ ->where('table.relationships.cid = ?', $cid)
+ ->where('table.metas.type = ?', 'tag')
+ ->order('table.metas.order', Typecho_Db::SORT_ASC));
+
+ $tagNames = array();
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ $tagNames[] = $tag['name'];
+ }
+ }
+
+ return $tagNames;
+ } catch (Exception $e) {
+ error_log('MyTrack: 获取文章标签失败: ' . $e->getMessage());
+ return array();
+ }
+ }
+
+ /**
+ * 处理图片URL,将相对路径转换为绝对路径
+ *
+ * @access private
+ * @param string $imageUrl 原始图片URL
+ * @return string 处理后的图片URL
+ */
+ private static function processImageUrl($imageUrl)
+ {
+ // 处理相对路径
+ if (strpos($imageUrl, 'http') !== 0) {
+ $options = Typecho_Widget::widget('Widget_Options');
+ $imageUrl = Typecho_Common::url($imageUrl, $options->siteUrl);
+ }
+
+ return $imageUrl;
+ }
+
+ /**
+ * 数据库迁移
+ * 更新数据库字段,添加新字段
+ *
+ * @access private
+ * @return void
+ */
+ private static function migrateDatabase()
+ {
+ try {
+ // 确保数据库路径已初始化
+ if (empty(self::$dbPath)) {
+ self::initDbPath();
+ }
+
+ $db = new PDO('sqlite:' . self::$dbPath);
+ $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // 检查表结构
+ $stmt = $db->prepare("PRAGMA table_info(plugin_track_footprint)");
+ $stmt->execute();
+ $columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $hasName = false;
+ $hasDate = false;
+ $hasUrl = false;
+ $hasUrlLabel = false;
+ $hasPhotos = false;
+ $hasCategories = false;
+ $hasMarkerColor = false;
+ $hasRelatedArticles = false;
+ $hasHighlights = false;
+
+ // 检查字段是否存在
+ foreach ($columns as $column) {
+ if ($column['name'] === 'name') {
+ $hasName = true;
+ }
+ if ($column['name'] === 'date') {
+ $hasDate = true;
+ }
+ if ($column['name'] === 'url') {
+ $hasUrl = true;
+ }
+ if ($column['name'] === 'urlLabel') {
+ $hasUrlLabel = true;
+ }
+ if ($column['name'] === 'photos') {
+ $hasPhotos = true;
+ }
+ if ($column['name'] === 'categories') {
+ $hasCategories = true;
+ }
+ if ($column['name'] === 'markerColor') {
+ $hasMarkerColor = true;
+ }
+ if ($column['name'] === 'related_articles') {
+ $hasRelatedArticles = true;
+ }
+ if ($column['name'] === 'highlights') {
+ $hasHighlights = true;
+ }
+ }
+
+ // 重命名字段:location 改为 name
+ if (!$hasName) {
+ // 检查是否存在 location 字段
+ $hasLocation = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'location') {
+ $hasLocation = true;
+ break;
+ }
+ }
+
+ if ($hasLocation) {
+ // 重命名 location 为 name
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN location TO name");
+ } else {
+ // 直接添加 name 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN name TEXT");
+ }
+ }
+
+ // 重命名字段:travel_time 改为 date
+ if (!$hasDate) {
+ // 检查是否存在 travel_time 字段
+ $hasTravelTime = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'travel_time') {
+ $hasTravelTime = true;
+ break;
+ }
+ }
+
+ if ($hasTravelTime) {
+ // 重命名 travel_time 为 date
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN travel_time TO date");
+ } else {
+ // 直接添加 date 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN date DATETIME");
+ }
+ }
+
+ // 重命名字段:article_link 改为 url
+ if (!$hasUrl) {
+ // 检查是否存在 article_link 字段
+ $hasArticleLink = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'article_link') {
+ $hasArticleLink = true;
+ break;
+ }
+ }
+
+ if ($hasArticleLink) {
+ // 重命名 article_link 为 url
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN article_link TO url");
+ } else {
+ // 直接添加 url 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN url TEXT");
+ }
+ }
+
+ // 重命名字段:article_title 改为 urlLabel
+ if (!$hasUrlLabel) {
+ // 检查是否存在 article_title 字段
+ $hasArticleTitle = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'article_title') {
+ $hasArticleTitle = true;
+ break;
+ }
+ }
+
+ if ($hasArticleTitle) {
+ // 重命名 article_title 为 urlLabel
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN article_title TO urlLabel");
+ } else {
+ // 直接添加 urlLabel 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN urlLabel TEXT");
+ }
+ }
+
+ // 重命名字段:image_links 改为 photos
+ if (!$hasPhotos) {
+ // 检查是否存在 image_links 字段
+ $hasImageLinks = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'image_links') {
+ $hasImageLinks = true;
+ break;
+ }
+ }
+
+ if ($hasImageLinks) {
+ // 重命名 image_links 为 photos
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN image_links TO photos");
+ } else {
+ // 直接添加 photos 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN photos TEXT");
+ }
+ }
+
+ // 重命名字段:status 改为 categories
+ if (!$hasCategories) {
+ // 检查是否存在 status 字段
+ $hasStatus = false;
+ foreach ($columns as $column) {
+ if ($column['name'] === 'status') {
+ $hasStatus = true;
+ break;
+ }
+ }
+
+ if ($hasStatus) {
+ // 重命名 status 为 categories
+ $db->exec("ALTER TABLE plugin_track_footprint RENAME COLUMN status TO categories");
+ } else {
+ // 直接添加 categories 字段
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN categories TEXT");
+ }
+ }
+
+ // 添加新字段:markerColor
+ if (!$hasMarkerColor) {
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN markerColor TEXT");
+ }
+
+ // 添加新字段:related_articles
+ if (!$hasRelatedArticles) {
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN related_articles TEXT");
+ }
+
+ // 添加新字段:highlights
+ if (!$hasHighlights) {
+ $db->exec("ALTER TABLE plugin_track_footprint ADD COLUMN highlights TEXT");
+ }
+
+ $db = null;
+ } catch (PDOException $e) {
+ error_log('MyTrack数据库迁移失败: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取数据库文件路径
+ *
+ * @access public
+ * @return string
+ */
+ public static function getDbPath()
+ {
+ if (empty(self::$dbPath)) {
+ self::initDbPath();
+ }
+ return self::$dbPath;
+ }
+
+ /**
+ * 通过足迹ID获取足迹信息
+ *
+ * @access public
+ * @param integer $id 足迹ID
+ * @return array|null 足迹信息或null
+ */
+ public static function getFootprintById($id)
+ {
+ if (empty($id)) {
+ return null;
+ }
+
+ try {
+ $db = self::getDbConnection();
+ $stmt = $db->prepare("SELECT * FROM plugin_track_footprint WHERE id = ?");
+ $stmt->execute(array($id));
+ $footprint = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($footprint) {
+ // 处理分类字段
+ if (!empty($footprint['categories'])) {
+ $footprint['categories'] = explode(',', $footprint['categories']);
+ } else {
+ $footprint['categories'] = array();
+ }
+
+ // 处理亮点字段
+ if (!empty($footprint['highlights'])) {
+ $footprint['highlights'] = explode(',', $footprint['highlights']);
+ } else {
+ $footprint['highlights'] = array();
+ }
+
+ // 获取文章信息(如果有关联文章)
+ if (!empty($footprint['article_cid'])) {
+ $articleInfo = self::getArticleInfo($footprint['article_cid']);
+ if (!empty($articleInfo['title']) && empty($footprint['urlLabel'])) {
+ $footprint['urlLabel'] = $articleInfo['title'];
+ }
+ if (!empty($articleInfo['link']) && empty($footprint['url'])) {
+ $footprint['url'] = $articleInfo['link'];
+ }
+ }
+
+ // 处理关联文章信息
+ if (!empty($footprint['related_articles'])) {
+ $footprint['related_articles_info'] = self::getRelatedArticlesInfo($footprint['related_articles']);
+ }
+
+ return $footprint;
+ }
+ } catch (Exception $e) {
+ error_log('MyTrack: 获取足迹信息失败: ' . $e->getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * 获取关联文章信息
+ *
+ * @access public
+ * @param string $relatedArticles 关联文章ID字符串(逗号分隔)
+ * @return array 关联文章信息数组
+ */
+ public static function getRelatedArticlesInfo($relatedArticles)
+ {
+ $result = array();
+
+ if (empty($relatedArticles)) {
+ return $result;
+ }
+
+ $articleIds = explode(',', $relatedArticles);
+ $articleIds = array_map('trim', $articleIds);
+ $articleIds = array_filter($articleIds);
+
+ foreach ($articleIds as $cid) {
+ if (is_numeric($cid)) {
+ $articleInfo = self::getArticleInfo($cid);
+ if ($articleInfo) {
+ $result[$cid] = array(
+ 'title' => $articleInfo['title'],
+ 'link' => $articleInfo['link'],
+ 'cid' => $cid
+ );
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析文章内容中的地图短代码
+ *
+ * @access public
+ * @param string $content 文章内容
+ * @param Widget_Abstract_Contents $widget 小部件对象
+ * @param array $lastResult 上次解析结果
+ * @return string 处理后的内容
+ */
+ public static function parseMapShortcode($content, $widget, $lastResult)
+ {
+ $content = empty($lastResult) ? $content : $lastResult;
+
+ // 匹配 {map-数字} 格式的短代码
+ $pattern = '/\{map-(\d+)\}/i';
+
+ if (preg_match_all($pattern, $content, $matches)) {
+ // 获取当前文章的CID
+ $currentCid = isset($widget->cid) ? $widget->cid : 0;
+
+ // 收集所有需要渲染的地图ID
+ $mapIds = $matches[1];
+
+ $mapIndex = 0;
+
+ foreach ($mapIds as $index => $mapId) {
+ // 渲染单个地图卡片
+ list($mapHtml, $config) = self::renderSingleMapCard($mapId, $mapIndex);
+
+ if ($mapHtml) {
+ $content = str_replace($matches[0][$index], $mapHtml, $content);
+
+ if ($config) {
+ self::$mapConfigs[] = $config;
+ }
+
+ // 在短代码解析时同步更新关联文章
+ if ($currentCid > 0) {
+ self::syncRelatedArticles($mapId, $currentCid);
+ }
+ } else {
+ $content = str_replace($matches[0][$index], '足迹ID ' . $mapId . ' 不存在
', $content);
+ }
+
+ $mapIndex++;
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * 渲染单个地图卡片HTML
+ *
+ * @access private
+ * @param integer $mapId 足迹ID
+ * @param integer $index 索引
+ * @return array [HTML字符串, 配置数组]
+ */
+ private static function renderSingleMapCard($mapId, $index)
+ {
+ // 获取足迹信息
+ $footprint = self::getFootprintById($mapId);
+ if (!$footprint) {
+ return array(null, null);
+ }
+
+ // 获取插件配置
+ $options = Typecho_Widget::widget('Widget_Options')->plugin('MyTrack');
+
+ // 获取配置
+ $cardMapWidth = isset($options->cardMapWidth) ? $options->cardMapWidth : '40%';
+ $cardMapHeight = isset($options->cardMapHeight) ? $options->cardMapHeight : '350px';
+
+ // 处理分类显示
+ $categoriesHtml = '';
+ if (!empty($footprint['categories'])) {
+ foreach ($footprint['categories'] as $category) {
+ $category = trim($category);
+ $class = '';
+ $text = '';
+
+ switch($category) {
+ case 'visited':
+ $class = 'mytrack-categories-visited-badge';
+ $text = '已玩';
+ break;
+ case 'want':
+ $class = 'mytrack-categories-want-badge';
+ $text = '向往';
+ break;
+ case 'plan':
+ $class = 'mytrack-categories-plan-badge';
+ $text = '计划';
+ break;
+ default:
+ $class = 'mytrack-categories-badge';
+ $text = $category;
+ }
+
+ $categoriesHtml .= '' . $text . '';
+ }
+ }
+
+ // 处理星级显示
+ $ratingHtml = '';
+ if (!empty($footprint['rating_level']) && $footprint['rating_level'] > 0) {
+ $ratingHtml = '';
+ for ($i = 1; $i <= 5; $i++) {
+ if ($i <= $footprint['rating_level']) {
+ $ratingHtml .= '★';
+ } else {
+ $ratingHtml .= '☆';
+ }
+ }
+ $ratingHtml .= '
';
+ }
+
+ // 处理亮点显示
+ $highlightsHtml = '';
+ if (!empty($footprint['highlights'])) {
+ $highlightsHtml = '亮点:';
+ foreach ($footprint['highlights'] as $highlight) {
+ $highlight = trim($highlight);
+ if ($highlight) {
+ $highlightsHtml .= '' . htmlspecialchars($highlight) . ' ';
+ }
+ }
+ $highlightsHtml .= '
';
+ }
+
+ // 处理关联文章显示
+ $relatedArticlesHtml = '';
+ if (!empty($footprint['related_articles_info'])) {
+ $relatedArticlesHtml .= '';
+ $relatedArticlesHtml .= '
这些文章也提到了本地点:';
+ foreach ($footprint['related_articles_info'] as $articleInfo) {
+ if (!empty($articleInfo['title']) && !empty($articleInfo['link'])) {
+ $relatedArticlesHtml .= '
';
+ }
+ }
+ $relatedArticlesHtml .= '
';
+ }
+
+ // 生成唯一ID
+ $uniqueId = 'map_' . $mapId . '_' . $index;
+ $mapCardId = 'mytrack-card-' . $uniqueId;
+ $mapContainerId = $mapCardId . '-map';
+
+ // 构建HTML
+ $html = '
+
+
+
+ ';
+
+ if ($footprint['address']) {
+ $html .= '
地址:' . htmlspecialchars($footprint['address']) . '
';
+ }
+
+ if ($footprint['location_type']) {
+ $html .= '
类型:' . htmlspecialchars($footprint['location_type']) . '
';
+ }
+
+ if ($categoriesHtml) {
+ $html .= '
分类:' . $categoriesHtml . '
';
+ }
+
+ if ($ratingHtml) {
+ $html .= '
推荐:' . $ratingHtml . '
';
+ }
+
+ $html .= $highlightsHtml;
+
+ if ($footprint['review']) {
+ $html .= '
简评:' . nl2br(htmlspecialchars($footprint['review'])) . '
';
+ }
+
+ $html .= $relatedArticlesHtml;
+
+ $html .= '
+
+
+
';
+
+ // 准备地图配置
+ $apiKey = isset($options->jsApiKey) ? $options->jsApiKey : '';
+ $cardMapZoom = isset($options->cardMapZoom) ? intval($options->cardMapZoom) : 16;
+ $mapTheme = isset($options->mapTheme) ? $options->mapTheme : 'normal';
+
+ // 修复:正确获取markerColor和对应的颜色值
+ $markerColor = isset($footprint['markerColor']) && !empty($footprint['markerColor']) ? trim($footprint['markerColor']) : '';
+ $colorValue = self::getMarkerColorValue($markerColor);
+
+ $config = array(
+ 'containerId' => $mapContainerId,
+ 'mapId' => $mapId,
+ 'name' => $footprint['name'],
+ 'latitude' => floatval($footprint['latitude']),
+ 'longitude' => floatval($footprint['longitude']),
+ 'markerColor' => $markerColor,
+ 'markerColorValue' => $colorValue,
+ 'zoom' => $cardMapZoom,
+ 'theme' => $mapTheme,
+ 'apiKey' => $apiKey
+ );
+
+ return array($html, $config);
+ }
+
+ /**
+ * 根据markerColor标识获取具体的颜色值
+ *
+ * @access private
+ * @param string $markerColor 标记颜色标识
+ * @return string 具体的颜色值
+ */
+ private static function getMarkerColorValue($markerColor)
+ {
+ // 如果markerColor为空,返回默认红色
+ if (empty($markerColor) || !is_string($markerColor)) {
+ return '#ff0000';
+ }
+
+ $markerColor = trim($markerColor);
+ if ($markerColor === '') {
+ return '#ff0000';
+ }
+
+ // 颜色映射表
+ $colorMap = array(
+ 'sunset' => 'linear-gradient(135deg, rgb(255, 179, 71), rgb(255, 111, 97))',
+ 'ocean' => 'linear-gradient(135deg, rgb(6, 190, 182), rgb(72, 177, 191))',
+ 'forest' => 'linear-gradient(135deg, rgb(94, 231, 223), rgb(57, 163, 124))',
+ 'amber' => 'linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133))',
+ 'violet' => 'linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235))',
+ 'citrus' => 'linear-gradient(135deg, rgb(253, 251, 143), rgb(161, 255, 206))'
+ );
+
+ // 如果找到对应的颜色,返回具体的颜色值,否则返回默认的红色
+ if (isset($colorMap[$markerColor])) {
+ return $colorMap[$markerColor];
+ }
+
+ return '#ff0000'; // 默认红色
+ }
+
+ /**
+ * 获取地图卡片CSS样式 - 修复标点颜色问题
+ *
+ * @access private
+ * @return string CSS样式
+ */
+ private static function getMapCardStyles()
+ {
+ return '
+ /* MyTrack 插件专用样式 - 防止与其他插件冲突 */
+ .mytrack-card {
+ border: 1px solid #e8e8e8;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ background: white;
+ margin: 20px 0;
+ }
+
+ .mytrack-card-content {
+ display: flex;
+ }
+
+ .mytrack-card-info {
+ flex: 1;
+ padding: 20px;
+ min-width: 0;
+ }
+
+ .mytrack-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+ }
+
+ .mytrack-card-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #333 !important; /* 修复标题颜色,确保显示 */
+ }
+
+ .mytrack-card-categories {
+ margin-top: 10px;
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .mytrack-categories-badge {
+ font-size: 14px;
+ background: #f0f0f0;
+ color: #666;
+ }
+
+ .mytrack-categories-visited-badge {
+ background: #fff;
+ color: #666;
+ }
+
+ .mytrack-categories-want-badge {
+ background: #e3f2fd;
+ color: #1565c0;
+ }
+
+ .mytrack-categories-plan-badge {
+ background: #fff3e0;
+ color: #ef6c00;
+ }
+
+ .mytrack-card-info > div {
+ margin-bottom: 10px;
+ font-size: 14px;
+ line-height: 1.5;
+ }
+
+ .mytrack-card-info strong {
+ color: #555;
+ font-weight: 600;
+ min-width: 40px;
+ display: inline-block;
+ }
+
+ .mytrack-card-address,
+ .mytrack-card-type,
+ .mytrack-card-review {
+ color: #666;
+ }
+
+ .mytrack-card-highlights {
+ color: #666;
+ }
+
+ .mytrack-highlight-badge {
+ background-color: #e8f5e9;
+ color: #2e7d32;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ margin-right: 5px;
+ display: inline-block;
+ }
+
+ .mytrack-card-rating {
+ display: flex;
+ align-items: center;
+ }
+
+ .mytrack-rating-display {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .mytrack-rating-star {
+ color: #ffc107;
+ font-size: 14px;
+ margin-right: 2px;
+ }
+
+ .mytrack-rating-empty-star {
+ color: #ddd;
+ font-size: 14px;
+ margin-right: 2px;
+ }
+
+ .mytrack-related-articles-section {
+ margin-top: 15px;
+ padding-top: 10px;
+ border-top: 1px dashed #eee;
+ }
+
+ .mytrack-related-articles-list {
+ margin: 5px 0 0 0;
+ padding: 0;
+ }
+
+ .mytrack-related-article-item {
+ margin-bottom: 5px;
+ font-size: 13px;
+ }
+
+ .mytrack-related-article-item a {
+ color: #3b82f6;
+ text-decoration: none;
+ }
+
+ .mytrack-related-article-item a:hover {
+ text-decoration: underline;
+ }
+
+ .mytrack-card-map {
+ position: relative;
+ background: #f5f5f5;
+ }
+
+ /* 修复地图标记点样式 - 移除有问题的inherit规则 */
+ .mytrack-marker-custom {
+ width: 20px !important;
+ height: 20px !important;
+ border-radius: 50% !important;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important;
+ cursor: pointer !important;
+ transition: transform 0.2s ease-out !important;
+ border: 2px solid #fff !important;
+ position: relative !important;
+ z-index: 100 !important;
+ display: block !important;
+ background: #ff0000 !important; /* 默认红色,会被JS内联样式覆盖 */
+ }
+
+ .mytrack-marker-custom:hover {
+ transform: scale(1.05) !important;
+ }
+
+ /* 自定义标记颜色类 */
+ .mytrack-marker-sunset { background: linear-gradient(135deg, rgb(255, 179, 71), rgb(255, 111, 97)) !important; }
+ .mytrack-marker-ocean { background: linear-gradient(135deg, rgb(6, 190, 182), rgb(72, 177, 191)) !important; }
+ .mytrack-marker-forest { background: linear-gradient(135deg, rgb(94, 231, 223), rgb(57, 163, 124)) !important; }
+ .mytrack-marker-amber { background: linear-gradient(135deg, rgb(246, 211, 101), rgb(253, 160, 133)) !important; }
+ .mytrack-marker-violet { background: linear-gradient(135deg, rgb(161, 140, 209), rgb(251, 194, 235)) !important; }
+ .mytrack-marker-citrus { background: linear-gradient(135deg, rgb(253, 251, 143), rgb(161, 255, 206)) !important; }
+
+ /* 隐藏高德地图版权信息 */
+ .mytrack-card-map .amap-logo,
+ .mytrack-card-map .amap-copyright {
+ display: none !important;
+ visibility: hidden !important;
+ opacity: 0 !important;
+ }
+
+ /* 确保所有地图容器中的版权信息都被隐藏 */
+ .mytrack-card-map div[style*="amap"] .amap-logo,
+ .mytrack-card-map div[style*="amap"] .amap-copyright,
+ .mytrack-card-map .amap-container .amap-logo,
+ .mytrack-card-map .amap-container .amap-copyright {
+ display: none !important;
+ visibility: hidden !important;
+ opacity: 0 !important;
+ }
+
+ .mytrack-error {
+ padding: 20px;
+ background: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+ border-radius: 4px;
+ text-align: center;
+ }
+
+ /* 深色模式适配 */
+ .dark .mytrack-card {
+ background: #1a1a1a;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ }
+
+ .dark .mytrack-card-title {
+ color: #fff !important; /* 修复深色模式标题颜色 */
+ }
+
+ .dark .mytrack-card-info strong {
+ color: #b5b5b5;
+ }
+
+ .dark .mytrack-card-address,
+ .dark .mytrack-card-type,
+ .dark .mytrack-card-review,
+ .dark .mytrack-card-highlights {
+ color: #ccc;
+ }
+
+ .dark .mytrack-categories-badge {
+ background: #1a1a1a;
+ color: #d1d5db;
+ }
+
+ .dark .mytrack-highlight-badge {
+ color: #d1d5db;
+ background:#dc2626;
+ }
+
+ .dark .mytrack-related-articles-section {
+ border-top-color: rgba(255, 255, 255, 0.1);
+ }
+
+ /* 深色模式下标记点边框调整为深色 */
+ .dark .mytrack-marker-custom {
+ border: 2px solid #fff !important;
+ }
+
+ /* 响应式设计 */
+ @media (max-width: 768px) {
+ .mytrack-card-content {
+ flex-direction: column;
+ }
+
+ .mytrack-card-map {
+ width: 100% !important;
+ height: 300px !important;
+ border-top: 1px solid #e8e8e8;
+ }
+
+ .dark .mytrack-card-map {
+ border-top-color: rgba(255, 255, 255, 0.1);
+ }
+ }
+ ';
+ }
+
+ /**
+ * 获取地图卡片JS脚本 - 修复标点颜色问题
+ *
+ * @access private
+ * @param array $mapConfigs 地图配置数组
+ * @return string JS脚本
+ */
+ private static function getMapCardScripts($mapConfigs)
+ {
+ if (empty($mapConfigs)) {
+ return '';
+ }
+
+ $configsJson = json_encode($mapConfigs);
+ $apiKey = !empty($mapConfigs[0]['apiKey']) ? $mapConfigs[0]['apiKey'] : '';
+
+ // 如果API密钥为空,直接返回错误提示
+ if (empty($apiKey)) {
+ return '
+ ';
+ }
+
+ return '
+ ';
+ }
+
+ /**
+ * 原来的renderMapCard方法(保持兼容性)- 修复主题模板调用时的标记颜色问题
+ *
+ * @access public
+ * @param integer $mapId 足迹ID
+ * @return string 地图卡片HTML
+ */
+ public static function renderMapCard($mapId)
+ {
+ static $index = 0;
+ $currentIndex = $index++;
+
+ // 直接调用renderSingleMapCard来确保配置正确
+ list($mapHtml, $config) = self::renderSingleMapCard($mapId, $currentIndex);
+
+ if (!$mapHtml) {
+ return '足迹ID ' . $mapId . ' 不存在
';
+ }
+
+ // 确保配置正确添加到mapConfigs数组中
+ if ($config) {
+ self::$mapConfigs[] = $config;
+ }
+
+ return $mapHtml;
+ }
+
+ /**
+ * 同步关联文章
+ *
+ * @access private
+ * @param int $footprintId 足迹ID
+ * @param int $articleCid 文章CID
+ * @return bool 是否成功
+ */
+ private static function syncRelatedArticles($footprintId, $articleCid)
+ {
+ try {
+ $db = self::getDbConnection();
+
+ // 获取当前的关联文章
+ $stmt = $db->prepare("SELECT related_articles FROM plugin_track_footprint WHERE id = ?");
+ $stmt->execute(array($footprintId));
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($result) {
+ $currentRelatedArticles = $result['related_articles'];
+ $relatedArticlesArray = array();
+
+ // 解析当前的关联文章
+ if (!empty($currentRelatedArticles)) {
+ $relatedArticlesArray = explode(',', $currentRelatedArticles);
+ $relatedArticlesArray = array_map('trim', $relatedArticlesArray);
+ $relatedArticlesArray = array_filter($relatedArticlesArray);
+ }
+
+ // 添加新的文章CID(如果不存在)
+ if (!in_array($articleCid, $relatedArticlesArray)) {
+ $relatedArticlesArray[] = $articleCid;
+ $newRelatedArticles = implode(',', $relatedArticlesArray);
+
+ // 更新数据库
+ $updateStmt = $db->prepare("UPDATE plugin_track_footprint SET related_articles = ? WHERE id = ?");
+ $updateStmt->execute(array($newRelatedArticles, $footprintId));
+
+ return true;
+ }
+ }
+ } catch (Exception $e) {
+ error_log('MyTrack: 同步关联文章失败: ' . $e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * 文章保存时的处理
+ *
+ * @access public
+ * @param array $content 文章数据
+ * @return array
+ */
+ public static function onPostSave($content)
+ {
+ // 获取当前文章CID
+ $cid = isset($content['cid']) ? $content['cid'] : 0;
+
+ if ($cid && isset($content['text'])) {
+ // 解析文章内容中的地图短代码,并验证足迹ID是否存在
+ $pattern = '/\{map-(\d+)\}/i';
+ if (preg_match_all($pattern, $content['text'], $matches)) {
+ foreach ($matches[1] as $mapId) {
+ $footprint = self::getFootprintById($mapId);
+ if ($footprint) {
+ // 更新足迹的关联文章字段
+ self::syncRelatedArticles($mapId, $cid);
+ }
+ }
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * 文章写入时的处理
+ *
+ * @access public
+ * @param array $content 文章数据
+ * @return array
+ */
+ public static function onPostWrite($content)
+ {
+ return self::onPostSave($content);
+ }
+}
\ No newline at end of file
diff --git a/Widget.php b/Widget.php
new file mode 100644
index 0000000..28a9e1c
--- /dev/null
+++ b/Widget.php
@@ -0,0 +1,2330 @@
+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 '请先在插件设置中配置高德地图JS API密钥
';
+ 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;
+
+ // 输出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 <<
+/* 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;
+ }
+}
+
+HTML;
+
+ echo '';
+
+ echo '
+
×
+
‹
+
›
+
![大图预览]()
+
';
+
+ // 输出JavaScript - 添加地图主题切换功能
+ echo <<
+(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 = '';
+ } else if (hasContent) {
+ markerContent = '';
+ } else {
+ markerContent = '';
+ }
+
+ 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(''),
+ size: new AMap.Size(40, 40),
+ offset: new AMap.Pixel(-20, -20),
+ textColor: 'white',
+ textSize: 14
+ },
+ {
+ url: 'data:image/svg+xml;base64,' + btoa(''),
+ 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(''),
+ 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 = '地址:' + footprint.address;
+ metaDiv.appendChild(addressDiv);
+ }
+
+ // 2. 类型信息
+ if (hasLocationType) {
+ var typeDiv = document.createElement("div");
+ typeDiv.className = "mytrack-card-type";
+ typeDiv.innerHTML = '类型:' + footprint.location_type;
+ metaDiv.appendChild(typeDiv);
+ }
+
+ // 3. 分类信息 - 翻译为中文显示
+ if (hasCategories) {
+ var categoriesDiv = document.createElement("div");
+ categoriesDiv.className = "mytrack-card-categories";
+ categoriesDiv.innerHTML = '分类:';
+
+ 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 = '推荐:';
+
+ 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 = '亮点:';
+
+ // 按逗号分隔亮点
+ 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 = '简评:' + footprint.review;
+ metaDiv.appendChild(reviewDivInMeta);
+ }
+
+ // 7. 关联文章
+ if (hasRelatedArticles && relatedArticlesInfo) {
+ var relatedArticlesDiv = document.createElement("div");
+ relatedArticlesDiv.className = "mytrack-related-articles-section";
+ relatedArticlesDiv.innerHTML = '这些文章提到了本地点:';
+
+ 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();
+ }
+ }
+ });
+})();
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/assets/admin.css b/assets/admin.css
new file mode 100644
index 0000000..e754867
--- /dev/null
+++ b/assets/admin.css
@@ -0,0 +1,439 @@
+/* MyTrack 后台管理样式 */
+
+.dark input[type=text], input[type=url], input[type=password], input[type=email], input[type=number], textarea, .typecho-option select, input[type=text]:disabled, input[type=text]:read-only, input[type=password]:disabled, input[type=password]:read-only, input[type=email]:disabled, input[type=email]:read-only, textarea:disabled, textarea:read-only, select
+ {
+ background: #fff!important;
+ border: 1px solid #000000;
+ outline: none;
+}
+
+/* 模态框样式 */
+.typecho-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.typecho-modal-content {
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+ width: 90%;
+ max-width: 600px;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.typecho-modal-title {
+ padding: 15px 20px;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #f9f9f9;
+}
+
+.typecho-modal-title span {
+ margin: 0;
+ font-size: 18px;
+ color: #333;
+ font-weight: bold;
+}
+
+.typecho-modal-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #999;
+ transition: color 0.3s;
+}
+
+.typecho-modal-close:hover {
+ color: #666;
+}
+
+.typecho-modal-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+ max-height: calc(90vh - 120px);
+}
+
+.typecho-modal-foot {
+ padding: 15px 20px;
+ border-top: 1px solid #eee;
+ text-align: right;
+ background: #f9f9f9;
+}
+
+/* 表单样式 */
+.typecho-modal-body ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.typecho-modal-body li {
+ margin-bottom: 5px;
+}
+
+.typecho-label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+ color: #333;
+}
+
+.typecho-modal-body input,
+.typecho-modal-body textarea,
+.typecho-modal-body select {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ transition: border-color 0.3s;
+ box-sizing: border-box;
+}
+
+.typecho-modal-body input:focus,
+.typecho-modal-body textarea:focus,
+.typecho-modal-body select:focus {
+ border-color: #4CAF50;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+}
+
+.typecho-modal-body .description {
+ display: block;
+ margin-top: 5px;
+ color: #666;
+ font-size: 12px;
+}
+
+.typecho-modal-body pre {
+ background: #f5f5f5;
+ padding: 10px;
+ border-radius: 4px;
+ overflow-x: auto;
+ font-size: 12px;
+ color: #333;
+}
+
+/* 加载提示 */
+.loading {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.8);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+}
+
+.spinner {
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #3498db;
+ border-radius: 50%;
+ width: 30px;
+ height: 30px;
+ animation: spin 1s linear infinite;
+ margin-bottom: 10px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* 消息提示 */
+.message {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 15px 20px;
+ border-radius: 4px;
+ color: white;
+ z-index: 3000;
+ max-width: 300px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.message.success {
+ background-color: #4CAF50;
+}
+
+.message.error {
+ background-color: #f44336;
+}
+
+.message.warning {
+ background-color: #ff9800;
+}
+
+.message.info {
+ background-color: #2196F3;
+}
+
+/* 表格样式 */
+.mytrack-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+}
+
+/* 描述文本截断样式 */
+.description-truncate {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-height: 3em; /* 约等于2行文本的高度 */
+ line-height: 1.5em;
+ word-wrap: break-word;
+}
+
+.mytrack-table th,
+.mytrack-table td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+
+.mytrack-table th {
+ background-color: #f9f9f9;
+ font-weight: bold;
+ color: #333;
+ position: sticky;
+ top: 0;
+}
+
+.mytrack-table tr:hover {
+ background-color: #f5f5f5;
+}
+
+.mytrack-table tr:last-child td {
+ border-bottom: none;
+}
+
+/* 分页样式 */
+.mytrack-pager {
+ display: flex;
+ justify-content: center;
+ list-style: none;
+ padding: 0;
+ margin: 20px 0 0;
+}
+
+.mytrack-pager li {
+ margin: 0 5px;
+}
+
+.mytrack-pager a {
+ display: block;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ text-decoration: none;
+ color: #333;
+ transition: all 0.3s;
+}
+
+.mytrack-pager a:hover {
+ background-color: #f5f5f5;
+ border-color: #4CAF50;
+}
+
+.mytrack-pager .current a {
+ background-color: #4CAF50;
+ color: white;
+ border-color: #4CAF50;
+}
+
+/* 图片预览 */
+.mytrack-image-preview {
+ max-width: 50px;
+ max-height: 50px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: transform 0.3s;
+}
+
+.mytrack-image-preview:hover {
+ transform: scale(1.1);
+}
+
+/* 地图容器 */
+.mytrack-map-container {
+ width: 100%;
+ height: 400px;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 20px;
+}
+
+/* 操作按钮组 */
+.mytrack-actions {
+ display: flex;
+ gap: 5px;
+ flex-wrap: wrap;
+}
+
+/* 导入导出区域 */
+.mytrack-import-export {
+ background: #f9f9f9;
+ padding: 15px;
+ border-radius: 4px;
+ margin-bottom: 20px;
+}
+
+.mytrack-import-export h4 {
+ margin-top: 0;
+ margin-bottom: 10px;
+ color: #333;
+}
+
+.mytrack-import-export pre {
+ background: #f5f5f5;
+ padding: 10px;
+ border-radius: 4px;
+ overflow-x: auto;
+ font-size: 12px;
+ color: #333;
+ margin-bottom: 0;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .typecho-modal-content {
+ width: 95%;
+ max-height: 95vh;
+ }
+
+ .typecho-modal-body {
+ max-height: calc(95vh - 120px);
+ }
+
+ .typecho-table th,
+ .typecho-table td {
+ padding: 8px 10px;
+ font-size: 12px;
+ }
+}
+
+/* 表单验证提示 */
+.typecho-modal-body li.has-error input,
+.typecho-modal-body li.has-error textarea,
+.typecho-modal-body li.has-error select {
+ border-color: #f44336;
+}
+
+.typecho-modal-body li .error-message {
+ color: #f44336;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+/* 统计信息 */
+.mytrack-stats {
+ display: flex;
+ gap: 20px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+.mytrack-stat-item {
+ background: #f9f9f9;
+ padding: 15px;
+ border-radius: 4px;
+ flex: 1;
+ min-width: 150px;
+ text-align: center;
+}
+
+.mytrack-stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #4CAF50;
+ margin-bottom: 5px;
+}
+
+.mytrack-stat-label {
+ font-size: 14px;
+ color: #666;
+}
+
+/* 搜索过滤区域 */
+.mytrack-filter {
+ background: #f9f9f9;
+ padding: 15px;
+ border-radius: 4px;
+ margin-bottom: 20px;
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ align-items: flex-end;
+}
+
+.mytrack-filter .mytrack-form-group {
+ margin-bottom: 0;
+ flex: 1;
+ min-width: 200px;
+}
+
+.mytrack-filter .mytrack-btn {
+ margin-bottom: 0;
+ white-space: nowrap;
+}
+
+/* 批量操作区域 */
+.mytrack-batch-actions {
+ background: #f9f9f9;
+ padding: 10px 15px;
+ border-radius: 4px;
+ margin-bottom: 20px;
+ display: none;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.mytrack-batch-actions.show {
+ display: flex;
+}
+
+.mytrack-batch-info {
+ color: #666;
+ font-size: 14px;
+}
+
+.mytrack-batch-buttons {
+ display: flex;
+ gap: 5px;
+}
\ No newline at end of file
diff --git a/cache/bac86e675f18b6df00448f832069f769.cache b/cache/bac86e675f18b6df00448f832069f769.cache
new file mode 100644
index 0000000..511ade5
--- /dev/null
+++ b/cache/bac86e675f18b6df00448f832069f769.cache
@@ -0,0 +1 @@
+a:31:{i:0;a:22:{s:2:"id";s:2:"12";s:8:"latitude";s:9:"26.870249";s:9:"longitude";s:10:"100.231338";s:4:"name";s:9:"万古楼";s:7:"address";s:67:"云南省丽江市古城区大研镇新华街丽江古城-狮子山";s:13:"location_type";s:6:"景区";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:9:"不错的";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"violet";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-16 12:24:26";s:10:"updated_at";s:19:"2025-12-21 11:02:20";s:21:"related_articles_info";a:0:{}}i:1;a:22:{s:2:"id";s:2:"13";s:8:"latitude";s:9:"26.868153";s:9:"longitude";s:10:"100.239876";s:4:"name";s:9:"柏咖啡";s:7:"address";s:39:"云南省丽江市古城区南门广场";s:13:"location_type";s:6:"酒店";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:9:"不错的";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-16 12:47:31";s:10:"updated_at";s:19:"2025-12-20 15:48:04";s:21:"related_articles_info";a:0:{}}i:2;a:22:{s:2:"id";s:2:"15";s:8:"latitude";s:9:"26.853559";s:9:"longitude";s:10:"100.230979";s:4:"name";s:12:"台北帮厨";s:7:"address";s:68:"云南省丽江市古城区庆云路中段祥和商业广场A座4楼";s:13:"location_type";s:6:"咖啡";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:5:"12551";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-18 17:16:38";s:10:"updated_at";s:19:"2025-12-20 15:48:15";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:12551;s:5:"title";s:63:"探店(第08期):半月合集 遛娃探店的闲暇时光";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_08.html";}}}i:3;a:22:{s:2:"id";s:2:"16";s:8:"latitude";s:9:"26.869224";s:9:"longitude";s:10:"100.235416";s:4:"name";s:12:"旺财咖啡";s:7:"address";s:55:"云南省丽江市古城区光义街官院巷56号59号";s:13:"location_type";s:6:"咖啡";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:15:"相当不错的";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:11:"12998,12551";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-19 14:09:16";s:10:"updated_at";s:19:"2025-12-21 10:05:30";s:21:"related_articles_info";a:2:{i:0;a:3:{s:3:"cid";i:12998;s:5:"title";s:63:"探店(第09期):三烧牛肉 丽江古城 韬奋书店 ";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_09.html";}i:1;a:3:{s:3:"cid";i:12551;s:5:"title";s:63:"探店(第08期):半月合集 遛娃探店的闲暇时光";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_08.html";}}}i:4;a:22:{s:2:"id";s:2:"17";s:8:"latitude";s:9:"26.872895";s:9:"longitude";s:10:"100.234502";s:4:"name";s:12:"韬奋书店";s:7:"address";s:50:"云南省丽江市古城区新义街密士巷74号";s:13:"location_type";s:6:"书店";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:6:"不错";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"12998";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-19 14:11:22";s:10:"updated_at";s:19:"2025-12-21 01:43:48";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:12998;s:5:"title";s:63:"探店(第09期):三烧牛肉 丽江古城 韬奋书店 ";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_09.html";}}}i:5;a:22:{s:2:"id";s:2:"27";s:8:"latitude";s:8:"26.87204";s:9:"longitude";s:10:"100.236599";s:4:"name";s:15:"屋顶咖啡馆";s:7:"address";s:55:"云南省丽江市古城区五一街兴仁中段48-3号";s:13:"location_type";s:6:"咖啡";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:6:"不错";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"12892";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-20 11:42:55";s:10:"updated_at";s:19:"2025-12-21 11:38:25";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:12892;s:5:"title";s:67:"郊游(第42期):屋顶咖啡 过丽江古城 夜爬狮子山";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_42.html";}}}i:6;a:22:{s:2:"id";s:2:"28";s:8:"latitude";s:8:"26.85335";s:9:"longitude";s:10:"100.230223";s:4:"name";s:9:"沃尔玛";s:7:"address";s:67:"云南省丽江市古城区玉雪大道祥和商业广场A栋2-3层";s:13:"location_type";s:6:"商场";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-20 15:46:44";s:10:"updated_at";s:19:"2025-12-20 15:46:58";s:21:"related_articles_info";a:0:{}}i:7;a:22:{s:2:"id";s:2:"29";s:8:"latitude";s:8:"26.82983";s:9:"longitude";s:10:"100.222788";s:4:"name";s:15:"丽客隆超市";s:7:"address";s:63:"云南省丽江市玉龙纳西族自治县丽客隆商业广场";s:13:"location_type";s:6:"商场";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-20 15:47:28";s:10:"updated_at";s:19:"2025-12-20 15:47:28";s:21:"related_articles_info";a:0:{}}i:8;a:22:{s:2:"id";s:2:"30";s:8:"latitude";s:9:"26.870507";s:9:"longitude";s:10:"100.235517";s:4:"name";s:12:"丽江古城";s:7:"address";s:63:"云南省丽江市古城区民主路与福慧路交叉口东南";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"12892";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-20 15:47:52";s:10:"updated_at";s:19:"2025-12-21 11:39:41";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:12892;s:5:"title";s:67:"郊游(第42期):屋顶咖啡 过丽江古城 夜爬狮子山";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_42.html";}}}i:9;a:22:{s:2:"id";s:2:"31";s:8:"latitude";s:9:"26.866718";s:9:"longitude";s:10:"100.235617";s:4:"name";s:12:"忠义市场";s:7:"address";s:41:"云南省丽江市古城区长水路85号";s:13:"location_type";s:6:"市场";s:12:"rating_level";i:3;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"13155";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-21 11:29:14";s:10:"updated_at";s:19:"2025-12-23 15:07:31";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13155;s:5:"title";s:71:"郊游(第43期):票务、探店、遛娃三不误的一天日常";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_43.html";}}}i:10;a:22:{s:2:"id";s:2:"32";s:8:"latitude";s:9:"26.869453";s:9:"longitude";s:10:"100.233536";s:4:"name";s:6:"木府";s:7:"address";s:50:"云南省丽江市古城区光义街官院巷49号";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"13155";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-21 11:30:45";s:10:"updated_at";s:19:"2025-12-23 15:07:31";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13155;s:5:"title";s:71:"郊游(第43期):票务、探店、遛娃三不误的一天日常";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_43.html";}}}i:11;a:22:{s:2:"id";s:2:"33";s:8:"latitude";s:9:"26.871005";s:9:"longitude";s:10:"100.232381";s:4:"name";s:9:"狮子山";s:7:"address";s:63:"云南省丽江市古城区大研镇新华街狮子山景区内";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:11:"12892,13155";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-21 11:31:26";s:10:"updated_at";s:19:"2025-12-23 15:07:31";s:21:"related_articles_info";a:2:{i:0;a:3:{s:3:"cid";i:12892;s:5:"title";s:67:"郊游(第42期):屋顶咖啡 过丽江古城 夜爬狮子山";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_42.html";}i:1;a:3:{s:3:"cid";i:13155;s:5:"title";s:71:"郊游(第43期):票务、探店、遛娃三不误的一天日常";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_43.html";}}}i:12;a:22:{s:2:"id";s:2:"34";s:8:"latitude";s:9:"26.927311";s:9:"longitude";s:10:"100.208571";s:4:"name";s:18:"深绿民宿酒店";s:7:"address";s:71:"云南省丽江市古城区束河街道龙泉居委会文明一社41号";s:13:"location_type";s:6:"酒店";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-22 12:35:24";s:10:"updated_at";s:19:"2025-12-22 12:35:43";s:21:"related_articles_info";a:0:{}}i:13;a:22:{s:2:"id";s:2:"35";s:8:"latitude";s:9:"26.923446";s:9:"longitude";s:10:"100.204974";s:4:"name";s:12:"佖屋酒店";s:7:"address";s:53:"云南省丽江市古城区束河古镇中和村16号";s:13:"location_type";s:6:"酒店";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-22 12:36:11";s:10:"updated_at";s:19:"2026-01-30 16:25:38";s:21:"related_articles_info";a:0:{}}i:14;a:22:{s:2:"id";s:2:"36";s:8:"latitude";s:9:"27.124436";s:9:"longitude";s:10:"100.245886";s:4:"name";s:9:"蓝月谷";s:7:"address";s:57:"云南省丽江市玉龙纳西族自治县雪山景区内";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"forest";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-22 12:37:06";s:10:"updated_at";s:19:"2025-12-22 12:37:06";s:21:"related_articles_info";a:0:{}}i:15;a:22:{s:2:"id";s:2:"37";s:8:"latitude";s:9:"27.129758";s:9:"longitude";s:10:"100.235197";s:4:"name";s:9:"云杉坪";s:7:"address";s:92:"云南省丽江市玉龙纳西族自治县15公里处玉龙雪山国家级风景名胜区内";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:4:"plan";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"forest";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-22 12:37:29";s:10:"updated_at";s:19:"2025-12-22 15:48:10";s:21:"related_articles_info";a:0:{}}i:16;a:22:{s:2:"id";s:2:"38";s:8:"latitude";s:9:"27.135685";s:9:"longitude";s:10:"100.187951";s:4:"name";s:12:"冰川公园";s:7:"address";s:78:"云南省丽江市玉龙纳西族自治县玉龙雪山国家级风景名胜区";s:13:"location_type";s:6:"景点";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"forest";s:16:"related_articles";s:0:"";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-22 12:39:17";s:10:"updated_at";s:19:"2025-12-22 12:39:17";s:21:"related_articles_info";a:0:{}}i:17;a:22:{s:2:"id";s:2:"41";s:8:"latitude";s:9:"26.864491";s:9:"longitude";s:10:"100.238076";s:4:"name";s:12:"昭庆市场";s:7:"address";s:42:"云南省丽江市古城区祥和路295号";s:13:"location_type";s:6:"市场";s:12:"rating_level";i:3;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"13155";s:10:"highlights";s:19:"好玩,花鸟市场";s:10:"created_at";s:19:"2025-12-23 14:28:23";s:10:"updated_at";s:19:"2025-12-26 07:19:05";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13155;s:5:"title";s:71:"郊游(第43期):票务、探店、遛娃三不误的一天日常";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_43.html";}}}i:18;a:22:{s:2:"id";s:2:"42";s:8:"latitude";s:9:"26.894586";s:9:"longitude";s:10:"100.208667";s:4:"name";s:15:"中济海公园";s:7:"address";s:54:"云南省丽江市古城区束河街道雪山路180号";s:13:"location_type";s:6:"公园";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"13340";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-25 15:42:31";s:10:"updated_at";s:19:"2026-01-03 15:06:31";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13340;s:5:"title";s:74:"郊游(第44期):滇超第三轮丽江主场对阵普洱现场看球";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_44.html";}}}i:19;a:22:{s:2:"id";s:2:"43";s:8:"latitude";s:9:"26.902327";s:9:"longitude";s:9:"100.21662";s:4:"name";s:18:"丽江市体育场";s:7:"address";s:69:"云南省丽江市古城区清溪路与锦和路交叉口东南120米";s:13:"location_type";s:6:"基建";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"forest";s:16:"related_articles";s:5:"13340";s:10:"highlights";s:0:"";s:10:"created_at";s:19:"2025-12-25 15:43:00";s:10:"updated_at";s:19:"2026-01-03 14:41:27";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13340;s:5:"title";s:74:"郊游(第44期):滇超第三轮丽江主场对阵普洱现场看球";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_44.html";}}}i:20;a:22:{s:2:"id";s:2:"44";s:8:"latitude";s:9:"26.852761";s:9:"longitude";s:10:"100.230391";s:4:"name";s:12:"克罗米星";s:7:"address";s:62:"云南省丽江市古城区庆云路祥和商业广场B栋2楼";s:13:"location_type";s:6:"电玩";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:0:"";s:10:"highlights";s:18:"大人小孩皆宜";s:10:"created_at";s:19:"2025-12-25 15:43:40";s:10:"updated_at";s:19:"2025-12-26 07:30:25";s:21:"related_articles_info";a:0:{}}i:21;a:22:{s:2:"id";s:2:"50";s:8:"latitude";s:9:"26.888448";s:9:"longitude";s:10:"100.239852";s:4:"name";s:9:"黑龙潭";s:7:"address";s:40:"云南省丽江市古城区民主路1号";s:13:"location_type";s:6:"公园";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:5:"13247";s:10:"highlights";s:12:"有山有水";s:10:"created_at";s:19:"2025-12-29 05:58:43";s:10:"updated_at";s:19:"2025-12-29 06:01:26";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13247;s:5:"title";s:57:"自然(第39期):黑龙潭公园 象山“扎营”";s:4:"link";s:44:"https://www.shitoucuo.com/ziranxueji_39.html";}}}i:22;a:22:{s:2:"id";s:2:"51";s:8:"latitude";s:9:"26.888526";s:9:"longitude";s:10:"100.238244";s:4:"name";s:6:"象山";s:7:"address";s:58:"云南省丽江市古城区民主路1号黑龙潭公园内";s:13:"location_type";s:3:"山";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"13247";s:10:"highlights";s:50:"有台阶,登高远望,看丽江、黑龙潭全景";s:10:"created_at";s:19:"2025-12-29 06:00:07";s:10:"updated_at";s:19:"2025-12-29 06:01:26";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13247;s:5:"title";s:57:"自然(第39期):黑龙潭公园 象山“扎营”";s:4:"link";s:44:"https://www.shitoucuo.com/ziranxueji_39.html";}}}i:23;a:22:{s:2:"id";s:2:"52";s:8:"latitude";s:9:"26.819469";s:9:"longitude";s:10:"100.209164";s:4:"name";s:12:"文笔水库";s:7:"address";s:66:"云南省丽江市玉龙纳西族自治县玉龙纳西族自治县";s:13:"location_type";s:6:"水库";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"forest";s:16:"related_articles";s:5:"13438";s:10:"highlights";s:32:"玩水,玩沙,远看玉龙雪山";s:10:"created_at";s:19:"2026-01-05 05:22:51";s:10:"updated_at";s:19:"2026-01-05 05:24:55";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13438;s:5:"title";s:76:"自然(第40期):环文笔海 远望玉龙雪山 村里咖啡图书屋";s:4:"link";s:44:"https://www.shitoucuo.com/ziranxueji_40.html";}}}i:24;a:22:{s:2:"id";s:2:"53";s:8:"latitude";s:9:"26.814603";s:9:"longitude";s:9:"100.20159";s:4:"name";s:6:"楸庭";s:7:"address";s:89:"云南省丽江市玉龙纳西族自治县黄山街道文华居委会文笔村一组35号";s:13:"location_type";s:6:"民宿";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"13438";s:10:"highlights";s:32:"图书室,咖啡厅,远看雪山";s:10:"created_at";s:19:"2026-01-05 05:24:03";s:10:"updated_at";s:19:"2026-01-05 05:24:55";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13438;s:5:"title";s:76:"自然(第40期):环文笔海 远望玉龙雪山 村里咖啡图书屋";s:4:"link";s:44:"https://www.shitoucuo.com/ziranxueji_40.html";}}}i:25;a:22:{s:2:"id";s:2:"54";s:8:"latitude";s:8:"27.01756";s:9:"longitude";s:10:"100.221065";s:4:"name";s:9:"石头厝";s:7:"address";s:75:"云南省丽江市玉龙纳西族自治县白沙镇玉湖村上村2组45号";s:13:"location_type";s:6:"酒店";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:5:"13582";s:10:"highlights";s:13:"野奢,亲子";s:10:"created_at";s:19:"2026-01-15 13:53:18";s:10:"updated_at";s:19:"2026-01-15 13:57:03";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13582;s:5:"title";s:95:"探店(第10期):玉龙雪山第一村 菌子火锅 徒步原始村落 远看玉龙雪山";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_10.html";}}}i:26;a:22:{s:2:"id";s:2:"55";s:8:"latitude";s:9:"26.999593";s:9:"longitude";s:10:"100.215046";s:4:"name";s:6:"玉湖";s:7:"address";s:64:"云南省丽江市玉龙纳西族自治县玉湖段玉湖村1号";s:13:"location_type";s:6:"景区";s:12:"rating_level";i:5;s:10:"categories";a:0:{}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"13582";s:10:"highlights";s:36:"石头房,玉湖,玉龙雪山,徒步";s:10:"created_at";s:19:"2026-01-15 13:54:05";s:10:"updated_at";s:19:"2026-01-15 13:57:03";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13582;s:5:"title";s:95:"探店(第10期):玉龙雪山第一村 菌子火锅 徒步原始村落 远看玉龙雪山";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_10.html";}}}i:27;a:22:{s:2:"id";s:2:"56";s:8:"latitude";s:8:"26.88763";s:9:"longitude";s:10:"100.223907";s:4:"name";s:12:"象山市场";s:7:"address";s:42:"云南省丽江市古城区象山路274号";s:13:"location_type";s:9:"菜市场";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"13582";s:10:"highlights";s:21:"本地人的菜市场";s:10:"created_at";s:19:"2026-01-15 13:55:00";s:10:"updated_at";s:19:"2026-01-15 13:57:03";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13582;s:5:"title";s:95:"探店(第10期):玉龙雪山第一村 菌子火锅 徒步原始村落 远看玉龙雪山";s:4:"link";s:41:"https://www.shitoucuo.com/tandian_10.html";}}}i:28;a:22:{s:2:"id";s:2:"63";s:8:"latitude";s:9:"26.865753";s:9:"longitude";s:9:"100.24226";s:4:"name";s:18:"多弗度假酒店";s:7:"address";s:51:"云南省丽江市古城区七一街古佑巷198号";s:13:"location_type";s:0:"";s:12:"rating_level";i:4;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:6:"sunset";s:16:"related_articles";s:5:"13656";s:10:"highlights";s:12:"纳西庭院";s:10:"created_at";s:19:"2026-01-21 14:40:55";s:10:"updated_at";s:19:"2026-01-21 14:44:46";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13656;s:5:"title";s:82:"郊游(第45期):突然早起 酒店早餐 疯玩几十公里外亲子乐园";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_45.html";}}}i:29;a:22:{s:2:"id";s:2:"64";s:8:"latitude";s:9:"26.778886";s:9:"longitude";s:10:"100.291806";s:4:"name";s:21:"圣贝诗儿童乐园";s:7:"address";s:57:"华丽高速出口与S49丽上高速交叉口东南440米";s:13:"location_type";s:12:"儿童乐园";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"ocean";s:16:"related_articles";s:5:"13656";s:10:"highlights";s:29:"场地大,设备多,配套全";s:10:"created_at";s:19:"2026-01-21 14:41:43";s:10:"updated_at";s:19:"2026-02-04 09:25:23";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13656;s:5:"title";s:82:"郊游(第45期):突然早起 酒店早餐 疯玩几十公里外亲子乐园";s:4:"link";s:41:"https://www.shitoucuo.com/jiaoyou_45.html";}}}i:30;a:22:{s:2:"id";s:2:"65";s:8:"latitude";s:9:"26.918933";s:9:"longitude";s:10:"100.210658";s:4:"name";s:20:"木屿·废墟餐厅";s:7:"address";s:78:"云南省丽江市古城区丽江乙立建筑有限公司东南门东北140米";s:13:"location_type";s:6:"餐厅";s:12:"rating_level";i:5;s:10:"categories";a:1:{i:0;s:7:"visited";}s:6:"review";s:0:"";s:11:"description";N;s:11:"article_cid";N;s:8:"urlLabel";N;s:3:"url";N;s:6:"photos";N;s:4:"tags";N;s:4:"date";N;s:11:"markerColor";s:5:"amber";s:16:"related_articles";s:5:"13753";s:10:"highlights";s:22:"云南菜,网红餐厅";s:10:"created_at";s:19:"2026-02-08 05:02:51";s:10:"updated_at";s:19:"2026-02-08 05:03:37";s:21:"related_articles_info";a:1:{i:0;a:3:{s:3:"cid";i:13753;s:5:"title";s:40:"滇超第五轮现场 束河废墟餐厅";s:4:"link";s:36:"https://www.shitoucuo.com/13753.html";}}}}
\ No newline at end of file
diff --git a/db/track_e670fbc4f2.db b/db/track_e670fbc4f2.db
new file mode 100644
index 0000000000000000000000000000000000000000..067c9888b3ba638def4aad987183ec7f7c21cba5
GIT binary patch
literal 73728
zcmeI4e^69cmd780A|jHQ7{Zu|&m>M_Y{vJae?E0>Ofb5G=qOZ%`LQfQi(Ns$rZqb?
zv(aY+Vvt4@ZNX*<3TliX_=~0yshUacxK%T=$?nXiCQ~N6mA}Imb)Xwg?
z@4arIX?mPL@`v$s@#yZ`@7;IK=brPu=iX}A_RT`4+rjRrb?qv5vm$dJlaj*NS(ag#
zH{nk13l#OJV2&0`ZhoR#dxqT+(*3bwLa
zwr<>%zqOS8MnNf?UsAkzV-a+_si3IXknqata<|jHr_#Z~y$ji*&Bbg{Nnzm(=hoI#
z-SNpa<+~hgalyBs!=#;7I_fK2&N{cVwnjZY@vX~UZf8ZcqpV`W)5It4aa9+VZ+BED
z=?hLt@@d`9T6b-Ik`vtJRmncCEQdn38NGU2asH+)iM!ZSNAGioT}&Hz16y8M>2lQTez4WG6%Yos5U1a-(+17J
zx}c?|s;t_v7hpz%D{3WYVBrc!RjteEh<4&~>~%U`)WEU3%3UuwT+h^2*Sho;mjgnL
zTC9y^^s1fCs-4wvxV47mTV~D4S+OFeRL!3H-PN${DXVwv-s7mLaKyb$osutcA3jCn
z&j430XPA`EdqaEfHZKo6nVP=XVoQ0s%I$JiRXJQ*R8Qz+ncKN*1_@=NgTHm-b5!;c
zUOFxC^Vb){xg}d@+OeC9W)RtFm#r&!Ze!83R;t5#S2Kw9wt`~aFf~)xOwVri`3(hI
z3s_LKHEdDA^IvyXu9|kAf}&?<{m@(MEfb01`j~NB{{S
z0VIF~kN^@u0!RP}Ab}|a=yIP?f6aNCzRJ#m-znKa27d5?1dsp{Kmter2_OL^fCP{L
z5q^B>_?;(ovqTH0lOiqqndiTIP*NwlnmoB*V-6!6hOQN7fdEzemH9pn7t%s_IJSQCAo}UdT;Ie9UuOky)^sV?CrnLRk~ZXE~57F
zW|kKuqeT+=Xj9B&&X~;P7H`CRUb8OJaFhhRQLBRfmPmU?sPka`o|<}x8~(9X(HA*l
z*f$bD0{035*)*T(^MugJ55$!2)*07d*MUC&e2wy;FX(Rqc?HRg%9^Hhf>9F8(I$as
zd0yfKN#K$xUGKTjYweNaP0EQu(&7pCcy$3+)jKcLJV5%7kf9-^;}S^uG!@+ItgUvq
zv*y0Y+?V#>%!0P;lC1Z$xcU1tf1WAKJC<=f(*HSq!JKc-?tv5T(MKkH>mft^
z_XzN1h=R!2&>a%R@6%E`HwUUxl6s)Sv5CAX1Q
zz1KX-wPDh5k(}y}$+JnVodvBnHRo}B9w*W!4k~OuiDD~V-OA-FAk-20Sme|N&}JyD
zc`HX+^*f~L1+-S7@}g+uvcEjG{`3HOH{Pc>RBXddPAQ|q8BP-CVU$jZQC1&qL
z({!>S&!Nc8j!@uAxNltXwuYN7D3?cxXIyL75R+45WZ-%za4vGjL#M(cE#a4aFd@S0
z(1DTYvZ;3YbSIWRF?G$gFFF8Aew}f4_kV&9
zH%^{@?3dwVU5f9O@Zj)dgMq#U;y6LmozvI2Yh_2wz^S0KH>+PaWMua7snPPH8r=+}
z;)gpvN$FkzHERpHIOEI`PuGZQ9*VH6HA{jCR=U&ejV)`nJcPGU
zJVB1bnpWv*4)Wq2qwI<5@1
zk%94qhMFY77-u`ghxjKA)dk~bhjQf*8EPQs+JiSbl@m?MNl(zfKh%5E$j@N9xHK-D
zGu2Rd9G2goubh1@1B%l3i#B)A3&W-`v_60HZ&)f9`ug@tr3DU!299WHiN^?#$wbL&1?l
zz?cl2jC7nP!^1`!;MCJ9PLM(K%r1(-`YjSUxR+*
z^jO6zfWA!x!W|hIISzq!74+CTU5=xZ1+{JEqf^sKRtKE!6214U=1+fD1;#DBMwX9G
zUMa+IXkR-kW?|H5A%1HM
zq&R^F%%r=q?kDeV!E2aIqeg3LV0a`hrRX}3&JfbOdzP)AcNXj&{cP>Wf2CZ0nGCjv
zJg4F;HA}{cvDb_@OQr;t#?*?M48=n4Ij9;E?7%6nH4Awn89Ff4S}iK%m5XVxPT>6k
zSer}Z;r+izt^3(6h=(WYmcE@ywYBHEwgneC+c?=~UNQ=jX(Fk#kbsgRS^W?Uw1YI!
zq-wYt9EF`lx_xN0f}K!IOxY{ccI74o#H#zIrWdt%MGlp$hP^)}pMu5rFDO{ERPhGF
zS5L=n8ro>N!$EpYOkiLx2epnQCYXd{h{-e!Fwxn-M1^3gxuH{jGIlJsm8kD@+H6*X
zy59+!HHTs3r|zHX168oxTuOa!<|psG0zS-ou;08g(tVNiH3vt|!Y*YLIj{Nv%YR{V
z7IH8OB5cu&~@xmBRTU1=$OR_=4-u2V^}j>R}2Bn)b;X&3ro^Hw)iU`urr&FK~i6kK^+=6U&(-!6
z@*IlX><9&}g!{%7Z)>>ef^vD3c*Y~62g%S-xW^ljk0~dbNQ;LIHIQ@d!5f|MHgvIF
z>1qr5_lHIgiYC%JtPHl1f$_Ax&U&ZYQCYrgyTiqcM$t-6-3*SLB?G+%a`Xf_-a|YM
zhVZyAfNeaA#kQ^7<>ZfdZ}>&?{P
zB@_NWt_|yx_El*w{bOW!++Mn1*>B(4dMwm^h4fw}p0228you#S$s|g=HLl45gMH^C
z8|oMXqa(+gz+h$MggTC|!fye_(n!ZL(r}anyiu=fD;(uDylAotktPr6J8cLCE|dNv
zaxNLrXylEP5P6F4lmVhoi~kSy%aeLKuXJ3JmkJL}8K3WI!?O&$+Fi^DCE!)*4e@_U
zV%JUf!X%ljl949fWMBUr$25PUp6Kj#PlkrT9|${njUg^)GSr-5c)Rp&QS(cif)GPD
zM}kLhkcQ^am^U}VkP=Z9sNYjl?{JgebHPA>47QT~*F&9+WWXn{SwZdR
zCvqT1%X!8wDSuS{P`Da${`Oav{>B_rso>bL@PW&4rK9
ze}fXZ0A2+Bo=E#yaD=`N^paKxl`}!V5A+el1elTT02vKLha#H%qLIs7J8SpUtj2dp
z;0q)me=naVj7h%*)B?*jRkGQoe_zYjSQbIT{NRP5<+Bvuc``KqqhS*0Ca1e2y+>fa
z)$&8gF{hAQEdPc2nU^2Pg^S_InpTJ}ya}RCEhpWg9FA1C_a+5au#yQ17ITP0eQkQJH
z4EkS&@(k)X(C^?_8yOzvZC02$r;0N#f)85p2Q_4*<21vh+P}a^#;%dTmzsbqSZT^n
ztY^xXs8VKu`f-(i^~Y~PX;%EwoD+{|RWU5Uw4z3>VDt(fL^@W~=uL?${8(wGbq0lu
zU@=0DgT87VN0VI_V5s30-$l?XD9A#c2V1Bf3{XdstXWpNu<0o4ht`O
zB7K5ofVSi^kef%9{=P4DbfI%d00|%gB!C2v01`j~NB{{S0VIF~kN^@u0!RP}AOR$R
w1dsp{Kmter2_OL^fCP{L5