2026-09-24 17:03:55 +08:00
|
|
|
|
<?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']]),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
?>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<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: ?>
|
2026-09-24 17:03:55 +08:00
|
|
|
|
|
|
|
|
|
|
<?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 home-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">
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<?php foreach ($activities as $a): ?>
|
2026-09-24 17:03:55 +08:00
|
|
|
|
<?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">
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<h3><?= e($a['title']) ?></h3>
|
2026-09-24 17:03:55 +08:00
|
|
|
|
<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>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
2026-09-24 17:03:55 +08:00
|
|
|
|
|
|
|
|
|
|
<?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, '&').replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php endif; ?>
|