2026-10-17 00:15:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 旅程地图(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, '&').replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 视野上限:别放到街道级(见文件头的说明) */
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-19 17:27:37 +08:00
|
|
|
|
/* 🔴 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>'
|
|
|
|
|
|
: '';
|
2026-10-17 00:15:06 +08:00
|
|
|
|
var html = '<div class="trip-pin" style="left:' + (C + dx) + 'px;top:' + (C + dy) + 'px;">'
|
|
|
|
|
|
+ '<div class="trip-pin-card">'
|
2026-10-19 17:27:37 +08:00
|
|
|
|
+ thumb
|
|
|
|
|
|
+ '<span class="trip-pin-body">'
|
2026-10-17 00:15:06 +08:00
|
|
|
|
+ '<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>' : '')
|
2026-10-19 17:27:37 +08:00
|
|
|
|
+ '</span></div></div>';
|
2026-10-17 00:15:06 +08:00
|
|
|
|
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 : '未知错误'));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|