活动详情页新增「行程路线」地图(高德,批 10)

按时间顺序把执行节点(reality)的起终点坐标连成路线:节点内实线
(showDir 方向箭头)、节点之间虚线衔接;每个节点在出发地(没有出发地
就放到达地)放一个圆形标记 —— 有图片附件显示图片,没有就显示节点
类型 emoji,左上角带序号;点击标记/红点打开对应节点详情。到达地与
出发地不同点时另画红色小圆点。

实现要点:
- 数据层 Attachments::firstImageIdsForNodes():一条 GROUP BY + MIN(id)
  批量取各执行节点首张图片附件,防 N+1;
- 模板只收「有坐标的执行节点」,计划节点与无坐标节点不上图;
  路线数据经 data-route 属性以 JSON 交给内联 JS;
- AMap 2.0(layout.php 已全局加载),加载失败时给一句人话兜底不白屏;
- 地图区块位于时间轴之下、评论区之前。

已验证:本地全链 11 步全绿(渲染检查 330 项);远端 e2e 全绿(新增
10 项地图断言);部署静态断言与双 PHP 真实渲染通过后提交。
This commit is contained in:
2026-09-24 11:12:27 +08:00
parent 0e30495f14
commit 69279db1b2
4 changed files with 189 additions and 0 deletions
+22
View File
@@ -141,6 +141,28 @@ final class Attachments
return $out;
}
/**
* 一批节点的「第一张图片附件」id(活动页路线地图的圆形标记用)。
* 一条 GROUP BY 带回全部节点,防 N+1;id 递增即上传顺序的第一张。
*
* @return array<int, int> node_id => attachment_id
*/
public static function firstImageIdsForNodes(array $nodeIds): array
{
if ($nodeIds === []) return [];
$ids = array_map('intval', $nodeIds);
$ph = implode(',', array_fill(0, count($ids), '?'));
$stmt = Database::get()->prepare("SELECT target_id, MIN(id) AS att_id FROM attachments
WHERE target_type = '" . self::TARGET_NODE . "' AND is_image = 1 AND target_id IN ($ph)
GROUP BY target_id");
$stmt->execute($ids);
$out = [];
foreach ($stmt->fetchAll() as $row) {
$out[(int)$row['target_id']] = (int)$row['att_id'];
}
return $out;
}
/** 历史行没有 storage 列时按本地处理 */
public static function storageOf(array $att): string
{
+2
View File
@@ -227,6 +227,8 @@ class ActivityController
'realityNodes' => $realityNodes,
'executedPlanIds' => $executedPlanIds,
'nodeMembers' => $nodeMembers,
// 路线地图:每个执行节点第一张图片附件 id(圆形标记用),一条 GROUP BY 防止 N+1
'nodeFirstImage' => Attachments::firstImageIdsForNodes(array_column($realityNodes, 'id')),
'expenseSummary' => Expenses::summaryForActivity($id),
'commentTree' => Comments::tree(Comments::TARGET_ACTIVITY, $id),
'pageTitle' => $activity['title'],