最终效果

先来看一下实现后的效果:

功能特点

  • 现代化设计:精致的卡片式 UI,流畅的动画效果
  • 🗺️ 交互式地图:基于高德地图 API,支持缩放、拖拽
  • 📍 标记点展示:点击标记查看地点详细信息
  • 🖼️ 图片灯箱:集成 Fancybox 5.0,优雅的图片预览
  • 🌓 暗色模式:完美支持亮色/暗色模式切换
  • 📱 响应式:适配桌面端和移动端
  • 📊 统计卡片:一体化设计,展示旅行数据

技术选型

地图服务选择

在实现旅行足迹功能时,我对比了几个地图服务:

服务商 优点 缺点 选择
高德地图 国内访问稳定、数据详细、API 文档完善 需要注册 Key ✅ 选择
Leaflet + OpenStreetMap 开源免费、无需 API Key 国内访问较慢、数据简单
Google Maps 功能强大、全球覆盖 需要付费 API、国内需特殊网络

最终选择高德地图,原因如下:

  1. 访问速度快:国内服务器,加载速度快
  2. 数据准确:国内地图数据最为详细
  3. 免费额度充足:个人开发者每日 30 万次调用
  4. API 文档完善:提供详细的开发文档和示例

UI 设计风格

在设计方面,我选择了“精致的旅行日记”风格:

  • 字体系统:使用 Noto Serif SC(思源宋体)作为标题字体,Inter 作为正文字体
  • 视觉层次:细腻的阴影、渐变装饰、流畅动画
  • 卡片设计:现代化的卡片布局,统一的视觉语言
  • 交互反馈:悬停动画、微妙的过渡效果

实现步骤

1. 申请高德地图 API Key

首先需要注册高德开放平台账号并申请 API Key:

  1. 访问高德开放平台注册账号
  2. 完成实名认证
  3. 进入控制台,创建应用
  4. 添加 Key,选择「Web端(JS API)」
  5. 记录下 Key安全密钥(如果有的话)

💡 提示:如果安全密钥显示为 -,说明不需要配置安全密钥,这是正常的。

2. 准备数据文件

创建 source/travel-locations.json 文件,用于存储旅行地点数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"version": "1.0.0",
"updated": "2025-01-31T12:00:00+08:00",
"map_config": {
"center": {
"lng": 116.397428,
"lat": 39.90923
},
"zoom": 5,
"map_style": "normal"
},
"locations": [
{
"id": 1,
"name": "故宫博物院",
"lng": 116.397428,
"lat": 39.90923,
"date": "2023-05-01",
"description": "中国明清两代的皇家宫殿,世界文化遗产。",
"images": ["图片URL"],
"category": "景点",
"tags": ["历史", "文化", "北京"]
}
]
}

数据字段说明

字段 类型 必填 说明
id Number 唯一标识符
name String 地点名称
lng Number 经度(高德地图 GCJ-02 坐标)
lat Number 纬度
date String 到达日期
description String 地点描述
images Array 图片 URL 数组
category String 分类
tags Array 标签数组

获取坐标的方法:使用高德地图坐标拾取器获取准确的经纬度。

3. 创建样式文件

创建 source/custom/css/travel-map.css,实现精美的 UI 设计。

