Files
TripLog/templates/activities/index.php
T
laoyang 331fadfb48 地图与正文同宽 / 详情页封面改列表行样式 / 封面拖拽上传(批 21)
① 带地图的页面(首页旅行地图、详情页行程地图)原本铺满整个视口
  (批 18 的负 margin 出血),用户反馈「上面正文固定宽、下面地图铺满屏」
  看着奇怪 → 改回与正文版心同宽,bleed() 已删、CSS 去边框覆盖已删。

② 活动详情页顶部整幅 16:9 大封面太大 → 改用活动列表同款「封面行」
  (.act-row-static:小封面 + 参与人 + 统计),数据复用 collectStats
  同一聚合口径;.activity-cover 样式删除。

③ 创建/编辑活动封面支持直接把图片拖进上传区:drop→DataTransfer 塞给
  原有 file input 并触发 change,类型/大小校验与裁剪流程原样复用;
  拖入悬停给高亮反馈。

验证:本地全链(含 _map12/_map16 同宽与封面行断言、5 档视口)全绿;
部署哨兵同步改向(批 17/18 锚定旧实现的断言全部反向或改口径)。
2026-09-24 22:13:29 +08:00

268 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/* 首页两个视图:默认「旅行地图」(一次旅行一枚图标,点进去就是那次旅行),
另一个标签是「活动列表」(每行一条:封面 + 标题简述 / 主要参与人 / 去了几个地方 /
经历了多长时间)。数据由 ActivityController::collectStats() 一次聚合好。 */
$stats = $stats ?? [];
$spots = [];
foreach ($activities as $a) {
$st = $stats[(int)$a['id']] ?? null;
if (!$st || $st['lat'] === null || $st['lng'] === null) continue;
$spots[] = [
'id' => (int)$a['id'],
'title' => (string)$a['title'],
'place' => (string)($st['place_main'] ?? ''),
'lat' => round((float)$st['lat'], 6),
'lng' => round((float)$st['lng'], 6),
'img' => !empty($a['cover_image']) ? media_cover((int)$a['id']) : '',
'url' => url('activities/show', ['id' => $a['id']]),
];
}
?>
<div class="page-header">
<h1>活动列表</h1>
</div>
<?php if (empty($activities)): ?>
<div class="empty">
<p>还没有任何活动</p>
<a href="<?= url('activities/create') ?>" class="btn-primary">创建第一个活动</a>
</div>
<?php else: ?>
<?php if ($spots): ?>
<div class="route-tabs" role="tablist">
<button type="button" class="route-tab is-active" data-panel="home-panel-map"
role="tab" aria-selected="true">🗺️ 旅行地图</button>
<button type="button" class="route-tab" data-panel="home-panel-list"
role="tab" aria-selected="false">📋 活动列表</button>
</div>
<div class="route-panel" id="home-panel-map">
<div class="route-map" id="home-map-canvas"
data-spots="<?= e(json_encode($spots, JSON_UNESCAPED_UNICODE)) ?>"></div>
<p class="route-map-hint">每一枚图标是一次旅行(落在那次旅行去过的地方的重心),点图标就进那次旅行的详情。</p>
</div>
<div class="route-panel" id="home-panel-list" hidden>
<?php endif; ?>
<div class="act-list">
<?php foreach ($activities as $a): ?>
<?php
$st = $stats[(int)$a['id']] ?? null;
$placeCount = (int)($st['place_count'] ?? 0);
$placeMain = (string)($st['place_main'] ?? '');
$nodeCount = (int)($st['node_count'] ?? 0);
$days = (int)($st['days'] ?? 0);
$spanText = (string)($st['span_text'] ?? '');
$memberCount = (int)($st['member_count'] ?? 0);
$members = array_slice($st['members'] ?? [], 0, 5);
?>
<a href="<?= url('activities/show', ['id' => $a['id']]) ?>" class="act-row">
<div class="act-row-cover">
<?php if (!empty($a['cover_image'])): ?>
<img src="<?= e(media_cover((int)$a['id'])) ?>" alt="">
<?php else: ?>
<div class="act-row-cover-ph">📸</div>
<?php endif; ?>
</div>
<div class="act-row-body">
<h3><?= e($a['title']) ?></h3>
<p class="muted act-row-desc"><?= e($a['description'] ?: '暂无简介') ?></p>
<?php if ($members): ?>
<div class="act-row-members">
<?php foreach ($members as $m): ?>
<span class="act-member">
<?php if (!empty($m['avatar'])): ?>
<img src="<?= e($m['avatar']) ?>" alt="">
<?php else: ?>
<span class="act-member-initial"><?= e(mb_substr((string)$m['name'], 0, 1)) ?></span>
<?php endif; ?>
<span class="act-member-name"><?= e($m['name']) ?></span>
</span>
<?php endforeach; ?>
<?php if ($memberCount > count($members)): ?>
<span class="act-member-more">等 <?= $memberCount ?> 人</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="meta act-row-meta">
<?php if ($placeCount > 0): ?>
<span>📍 <?= $placeCount ?> 个地方<?= $placeMain !== '' ? '(' . e($placeMain) . ')' : '' ?></span>
<?php endif; ?>
<?php if ($days > 0): ?>
<span>🕒 <?= $days ?> 天<?= $spanText !== '' ? '(' . e($spanText) . ')' : '' ?></span>
<?php endif; ?>
<span>🧳 <?= $nodeCount ?> 个节点</span>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php if ($spots): ?>
</div>
<script>
(function () {
'use strict';
/* ── 标签页:地图默认打开;切回地图时重新适配视野并重排图标 ── */
var map = null;
var tabs = Array.prototype.slice.call(document.querySelectorAll('.route-tab'));
var panels = {
'home-panel-map': document.getElementById('home-panel-map'),
'home-panel-list': document.getElementById('home-panel-list'),
};
tabs.forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-panel');
if (!panels[id]) return;
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 (k) {
if (panels[k]) panels[k].hidden = (k !== id);
});
if (id === 'home-panel-map' && map) {
fitView();
relayout();
}
});
});
var el = document.getElementById('home-map-canvas');
if (!el) return;
if (typeof AMap === 'undefined' || window.__AMAP_ERROR) {
el.innerHTML = '<p class="route-map-fallback">地图脚本没有加载成功,暂时无法显示旅行地图。</p>';
return;
}
var spots;
try {
spots = JSON.parse(el.getAttribute('data-spots') || '[]');
} catch (e) {
return;
}
if (!spots.length) return;
/* 图标下面还挂了一行活动名,整块是 56×64 的**矩形**,所以这里按矩形判重叠。
(详情页那两处是纯圆标,才按中心距判 —— 形状不同判定就不同,别混用。) */
var MARK_W = 56, MARK_H = 64, STEP = MARK_W + 12;
function shortTitle(t) {
return (t && t.length > 8) ? t.slice(0, 8) + '…' : (t || '');
}
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* 批 21:地图不再铺满整个视口 —— 用户反馈「上面正文固定宽、下面地图铺满屏」
看着奇怪,改回与正文版心同宽(批 18 的负 margin 出血逻辑 bleed() 已删)。 */
map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
window.__homeMap = map;
var markers = [];
spots.forEach(function (s) {
var inner = s.img
? '<img src="' + esc(s.img) + '" alt="">'
: '<span class="home-marker-emoji">📍</span>';
var tip = s.place ? s.title + '(' + s.place + ')' : s.title;
var html = '<div class="home-marker-wrap" title="' + esc(tip) + '">'
+ '<div class="home-marker">' + inner + '</div>'
+ '<div class="home-marker-label">' + esc(shortTitle(s.title)) + '</div>'
+ '</div>';
/* ⚠️ position 必须是 [lng, lat] 数组:传 {lng,lat} 裸对象会让 AMap 抛错,
整个绘制循环当场中断(批 11 的「光秃秃地图」就是它)。 */
var mk = new AMap.Marker({
position: [s.lng, s.lat],
content: html,
anchor: 'center',
map: map,
});
mk.on('click', function () { window.location.href = s.url; });
markers.push(mk);
});
/* 适配视野,但**别放到街道级**:只有一次旅行(或几次都在同城)时
setFitView 会一路放大到 18 级,那是导航视角不是"旅行足迹"视角。
上限定在 11 级(能看清一座城市及其周边)。setZoom 会触发 zoomend → 自动重排。 */
var MAX_ZOOM = 11;
function fitView() {
if (!markers.length) return;
map.setFitView(null, false, [40, 40, 40, 40]);
if (map.getZoom() > MAX_ZOOM) map.setZoom(MAX_ZOOM);
}
fitView();
/* 图标重合(同一个城市去过好几次)时让开:没有线段可沿,就先左右、
再上下,按**屏幕像素**判定(缩放到"看全国"时两个城市会叠在一起)。 */
function place(base, placed) {
var cand = [{ x: 0, y: 0 }];
var k, m, sg;
for (k = 1; k <= 6; k++) {
m = Math.ceil(k / 2) * STEP; sg = (k % 2 === 1) ? 1 : -1;
cand.push({ x: m * sg, y: 0 });
}
for (k = 1; k <= 4; k++) {
m = Math.ceil(k / 2) * Math.round(MARK_H * 0.9); sg = (k % 2 === 1) ? 1 : -1;
cand.push({ x: 0, y: m * sg });
}
var hit = function (x, y) {
for (var j = 0; j < placed.length; j++) {
var q = placed[j];
if (Math.abs(x - q.x) < MARK_W && Math.abs(y - q.y) < MARK_H) return true;
}
return false;
};
var pick = null;
for (var i = 0; i < cand.length; i++) {
if (!hit(base.x + cand[i].x, base.y + cand[i].y)) { pick = cand[i]; break; }
}
if (!pick) pick = cand[0];
placed.push({ x: base.x + pick.x, y: base.y + pick.y });
return pick;
}
/* 缩放会改变图标之间的屏幕距离,所以每次缩放结束都要重排一次。
只改 offset,不重建标记 —— 重建会把点击事件也一起丢掉。 */
function relayout() {
var placed = [];
markers.forEach(function (mk) {
var base = null;
try {
base = map.lngLatToContainer(mk.getPosition());
} catch (e) {
base = null;
}
if (!base) return;
var off = place(base, placed);
mk.setOffset(new AMap.Pixel(off.x, off.y));
});
}
relayout();
map.on('zoomend', relayout);
/* 改窗口大小 / 旋转屏幕 / 换分辨率:地图容器跟着版心变,视野和图标排布
都得按新尺寸重算一次。防抖 200ms,别在拖动时反复重排。 */
var resizeTimer = null;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
if (!map) return;
map.resize();
fitView();
relayout();
}, 200);
});
})();
</script>
<?php endif; ?>
<?php endif; ?>