首页地图图标改「左图右文」方形卡片 + 同城拥挤扇形拉出排布(批 22)
① 首页地图圆形图标改成方形小卡片:左侧封面图(无封面放 📍),
右侧活动名(10 字截断)+ 地点/参与人摘要(超出 5 位显示「等 N 人」)。
② 同城/邻近(屏幕中心距 <110px)的活动归为一簇,按环形扇开拉到
外围规则排开,SVG 引线连回真实位置;单独的活动原地不动不画线。
判碰用椭圆(横半轴卡宽/纵半轴卡高),并查集聚类,半径 70→300 逐轮
放大;缩放/换视野/改窗口后重排。卡片仍锚定真实坐标,点击进活动。
③ 修复扇开越界:原先固定从正上方开始扇,城市贴近地图上缘时卡片被
甩到容器外(y=-29)、被上方标签栏挡住点不到。改为起始角优先朝向
地图中心、每 22.5° 环绕尝试,且候选点必须落在地图容器边界内;
全部放不下时钳回边界内兜底。
验证:本地全链(含 _map16 真浏览器:3 卡片、扇形拉出、引线、椭圆
判碰、容器边界不越界、5 档视口、点击进详情)全绿。
This commit is contained in:
+37
-16
@@ -1324,23 +1324,44 @@ video.video-thumb.video-thumb-mini {
|
|||||||
/* 联想选中后回填的详细地址(让人看见记下来的是什么) */
|
/* 联想选中后回填的详细地址(让人看见记下来的是什么) */
|
||||||
.loc-detail { margin-top: 6px; font-size: 12.5px; word-break: break-all; }
|
.loc-detail { margin-top: 6px; font-size: 12.5px; word-break: break-all; }
|
||||||
|
|
||||||
/* ── 22. 首页:旅行地图 + 活动列表两个标签页(批 16)───────────── */
|
/* ── 22. 首页:旅行地图 + 活动列表两个标签页(批 16,批 22 改卡片)── */
|
||||||
/* 一次旅行一枚图标:圆形(有封面放封面,没有放 📍)+ 下面一行活动名 */
|
/* 一次旅行一枚「方形小卡片」:左边封面图(没有放 📍),右边活动名 +
|
||||||
.home-marker-wrap { display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: pointer; }
|
地点/参与人摘要。同城/邻近的旅行被扇开到外围时,用一条引线连回真实位置
|
||||||
.home-marker {
|
(线是 marker content 里的 SVG;外层画布 pointer-events:none,
|
||||||
width: 44px; height: 44px; border-radius: 50%;
|
不挡底图拖拽,只有卡片本身可点)。 */
|
||||||
border: 3px solid #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
|
.home-pin { position: relative; width: 600px; height: 600px; pointer-events: none; }
|
||||||
background: var(--surface); overflow: hidden;
|
/* AMap 的标记包装层 .amap-marker 尺寸跟着 content(600×600)走、默认可命中,
|
||||||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
会把别家卡片的点击整层挡住(amap-marker intercepts pointer events)。
|
||||||
|
整层禁掉命中、只留卡片本身可点 —— 点击事件沿 DOM 冒泡,AMap 的
|
||||||
|
marker click 照常触发。 */
|
||||||
|
#home-map-canvas .amap-marker { pointer-events: none !important; }
|
||||||
|
.home-pin svg { position: absolute; left: 0; top: 0; overflow: visible; }
|
||||||
|
.home-pin-line { stroke: #8aa0b8; stroke-width: 1.5; }
|
||||||
|
.home-pin-dot { fill: #3b82f6; }
|
||||||
|
.home-card {
|
||||||
|
position: absolute; transform: translate(-50%, -50%);
|
||||||
|
display: flex; align-items: center; gap: 7px;
|
||||||
|
padding: 5px 10px 5px 5px; background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid var(--border); border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
|
||||||
|
cursor: pointer; pointer-events: auto;
|
||||||
|
width: max-content; max-width: 190px;
|
||||||
}
|
}
|
||||||
.home-marker img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
.home-card:hover { border-color: var(--accent); }
|
||||||
.home-marker-emoji { line-height: 1; }
|
.home-card-img {
|
||||||
.home-marker-label {
|
width: 42px; height: 42px; border-radius: 7px; overflow: hidden; flex: none;
|
||||||
max-width: 92px; padding: 1px 7px; border-radius: 999px;
|
background: var(--bg-soft); display: flex; align-items: center; justify-content: center;
|
||||||
background: rgba(255, 255, 255, .94); color: var(--text);
|
font-size: 20px;
|
||||||
font-size: 11.5px; line-height: 17px; white-space: nowrap;
|
}
|
||||||
overflow: hidden; text-overflow: ellipsis;
|
.home-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .22);
|
.home-card-body { min-width: 0; }
|
||||||
|
.home-card-title {
|
||||||
|
font-size: 12.5px; font-weight: 600; color: var(--text); line-height: 1.35;
|
||||||
|
max-width: 122px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.home-card-sub {
|
||||||
|
font-size: 11px; color: var(--text-soft); line-height: 1.4; margin-top: 1px;
|
||||||
|
max-width: 122px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 列表:每行一条 —— 左边封面、右边标题/简述/参与人/摘要 */
|
/* 列表:每行一条 —— 左边封面、右边标题/简述/参与人/摘要 */
|
||||||
|
|||||||
+158
-48
@@ -14,6 +14,9 @@ foreach ($activities as $a) {
|
|||||||
'lat' => round((float)$st['lat'], 6),
|
'lat' => round((float)$st['lat'], 6),
|
||||||
'lng' => round((float)$st['lng'], 6),
|
'lng' => round((float)$st['lng'], 6),
|
||||||
'img' => !empty($a['cover_image']) ? media_cover((int)$a['id']) : '',
|
'img' => !empty($a['cover_image']) ? media_cover((int)$a['id']) : '',
|
||||||
|
// 批 22:地图卡片右侧的简要参与人(前两位 + 总人数),与列表同一份聚合口径
|
||||||
|
'members' => array_slice($st['members'] ?? [], 0, 2),
|
||||||
|
'memberCount' => (int)($st['member_count'] ?? 0),
|
||||||
'url' => url('activities/show', ['id' => $a['id']]),
|
'url' => url('activities/show', ['id' => $a['id']]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -148,12 +151,17 @@ foreach ($activities as $a) {
|
|||||||
}
|
}
|
||||||
if (!spots.length) return;
|
if (!spots.length) return;
|
||||||
|
|
||||||
/* 图标下面还挂了一行活动名,整块是 56×64 的**矩形**,所以这里按矩形判重叠。
|
/* ── 批 22:图标改成「方形小卡片」(左图右文:活动名 + 地点/参与人摘要)。
|
||||||
(详情页那两处是纯圆标,才按中心距判 —— 形状不同判定就不同,别混用。) */
|
同城/邻近的旅行在屏幕上挤在一起时,按**环形扇开**拉到外围规则排开,
|
||||||
var MARK_W = 56, MARK_H = 64, STEP = MARK_W + 12;
|
并用一条引线连回真实位置 —— 线画在 marker content 的 SVG 里,
|
||||||
|
position 仍是真实坐标(点卡片进对应活动,线只是视觉引导)。
|
||||||
|
没被挤到的旅行不偏移也不画线,卡片就落在真实位置上。 */
|
||||||
|
var CARD_W = 186, CARD_H = 58; // 卡片名义尺寸(按中心距判碰用)
|
||||||
|
var CLUSTER_DIST = 110; // 中心距小于它 → 归为同簇(要扇开)
|
||||||
|
var CANVAS = 600, C = CANVAS / 2; // 每枚标记的 content 画布(真实位置在画布中心)
|
||||||
|
|
||||||
function shortTitle(t) {
|
function shortTitle(t) {
|
||||||
return (t && t.length > 8) ? t.slice(0, 8) + '…' : (t || '');
|
return (t && t.length > 10) ? t.slice(0, 10) + '…' : (t || '');
|
||||||
}
|
}
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
return String(s === null || s === undefined ? '' : s)
|
return String(s === null || s === undefined ? '' : s)
|
||||||
@@ -167,26 +175,52 @@ foreach ($activities as $a) {
|
|||||||
map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
|
map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
|
||||||
window.__homeMap = map;
|
window.__homeMap = map;
|
||||||
|
|
||||||
var markers = [];
|
function shortMembers(s) {
|
||||||
spots.forEach(function (s) {
|
var names = (s.members || []).map(function (m) { return m.name; });
|
||||||
|
var t = names.join('、');
|
||||||
|
if (s.memberCount > names.length) t += ' 等 ' + s.memberCount + ' 人';
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
function cardHtml(s, dx, dy) {
|
||||||
|
var moved = Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5;
|
||||||
var inner = s.img
|
var inner = s.img
|
||||||
? '<img src="' + esc(s.img) + '" alt="">'
|
? '<img src="' + esc(s.img) + '" alt="">'
|
||||||
: '<span class="home-marker-emoji">📍</span>';
|
: '<span>📍</span>';
|
||||||
|
var sub = [];
|
||||||
|
if (s.place) sub.push(esc(s.place));
|
||||||
|
var mem = shortMembers(s);
|
||||||
|
if (mem) sub.push(esc(mem));
|
||||||
var tip = s.place ? s.title + '(' + s.place + ')' : s.title;
|
var tip = s.place ? s.title + '(' + s.place + ')' : s.title;
|
||||||
var html = '<div class="home-marker-wrap" title="' + esc(tip) + '">'
|
var html = '<div class="home-pin">'
|
||||||
+ '<div class="home-marker">' + inner + '</div>'
|
+ '<svg width="' + CANVAS + '" height="' + CANVAS + '">';
|
||||||
+ '<div class="home-marker-label">' + esc(shortTitle(s.title)) + '</div>'
|
if (moved) {
|
||||||
+ '</div>';
|
html += '<line class="home-pin-line" x1="' + C + '" y1="' + C + '"'
|
||||||
|
+ ' x2="' + (C + dx) + '" y2="' + (C + dy) + '"/>'
|
||||||
|
+ '<circle class="home-pin-dot" cx="' + C + '" cy="' + C + '" r="3"/>';
|
||||||
|
}
|
||||||
|
html += '</svg>'
|
||||||
|
+ '<div class="home-card" style="left:' + (C + dx) + 'px;top:' + (C + dy) + 'px;"'
|
||||||
|
+ ' title="' + esc(tip) + '">'
|
||||||
|
+ '<div class="home-card-img">' + inner + '</div>'
|
||||||
|
+ '<div class="home-card-body">'
|
||||||
|
+ '<div class="home-card-title">' + esc(shortTitle(s.title)) + '</div>'
|
||||||
|
+ (sub.length ? '<div class="home-card-sub">' + sub.join(' · ') + '</div>' : '')
|
||||||
|
+ '</div></div></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
var markers = []; // { mk: AMap.Marker, spot: spot }
|
||||||
|
spots.forEach(function (s) {
|
||||||
/* ⚠️ position 必须是 [lng, lat] 数组:传 {lng,lat} 裸对象会让 AMap 抛错,
|
/* ⚠️ position 必须是 [lng, lat] 数组:传 {lng,lat} 裸对象会让 AMap 抛错,
|
||||||
整个绘制循环当场中断(批 11 的「光秃秃地图」就是它)。 */
|
整个绘制循环当场中断(批 11 的「光秃秃地图」就是它)。 */
|
||||||
var mk = new AMap.Marker({
|
var mk = new AMap.Marker({
|
||||||
position: [s.lng, s.lat],
|
position: [s.lng, s.lat],
|
||||||
content: html,
|
content: cardHtml(s, 0, 0),
|
||||||
anchor: 'center',
|
anchor: 'center',
|
||||||
map: map,
|
map: map,
|
||||||
});
|
});
|
||||||
mk.on('click', function () { window.location.href = s.url; });
|
mk.on('click', function () { window.location.href = s.url; });
|
||||||
markers.push(mk);
|
markers.push({ mk: mk, spot: s });
|
||||||
});
|
});
|
||||||
|
|
||||||
/* 适配视野,但**别放到街道级**:只有一次旅行(或几次都在同城)时
|
/* 适配视野,但**别放到街道级**:只有一次旅行(或几次都在同城)时
|
||||||
@@ -200,49 +234,125 @@ foreach ($activities as $a) {
|
|||||||
}
|
}
|
||||||
fitView();
|
fitView();
|
||||||
|
|
||||||
/* 图标重合(同一个城市去过好几次)时让开:没有线段可沿,就先左右、
|
/* 椭圆判碰:横向半轴按卡宽、纵向按卡高 —— 水平相邻要留出卡宽、
|
||||||
再上下,按**屏幕像素**判定(缩放到"看全国"时两个城市会叠在一起)。 */
|
垂直相邻只要留出卡高(卡片是横条,椭圆比圆省地方)。 */
|
||||||
function place(base, placed) {
|
function farEnough(a, b) {
|
||||||
var cand = [{ x: 0, y: 0 }];
|
var dx = Math.abs(a.x - b.x) / (CARD_W / 2 + 8);
|
||||||
var k, m, sg;
|
var dy = Math.abs(a.y - b.y) / (CARD_H / 2 + 8);
|
||||||
for (k = 1; k <= 6; k++) {
|
return (dx * dx + dy * dy) >= 1;
|
||||||
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,不重建标记 —— 重建会把点击事件也一起丢掉。 */
|
再把每簇**环形均匀扇开**——起始角优先朝向地图中心(往空旷处扇),
|
||||||
|
再每 22.5° 环绕一周逐个尝试;半径从 70px 起步逐轮放大,直到簇内
|
||||||
|
两两、与其他簇已放的卡片、以及**地图容器边界**都不冲突为止。
|
||||||
|
单枚标记原地不动。缩放/换视野/改窗口后重跑。
|
||||||
|
(边界约束是批 22 修的:原先固定从正上方开始扇,城市贴近地图上缘时
|
||||||
|
卡片被甩到容器外、被上方的标签栏挡住,点不到。) */
|
||||||
function relayout() {
|
function relayout() {
|
||||||
var placed = [];
|
var items = [];
|
||||||
markers.forEach(function (mk) {
|
markers.forEach(function (m) {
|
||||||
var base = null;
|
var base = null;
|
||||||
try {
|
try {
|
||||||
base = map.lngLatToContainer(mk.getPosition());
|
base = map.lngLatToContainer(new AMap.LngLat(m.spot.lng, m.spot.lat));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
base = null;
|
base = null;
|
||||||
}
|
}
|
||||||
if (!base) return;
|
items.push({ mk: m.mk, spot: m.spot, base: base, dx: 0, dy: 0 });
|
||||||
var off = place(base, placed);
|
});
|
||||||
mk.setOffset(new AMap.Pixel(off.x, off.y));
|
|
||||||
|
/* 并查集:中心距 < CLUSTER_DIST 的归为同簇 */
|
||||||
|
var parent = items.map(function (_, i) { return i; });
|
||||||
|
function find(i) {
|
||||||
|
while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (!items[i].base) continue;
|
||||||
|
for (var j = i + 1; j < items.length; j++) {
|
||||||
|
if (!items[j].base) continue;
|
||||||
|
if (Math.hypot(items[i].base.x - items[j].base.x,
|
||||||
|
items[i].base.y - items[j].base.y) < CLUSTER_DIST) {
|
||||||
|
parent[find(i)] = find(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var groups = {};
|
||||||
|
items.forEach(function (it, i) {
|
||||||
|
if (!it.base) return;
|
||||||
|
var r = find(i);
|
||||||
|
(groups[r] = groups[r] || []).push(it);
|
||||||
|
});
|
||||||
|
|
||||||
|
var placedPts = []; // 其他簇已放的卡片中心(跨簇也要防叠)
|
||||||
|
var mapW = 0, mapH = 0;
|
||||||
|
try {
|
||||||
|
mapW = map.getContainer().clientWidth || 0;
|
||||||
|
mapH = map.getContainer().clientHeight || 0;
|
||||||
|
} catch (e2) { mapW = 0; mapH = 0; }
|
||||||
|
/* 卡片中心不许越出地图容器:上下留半张卡高、左右留半张卡宽 */
|
||||||
|
function inBounds(p) {
|
||||||
|
if (!mapW || !mapH) return true;
|
||||||
|
return p.x >= CARD_W / 2 + 6 && p.x <= mapW - CARD_W / 2 - 6
|
||||||
|
&& p.y >= CARD_H / 2 + 6 && p.y <= mapH - CARD_H / 2 - 6;
|
||||||
|
}
|
||||||
|
Object.keys(groups).forEach(function (g) {
|
||||||
|
var grp = groups[g];
|
||||||
|
if (grp.length === 1) {
|
||||||
|
placedPts.push({ x: grp[0].base.x, y: grp[0].base.y });
|
||||||
|
return; // 单枚:原地,不偏移不画线
|
||||||
|
}
|
||||||
|
/* 簇中心 = 各真实位置的平均;起始角优先朝地图中心,再每 22.5° 环绕 */
|
||||||
|
var cx = 0, cy = 0;
|
||||||
|
grp.forEach(function (it) { cx += it.base.x; cy += it.base.y; });
|
||||||
|
cx /= grp.length; cy /= grp.length;
|
||||||
|
var toCenter = (mapW && mapH)
|
||||||
|
? Math.atan2(mapH / 2 - cy, mapW / 2 - cx) : 0;
|
||||||
|
var best = null;
|
||||||
|
for (var ai = 0; ai < 16 && !best; ai++) {
|
||||||
|
var a0 = toCenter + ai * Math.PI / 8;
|
||||||
|
for (var R = 70; R <= 300; R += 24) {
|
||||||
|
var pts = [], okAll = true;
|
||||||
|
for (var k = 0; k < grp.length; k++) {
|
||||||
|
var a = a0 + k * 2 * Math.PI / grp.length;
|
||||||
|
var p = { x: cx + R * Math.cos(a), y: cy + R * Math.sin(a) };
|
||||||
|
if (!inBounds(p)) { okAll = false; break; }
|
||||||
|
pts.push(p);
|
||||||
|
}
|
||||||
|
if (!okAll) continue;
|
||||||
|
for (var p1 = 0; p1 < pts.length && okAll; p1++) {
|
||||||
|
for (var q = 0; q < p1 && okAll; q++) {
|
||||||
|
if (!farEnough(pts[p1], pts[q])) okAll = false;
|
||||||
|
}
|
||||||
|
for (var q2 = 0; q2 < placedPts.length && okAll; q2++) {
|
||||||
|
if (!farEnough(pts[p1], placedPts[q2])) okAll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (okAll) { best = pts; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best) {
|
||||||
|
/* 各角度半径都放不下(地图太挤/太小):按优先方向摆一圈后
|
||||||
|
把坐标钳回容器内 —— 宁可挤一点也不许出界。 */
|
||||||
|
best = grp.map(function (it3, k3) {
|
||||||
|
var a3 = toCenter + k3 * 2 * Math.PI / grp.length;
|
||||||
|
var px = cx + 300 * Math.cos(a3), py = cy + 300 * Math.sin(a3);
|
||||||
|
if (mapW && mapH) {
|
||||||
|
px = Math.max(CARD_W / 2 + 6, Math.min(mapW - CARD_W / 2 - 6, px));
|
||||||
|
py = Math.max(CARD_H / 2 + 6, Math.min(mapH - CARD_H / 2 - 6, py));
|
||||||
|
}
|
||||||
|
return { x: px, y: py };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
grp.forEach(function (it, k2) {
|
||||||
|
it.dx = best[k2].x - it.base.x;
|
||||||
|
it.dy = best[k2].y - it.base.y;
|
||||||
|
placedPts.push(best[k2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
items.forEach(function (it) {
|
||||||
|
if (it.base) it.mk.setContent(cardHtml(it.spot, it.dx, it.dy));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
relayout();
|
relayout();
|
||||||
|
|||||||
Reference in New Issue
Block a user