核心样式包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* CSS 变量定义 */
:root {
--travel-font-heading: 'Noto Serif SC', 'Source Han Serif SC', serif;
--travel-font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--travel-accent: var(--theme, #4caf50);
--travel-card-bg: var(--card, #ffffff);
--travel-radius: 16px;
--travel-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 地图容器 */
#travel-map-container {
position: relative;
width: 100%;
height: 650px;
background: var(--travel-card-bg);
border-radius: var(--travel-radius);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.08);
transition: var(--travel-transition);
}

/* 统计卡片 */
.journey-stats-card {
max-width: 900px;
margin: 3rem auto;
background: var(--travel-card-bg);
border-radius: var(--travel-radius);
overflow: hidden;
}

设计亮点

  1. 使用 CSS 变量:完美集成主题的 --theme--card 等变量
  2. 现代化阴影:多层阴影叠加,营造立体感
  3. 流畅动画:使用 cubic-bezier 缓动函数
  4. 渐变装饰:微妙的渐变背景增加层次感

4. 创建交互逻辑

创建 source/custom/js/travel-map.js,实现地图的交互逻辑。

核心功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 加载地图数据
async function loadTravelData() {
const response = await fetch('/travel-locations.json');
const data = await response.json();
initMap(data);
}

// 初始化地图
function initMap(data) {
const map = new AMap.Map('travel-map', {
zoom: data.map_config.zoom,
center: [data.map_config.center.lng, data.map_config.center.lat]
});

addMarkers(map, data.locations);
}

// 添加标记点
function addMarkers(map, locations) {
locations.forEach(location => {
const marker = new AMap.Marker({
position: [location.lng, location.lat],
title: location.name
});

const infoWindow = createInfoWindow(location);
marker.on('click', () => {
infoWindow.open(map, marker.getPosition());
// 重新绑定 Fancybox
setTimeout(() => {
Fancybox.bind('[data-fancybox="travel-map"]');
}, 300);
});

map.add(marker);
});
}

5. 更新配置文件

_config.yml 中添加 CSS 和 JS 引用:

1
2
3
4
5
inject:
head:
- <link rel="stylesheet" href="/custom/css/travel-map.css">
script:
- <script type="text/javascript" src="/custom/js/travel-map.js"></script>

6. 更新探索页面

编辑 source/explore/index.md,添加地图容器和 API Key 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
---
title: 探索
menu_id: explore
# ... 其他配置
inject:
head:
- <script src="https://webapi.amap.com/maps?v=2.0&key=您的Key"></script>
- <link rel="stylesheet" href="https://gcore.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css">
- <script src="https://gcore.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script>
---

# 旅行足迹

<div id="travel-map-container">
<div class="travel-map-loading">
<div class="spinner"></div>
<p>地图加载中...</p>
</div>
</div>

## 旅程统计

<div class="journey-stats-card">
<!-- 统计内容 -->
</div>

遇到的问题和解决方案

问题 1:数据文件 404 错误

现象:页面报错 HTTP error! status: 404

原因:数据文件放在 source/_data/ 目录下,该目录用于存放数据源文件,不会被 Hexo 复制到生成的站点中。

解决方案:将数据文件移动到 source/ 根目录。

1
mv source/_data/travel-locations.json source/travel-locations.json

问题 2:图片点击下载而不是打开灯箱

现象:点击信息窗口中的图片直接下载,没有 Fancybox 灯箱效果。

原因:地图信息窗口是动态生成的,Fancybox 在页面加载时已经初始化完成,没有自动绑定到后来添加的元素上。

解决方案:在信息窗口打开后,手动重新绑定 Fancybox。

1
2
3
4
5
6
7
8
9
10
marker.on('click', function() {
infoWindow.open(map, marker.getPosition());

setTimeout(() => {
if (typeof Fancybox !== 'undefined') {
Fancybox.unbind('[data-fancybox="travel-map"]');
Fancybox.bind('[data-fancybox="travel-map"]');
}
}, 300);
});

问题 3:Fancybox 未加载

现象:控制台显示 Fancybox 未加载

原因:探索页面没有 [data-fancybox] 元素,主题的 Fancybox 加载逻辑跳过了该页面。

解决方案:在探索页面强制加载 Fancybox CSS 和 JS。

1
2
3
4
inject:
head:
- <link rel="stylesheet" href="https://gcore.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css">
- <script src="https://gcore.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script>

问题 4:高德地图控件报错

现象:控制台报错 AMap.ToolBar is not a constructor

原因:高德地图 API 2.0 中,控件需要通过插件方式加载。

解决方案:暂时移除控件,保持地图简洁。

1
2
3
function addMapControls(map) {
// 暂时禁用控件以避免兼容性问题
}

问题 5:YAML 格式错误

现象:部署时报错 bad indentation of a sequence entry

原因:YAML 中多行脚本需要使用正确的语法。

解决方案:使用 | 符号表示多行字符串。

1
2
3
4
5
6
7
8
inject:
head:
- |
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: 'xxx'
};
</script>

设计细节

字体选择

为了营造”旅行日记”的氛围,我选择了:

  • 标题字体:Noto Serif SC(思源宋体)- 优雅的衬线字体
  • 正文字体:Inter - 现代无衬线字体
1
2
3
4
5
6
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@400;500;600&display=swap');

:root {
--travel-font-heading: 'Noto Serif SC', serif;
--travel-font-body: 'Inter', sans-serif;
}

动画效果

为统计卡片添加了浮动动画:

1
2
3
4
5
6
7
8
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}

.journey-stats-icon {
animation: float 3s ease-in-out infinite;
}

暗色模式适配

使用主题变量自动适配:

1
2
3
4
5
6
.dark #travel-map-container,
[data-theme="dark"] #travel-map-container {
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.2),
0 4px 12px rgba(0, 0, 0, 0.3);
}

完整的文件结构

1
2
3
4
5
6
7
8
9
10
11
hexo-stellar/
├── source/
│ ├── custom/
│ │ ├── css/
│ │ │ └── travel-map.css # 地图样式
│ │ └── js/
│ │ └── travel-map.js # 地图交互逻辑
│ ├── explore/
│ │ └── index.md # 探索页面
│ └── travel-locations.json # 旅行地点数据
└── _config.yml # 全局配置(添加 inject)

后续优化方向

目前实现的功能已经比较完善,但还有一些可以优化的方向:

  1. 地点分类筛选:添加标签筛选功能,按类别显示地点
  2. 时间轴模式:按时间顺序展示旅行路线,绘制轨迹
  3. 数据可视化:添加统计图表,展示每年的旅行次数
  4. 自定义标记图标:使用自定义图标美化标记点
  5. 图片懒加载:优化图片加载性能
  6. 导出功能:支持导出 GPX 文件

总结

通过这次实践,我成功为博客添加了一个精美的旅行足迹功能。整个过程中,我学到了很多:

  1. 高德地图 API 的使用方法
  2. Fancybox 5.0 的集成和动态绑定
  3. Hexo 主题的定制和扩展
  4. 响应式设计的最佳实践
  5. 暗色模式的适配技巧

这个功能不仅让博客更加个性化和有趣,也记录了我旅行中的美好回忆。如果你也想为你的博客添加类似功能,希望本文能对你有所帮助!

参考资源


最后:如果你在实现过程中遇到问题,欢迎在评论区交流讨论!