修「光秃秃地图」回归 + 详情页改标签页布局(批 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
+14 -3
View File
@@ -1253,9 +1253,19 @@ video.video-thumb.video-thumb-mini {
.lightbox-file-dl { display: inline-block; } .lightbox-file-dl { display: inline-block; }
/* ── 20. 活动页路线地图(批 10)──────────────────────────────────────────── */ /* ── 20. 活动页路线地图(批 10)──────────────────────────────────────────── */
.route-map-section { margin-top: 28px; } /* 批 12:详情页标签页 —— 地图独占一页、尽量大;节点详情切到另一页 */
.route-tabs { display: flex; gap: 8px; margin: 0 0 14px; }
.route-tab {
border: 1px solid var(--border); background: var(--surface);
border-radius: 999px; padding: 8px 18px; font-size: 14px;
cursor: pointer; color: var(--text-soft);
}
.route-tab:hover { border-color: var(--border-strong); color: var(--text); }
.route-tab.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
.route-panel[hidden] { display: none; }
.route-map-section { margin-top: 0; }
.route-map { .route-map {
height: 380px; height: clamp(480px, 72vh, 860px);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
overflow: hidden; overflow: hidden;
@@ -1290,5 +1300,6 @@ video.video-thumb.video-thumb-mini {
} }
.route-legend-end { color: #ef4444; font-weight: 700; } .route-legend-end { color: #ef4444; font-weight: 700; }
@media (max-width: 640px) { @media (max-width: 640px) {
.route-map { height: 300px; } .route-map { height: 62vh; }
.route-tab { padding: 7px 14px; font-size: 13px; }
} }
+76 -35
View File
@@ -67,8 +67,14 @@ foreach (($realityNodes ?? []) as $rn) {
} }
?> ?>
<?php if ($routeNodes !== []): ?> <?php if ($routeNodes !== []): ?>
<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"> <section class="route-map-section">
<h2 class="section-title" id="route-map">🗺️ 行程路线</h2>
<div class="route-map" id="route-map-canvas" <div class="route-map" id="route-map-canvas"
data-route="<?= e(json_encode($routeNodes, JSON_UNESCAPED_UNICODE)) ?>"></div> data-route="<?= e(json_encode($routeNodes, JSON_UNESCAPED_UNICODE)) ?>"></div>
<p class="muted route-map-hint"> <p class="muted route-map-hint">
@@ -76,9 +82,72 @@ foreach (($realityNodes ?? []) as $rn) {
<span class="route-legend-end">红点</span> = 到达地。只有地名没坐标的地点会自动向高德查询定位,太偏僻的地名可能查不到。 <span class="route-legend-end">红点</span> = 到达地。只有地名没坐标的地点会自动向高德查询定位,太偏僻的地名可能查不到。
</p> </p>
</section> </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> <script>
(function () { (function () {
'use strict'; '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'); var el = document.getElementById('route-map-canvas');
if (!el) return; if (!el) return;
@@ -95,7 +164,7 @@ foreach (($realityNodes ?? []) as $rn) {
} }
if (!route.length) return; 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 = []; // 当前画上去的所有覆盖物;地理编码补点后整体重画 var overlays = []; // 当前画上去的所有覆盖物;地理编码补点后整体重画
/* 全量重画:先清掉旧覆盖物再按当前 route 数据画。 /* 全量重画:先清掉旧覆盖物再按当前 route 数据画。
@@ -113,10 +182,13 @@ foreach (($realityNodes ?? []) as $rn) {
if (s && en) inPath.push([s.lng, s.lat], [en.lng, en.lat]); 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]); 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) var pos = (s && en)
? [(s.lng + en.lng) / 2, (s.lat + en.lat) / 2] ? [(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) { if (pos) {
var html = '<div class="route-marker" title="' + n.title.replace(/"/g, '&quot;') + '">' var html = '<div class="route-marker" title="' + n.title.replace(/"/g, '&quot;') + '">'
+ (n.img + (n.img
@@ -188,34 +260,3 @@ foreach (($realityNodes ?? []) as $rn) {
})(); })();
</script> </script>
<?php endif; ?> <?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';
?>