Files
FamilyBook/assets/js/trip_map.js
T

128 lines
5.8 KiB
JavaScript
Raw Normal View History

/**
* 旅程地图(v54 用户 2026-10-16 第 3 条)。
*
* 用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,可以在地图上显示旅程数据。
* 1. 先读一下它之前那个代码是怎么实现的,在我们的旅程模块也实现一下。」
*
* ── 移植自 TripLog `templates/activities/index.php` 的那套(要点)──
* · 高德 JS API 2.0;`AMap.Map` 建图 + `AMap.Marker` 打点;
* · 每枚标记的 `position` 必须是 **[lng, lat] 数组**(传 `{lng,lat}` 裸对象
* 会让 AMap 抛错、整个绘制循环当场中断 —— TripLog 踩过这个,注释里写着
* "批 11 的光秃秃地图就是它");
* · `setFitView()` 自动框住所有点,但**上限 11 级**:只有一趟旅程时
* setFitView 会一路放大到街道级,那是导航视角、不是"足迹"视角;
* · 卡片(自绘 content)用**方形画布 + 居中定位**,这样卡片可以偏离真实坐标
* 而不影响标记锚点(TripLog 用它做"同城扇开")。
*
* ── 本版**不**做"同城扇开"(有意取舍)──
* TripLog 那个扇开是为了"同一座城市的多次旅行挤在一起"。
* 本仓的旅程列表通常没有那么多趟、且每枚卡片更窄(只有标题+时间),
* 先做"同一城市多点重合就按到达顺序错开一点"这种**轻量去重叠**即可 ——
* 完整扇开(环形均匀分布 + 引线 + 与容器边界求解)是几百行几何代码,
* 在没有真实数据压出这个问题之前不值得(TripLog 也是撞到了才写的)。
*
* ⚠️ 合规:地图是高德(白名单内),SDK 由 layout.php 的 `fb-amap` 按需提供,
* 本文件**不自己拼 SDK 地址、不碰 key**。
* ⚠️ SDK 未就位(没配 key / 加载失败)时降级成一句说明,不留白框。
*/
(function () {
'use strict';
var el = document.getElementById('tripMapCanvas');
if (!el) { return; }
var points;
try {
points = JSON.parse(el.getAttribute('data-points') || '[]');
} catch (e) {
points = [];
}
if (!points || !points.length) { return; }
function fallback(msg) {
el.innerHTML = '<p class="trip-map-fallback">' + msg + '</p>';
}
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* 视野上限:别放到街道级(见文件头的说明) */
var MAX_ZOOM = 11;
/* 卡片画布:内容绝对居中,卡片可以在这块画布里自由偏移(本版只用于微错开) */
var CANVAS = 260;
var C = CANVAS / 2;
function boot() {
var map = new AMap.Map(el, { viewMode: '2D', zoom: 5, resizeEnable: true });
/* 轻量去重叠:同一位置的多个点(坐标差 < 极小阈值)沿一个方向的螺旋微移,
免得完全重合、只能点到一个。位移**只改卡片绘制位置**,
若做不明显,至少保证每枚都能被点到(content 是各自的 DOM)。 */
var seen = {};
var markers = [];
points.forEach(function (p, i) {
var key = p.lng.toFixed(3) + ',' + p.lat.toFixed(3);
var n = seen[key] = (seen[key] || 0);
seen[key]++;
var dx = 0, dy = 0;
if (n > 0) {
// 螺旋微移:每多重一个,就往外挪一点(角度按 n 递增)
var ang = n * (Math.PI / 3);
var rad = 26 + n * 6;
dx = Math.cos(ang) * rad;
dy = Math.sin(ang) * rad;
}
/* 🔴 v63(用户 2026-10-19 第 3 条):「旅程地图模式,应该显示旅程的题图。」
⇒ 卡片左侧一枚 40×40 的题图(`trip_details.cover_key` → 服务端算好的 URL,
见 _map_body.php)。没有题图就**只显示文字**,不摆灰色占位方块
(地图上每枚卡片都带一个灰框,整张图会显得脏)。 */
var thumb = p.cover
? '<span class="trip-pin-thumb"><img src="' + esc(p.cover) + '" alt="" loading="lazy"></span>'
: '';
var html = '<div class="trip-pin" style="left:' + (C + dx) + 'px;top:' + (C + dy) + 'px;">'
+ '<div class="trip-pin-card">'
+ thumb
+ '<span class="trip-pin-body">'
+ '<div class="trip-pin-title">' + esc(p.title) + '</div>'
+ (p.when ? '<div class="trip-pin-sub">' + esc(p.when) + '</div>' : '')
+ (p.city ? '<div class="trip-pin-sub">📍 ' + esc(p.city) + '</div>' : '')
+ '</span></div></div>';
var mk = new AMap.Marker({
// ⚠️ 必须是 [lng, lat] 数组(见文件头的说明)
position: [p.lng, p.lat],
content: '<div class="trip-pin-stage" style="width:' + CANVAS + 'px;height:' + CANVAS + 'px;">'
+ html + '</div>',
anchor: 'center',
map: map,
});
mk.on('click', function () { window.location.href = p.url; });
markers.push(mk);
});
map.setFitView(null, false, [40, 40, 40, 40]);
if (map.getZoom() > MAX_ZOOM) { map.setZoom(MAX_ZOOM); }
el.setAttribute('data-map-ready', '1');
}
/* SDK 由 layout 的按需加载器提供(`window.fbAmapSdk()`)。
⚠️ 没配高德 key 时那个函数不存在 —— 直接降级。 */
if (typeof window.fbAmapSdk !== 'function') {
fallback('地图没启用(还没在配置里填高德 key);用左侧的「列表」看旅程。');
return;
}
window.fbAmapSdk().then(function (ok) {
if (!ok || typeof window.AMap === 'undefined') {
fallback('地图脚本没加载成功,暂时看不了地图;用左侧的「列表」看旅程。');
return;
}
try {
boot();
} catch (e) {
fallback('地图初始化失败:' + (e && e.message ? e.message : '未知错误'));
}
});
})();