修「光秃秃地图」回归 + 详情页改标签页布局(批 12)

回归修复(用户反馈:地图上什么都不显示):
- 批 11 重构时把 {lng,lat} 对象直接传给了 AMap.Marker.position ——
  AMap 只认 [lng, lat] 数组/LngLat,构造器抛 Invalid Object,
  forEach 当场中断,整张地图一个覆盖物都不剩(光秃秃地图)。
- 现在 position 一律归一化为数组(中点/单点三分支),并新增
  _page11.php + _map12.js 真 AMap 无头浏览器用例锁死(覆盖双坐标/
  单坐标/只有地名三种节点形态 + 无 JS 报错 + 标记数量断言),
  已接入 _chain3.py 全链(步骤 10b)。

标签页布局(用户需求):
- 活动详情页拆成两个标签:「🗺️ 行程地图」(默认)与「📋 计划与实际」;
- 地图页独占整页,高度加大到 clamp(480px, 72vh, 860px)(手机 62vh);
- 节点页 = 原 时间轴(计划/实际两列)+ 评论区,默认隐藏;
- 切回地图标签时重新 setFitView(隐藏期间容器尺寸无意义);
- 活动没有可上图的执行节点时不渲染标签条,退化为原单页布局。

已验证:本地全链 12 步全绿(渲染检查 341 项 + 真 AMap 浏览器验证
11 项);远端 e2e 341 项 0 失败;部署静态断言/双 PHP 渲染/内容断言
全过后提交。
This commit is contained in:
2026-09-24 12:04:31 +08:00
parent 00cd99d07d
commit c4c3b17cf7
2 changed files with 98 additions and 46 deletions
+84 -43
View File
@@ -67,18 +67,87 @@ foreach (($realityNodes ?? []) as $rn) {
}
?>
<?php if ($routeNodes !== []): ?>
<section class="route-map-section">
<h2 class="section-title" id="route-map">🗺️ 行程路线</h2>
<div class="route-map" id="route-map-canvas"
data-route="<?= e(json_encode($routeNodes, JSON_UNESCAPED_UNICODE)) ?>"></div>
<p class="muted route-map-hint">
按时间顺序连出的执行节点路线(节点间虚线衔接),圆标在每段行程的中间,点它可打开对应节点。
<span class="route-legend-end">红点</span> = 到达地。只有地名没坐标的地点会自动向高德查询定位,太偏僻的地名可能查不到。
</p>
</section>
<div class="route-tabs" role="tablist" aria-label="行程地图与节点详情切换">
<button type="button" class="route-tab is-active" data-panel="route-panel-map"
aria-selected="true">🗺️ 行程地图</button>
<button type="button" class="route-tab" data-panel="route-panel-nodes"
aria-selected="false">📋 计划与实际</button>
</div>
<div class="route-panel" id="route-panel-map">
<section class="route-map-section">
<div class="route-map" id="route-map-canvas"
data-route="<?= e(json_encode($routeNodes, JSON_UNESCAPED_UNICODE)) ?>"></div>
<p class="muted route-map-hint">
按时间顺序连出的执行节点路线(节点间虚线衔接),圆标在每段行程的中间,点它可打开对应节点。
<span class="route-legend-end">红点</span> = 到达地。只有地名没坐标的地点会自动向高德查询定位,太偏僻的地名可能查不到。
</p>
</section>
</div>
<div class="route-panel" id="route-panel-nodes" hidden>
<?php endif; ?>
<div class="timeline-grid">
<section class="timeline-col">
<h2>📋 计划 (<?= count($planNodes) ?>)</h2>
<?php if (empty($planNodes)): ?>
<p class="muted">暂无计划节点</p>
<?php else: ?>
<?php foreach ($planNodes as $n): ?>
<?php $cardCategory = 'plan'; require __DIR__ . '/_node_card.php'; ?>
<?php endforeach; ?>
<?php endif; ?>
</section>
<section class="timeline-col">
<h2>🎬 实际 (<?= count($realityNodes) ?>)</h2>
<?php if (empty($realityNodes)): ?>
<p class="muted">暂无执行节点</p>
<?php else: ?>
<?php foreach ($realityNodes as $n): ?>
<?php $cardCategory = 'reality'; require __DIR__ . '/_node_card.php'; ?>
<?php endforeach; ?>
<?php endif; ?>
</section>
</div>
<?php
$commentTargetType = \App\Comments::TARGET_ACTIVITY;
$commentTargetId = (int)$activity['id'];
require dirname(__DIR__) . '/comments/_block.php';
?>
<?php if ($routeNodes !== []): ?>
</div>
<script>
(function () {
'use strict';
/* ── 标签页切换:行程地图 / 计划与实际 ──────────────────────
地图标签默认打开(地图是主视图);切回来时重新 setFitView,
因为容器隐藏期间尺寸没有意义。 */
var tabs = Array.prototype.slice.call(document.querySelectorAll('.route-tab'));
var panels = {
'route-panel-map': document.getElementById('route-panel-map'),
'route-panel-nodes': document.getElementById('route-panel-nodes'),
};
var map = null; // 下面初始化后挂进来,供切换标签时重新适配视野
tabs.forEach(function (btn) {
btn.addEventListener('click', function () {
tabs.forEach(function (b) {
var on = (b === btn);
b.classList.toggle('is-active', on);
b.setAttribute('aria-selected', on ? 'true' : 'false');
});
Object.keys(panels).forEach(function (id) {
if (panels[id]) panels[id].hidden = (id !== btn.getAttribute('data-panel'));
});
if (btn.getAttribute('data-panel') === 'route-panel-map' && map) {
map.setFitView(null, false, [50, 50, 50, 50]);
}
});
});
/* ── 路线地图(AMap 2.0,layout.php 已全局加载)────────────── */
var el = document.getElementById('route-map-canvas');
if (!el) return;
@@ -95,7 +164,7 @@ foreach (($realityNodes ?? []) as $rn) {
}
if (!route.length) return;
var map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
var overlays = []; // 当前画上去的所有覆盖物;地理编码补点后整体重画
/* 全量重画:先清掉旧覆盖物再按当前 route 数据画。
@@ -113,10 +182,13 @@ foreach (($realityNodes ?? []) as $rn) {
if (s && en) inPath.push([s.lng, s.lat], [en.lng, en.lat]);
if (s && prevEnd) hopPath.push([prevEnd.lng, prevEnd.lat], [s.lng, s.lat]);
/* 圆形标记放这段行程线的中点(起终点都有才取中点),缺一头就放有的一头 */
/* 圆形标记放这段行程线的中点(起终点都有才取中点),缺一头就放有的一头。
⚠️ position 必须是 [lng, lat] 数组 —— 把 {lng,lat} 对象直接传给
AMap 会抛 Invalid Object,forEach 当场中断、整张地图一个覆盖物
都不剩(批 11 上线后踩的坑,光秃秃地图就是它)。 */
var pos = (s && en)
? [(s.lng + en.lng) / 2, (s.lat + en.lat) / 2]
: (s || en);
: (s ? [s.lng, s.lat] : (en ? [en.lng, en.lat] : null));
if (pos) {
var html = '<div class="route-marker" title="' + n.title.replace(/"/g, '&quot;') + '">'
+ (n.img
@@ -188,34 +260,3 @@ foreach (($realityNodes ?? []) as $rn) {
})();
</script>
<?php endif; ?>
<div class="timeline-grid">
<section class="timeline-col">
<h2>📋 计划 (<?= count($planNodes) ?>)</h2>
<?php if (empty($planNodes)): ?>
<p class="muted">暂无计划节点</p>
<?php else: ?>
<?php foreach ($planNodes as $n): ?>
<?php $cardCategory = 'plan'; require __DIR__ . '/_node_card.php'; ?>
<?php endforeach; ?>
<?php endif; ?>
</section>
<section class="timeline-col">
<h2>🎬 实际 (<?= count($realityNodes) ?>)</h2>
<?php if (empty($realityNodes)): ?>
<p class="muted">暂无执行节点</p>
<?php else: ?>
<?php foreach ($realityNodes as $n): ?>
<?php $cardCategory = 'reality'; require __DIR__ . '/_node_card.php'; ?>
<?php endforeach; ?>
<?php endif; ?>
</section>
</div>
<?php
$commentTargetType = \App\Comments::TARGET_ACTIVITY;
$commentTargetId = (int)$activity['id'];
require dirname(__DIR__) . '/comments/_block.php';
?>