diff --git a/assets/style.css b/assets/style.css index d6342b2..3b8deec 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1242,3 +1242,44 @@ video.video-thumb.video-thumb-mini { } .lightbox-file-hint { margin: 0; font-size: 13px; color: var(--text-mute); } .lightbox-file-dl { display: inline-block; } + +/* ── 20. 活动页路线地图(批 10)──────────────────────────────────────────── */ +.route-map-section { margin-top: 28px; } +.route-map { + height: 380px; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + background: var(--bg-soft); +} +.route-map-hint { margin-top: 8px; font-size: 12.5px; } +.route-map-fallback { padding: 24px; text-align: center; color: var(--text-mute); } +/* 节点圆形标记:有图片附件放图片,没有放类型 emoji;左上角序号 */ +.route-marker { + position: relative; width: 40px; height: 40px; border-radius: 50%; + border: 3px solid #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, .35); + background: var(--surface); cursor: pointer; overflow: visible; + display: flex; align-items: center; justify-content: center; + font-size: 20px; +} +.route-marker img { + width: 100%; height: 100%; border-radius: 50%; + object-fit: cover; display: block; +} +.route-marker-emoji { line-height: 1; } +.route-marker-num { + position: absolute; top: -6px; right: -6px; min-width: 17px; height: 17px; + border-radius: 999px; background: #ef4444; color: #fff; + font-size: 11px; font-weight: 700; line-height: 17px; + text-align: center; padding: 0 3px; +} +/* 到达地小圆点 */ +.route-end-dot { + width: 12px; height: 12px; border-radius: 50%; + background: #ef4444; border: 2px solid #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, .3); +} +.route-legend-end { color: #ef4444; font-weight: 700; } +@media (max-width: 640px) { + .route-map { height: 300px; } +} diff --git a/src/Attachments.php b/src/Attachments.php index fb9301b..25e1f69 100644 --- a/src/Attachments.php +++ b/src/Attachments.php @@ -141,6 +141,28 @@ final class Attachments return $out; } + /** + * 一批节点的「第一张图片附件」id(活动页路线地图的圆形标记用)。 + * 一条 GROUP BY 带回全部节点,防 N+1;id 递增即上传顺序的第一张。 + * + * @return array 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 { diff --git a/src/Controllers/ActivityController.php b/src/Controllers/ActivityController.php index 2201f72..e0beee2 100644 --- a/src/Controllers/ActivityController.php +++ b/src/Controllers/ActivityController.php @@ -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'], diff --git a/templates/activities/show.php b/templates/activities/show.php index 11b615d..d400c4d 100644 --- a/templates/activities/show.php +++ b/templates/activities/show.php @@ -54,6 +54,130 @@ + (int)$rn['id'], + 'title' => (string)($rn['title'] ?: '(无标题节点)'), + 'icon' => (string)($rn['node_type_icon'] ?: '📍'), + 'img' => $imgId > 0 ? media_att($imgId) : '', + 'url' => url('nodes/show', ['id' => (int)$rn['id']]), + 'start' => $hasS ? ['lng' => (float)$s['lng'], 'lat' => (float)$s['lat']] : null, + 'end' => $hasE ? ['lng' => (float)$e['lng'], 'lat' => (float)$e['lat']] : null, + ]; +} +?> + +
+

🗺️ 行程路线

+
+

+ 按时间顺序连出的执行节点路线(节点间虚线衔接),点标记可打开对应节点。 + 线上圆标 = 节点出发地,红点 = 到达地。 +

+
+ + +