- 新增 src/Ics.php:RFC 5545 生成器(CRLF、75 octet 折行且不切断
UTF-8 字符、TEXT 转义、时间输出 UTC Z、UID 固定为 triplog-node-{id}
使重复导入按 UID 更新而不堆重复事件)。
- 新增路由 nodes/ics、activities/ics:节点级导出单个事件
(SUMMARY=活动名·节点标题、DTSTART/DTEND、LOCATION=出发地、
DESCRIPTION=车次/航班号+备注);活动级导出整个行程(跳过已取消
节点与已生成执行记录的计划节点,口径与详情页展示一致)。
- 入口:活动详情页「导出日历」、节点详情页「导出日历」、
节点卡片「日历」链接。
- 修复隐性事故:index.php 漏 require src/Ics.php(无 autoloader 的
项目里新增类必须同步入口),部署哨兵新增「src/ 每个类文件都被
index.php require」逐文件对账。
- 验证:php -l 61 文件全过;渲染检查 386 项全过;Ics 单测 42 项
全过(转义/折行/UTC/拼装);服务器 e2e 391 项全过(含 11c 节
17 项 ICS 断言)。
471 lines
24 KiB
PHP
471 lines
24 KiB
PHP
<div class="page-header">
|
||
<div>
|
||
<h1><?= e($activity['title']) ?></h1>
|
||
<p class="muted"><?= e($activity['description'] ?: '暂无简介') ?></p>
|
||
</div>
|
||
<div class="actions" style="display:flex; gap:8px; flex-wrap:wrap;">
|
||
<a href="<?= url('nodes/create', ['activity_id' => $activity['id'], 'category' => 'plan']) ?>" class="btn">+ 计划节点</a>
|
||
<a href="<?= url('nodes/create', ['activity_id' => $activity['id'], 'category' => 'reality']) ?>" class="btn-primary">+ 执行节点</a>
|
||
<a href="<?= url('activities/edit', ['id' => $activity['id']]) ?>" class="btn">编辑活动</a>
|
||
<a href="<?= url('activities/ics', ['id' => $activity['id']]) ?>" class="btn"
|
||
title="下载 .ics 导入到手机/电脑日历">📅 导出日历</a>
|
||
<form method="post" action="<?= url('activities/destroy') ?>" style="display:inline;"
|
||
onsubmit="return confirm('确定删除活动「<?= e($activity['title']) ?>」?\n该活动下的所有节点、附件、评论都会一并删除,且不可恢复。');">
|
||
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
|
||
<input type="hidden" name="id" value="<?= (int)$activity['id'] ?>">
|
||
<button type="submit" class="btn btn-danger">删除活动</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php
|
||
/* 批 21:顶部不再放整幅 16:9 大封面(太大),改用活动列表同款的「封面行」——
|
||
左边小封面、右边参与人与统计(去了几个地方 / 几天 / 几个节点)。
|
||
数据与列表同一口径(ActivityController::collectStats);没传 stats 时只渲染封面。 */
|
||
$actSt = $actStats[(int)$activity['id']] ?? null;
|
||
$placeCount = (int)($actSt['place_count'] ?? 0);
|
||
$placeMain = (string)($actSt['place_main'] ?? '');
|
||
$days = (int)($actSt['days'] ?? 0);
|
||
$spanText = (string)($actSt['span_text'] ?? '');
|
||
$nodeCount = (int)($actSt['node_count'] ?? 0);
|
||
$membersTop = array_slice($actSt['members'] ?? [], 0, 5);
|
||
$memberCount = (int)($actSt['member_count'] ?? 0);
|
||
?>
|
||
<?php if (!empty($activity['cover_image']) || $actSt): ?>
|
||
<div class="act-row act-row-static">
|
||
<div class="act-row-cover">
|
||
<?php if (!empty($activity['cover_image'])): ?>
|
||
<img src="<?= e(media_cover((int)$activity['id'])) ?>" alt="">
|
||
<?php else: ?>
|
||
<div class="act-row-cover-ph">📸</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php if ($actSt): ?>
|
||
<div class="act-row-body">
|
||
<?php if ($membersTop): ?>
|
||
<div class="act-row-members">
|
||
<?php foreach ($membersTop 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($membersTop)): ?>
|
||
<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>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php $expenseSummary = $expenseSummary ?? ['total' => 0.0, 'count' => 0]; ?>
|
||
<?php if ((int)$expenseSummary['count'] > 0): ?>
|
||
<div class="activity-stats">
|
||
<span class="activity-stat">💰 共 <?= (int)$expenseSummary['count'] ?> 笔</span>
|
||
<span class="activity-stat">总计 ¥<?= number_format((float)$expenseSummary['total'], 2) ?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php
|
||
/* ── 路线地图(批 10 引入,批 11 调整位置与标记逻辑)──────────────
|
||
位置放在封面之下、时间轴之前:行程一长,拉到底才看到地图的体验很差。
|
||
按时间顺序把执行节点的起终点连成路线:节点内实线、节点之间虚线;
|
||
圆形标记放在**这段行程线的中点**(缺一头就放有的一头)——
|
||
有图片附件显示图片,没有显示节点类型 emoji,左上角带序号,
|
||
点一下打开该节点详情。
|
||
只有地名、没坐标的地点(偏僻地方下拉选不到点位):浏览器端调
|
||
高德地理编码(AMap.Geocoder)按地名查坐标,查到就补进路线重画;
|
||
查不到就保持缺省,不硬造位置。AMap 2.0 已由 layout.php 全局加载。 */
|
||
$routeNodes = [];
|
||
$nodeFirstImage = $nodeFirstImage ?? [];
|
||
$modeIcons = $modeIcons ?? []; // 批 19:出行方式→图标映射,由控制器注入
|
||
foreach (($realityNodes ?? []) as $rn) {
|
||
$s = json_decode_field($rn['start_location'] ?? null);
|
||
$e = json_decode_field($rn['end_location'] ?? null);
|
||
$hasS = !empty($s['lng']) && !empty($s['lat']);
|
||
$hasE = !empty($e['lng']) && !empty($e['lat']);
|
||
$sname = trim((string)($s['name'] ?? ''));
|
||
$ename = trim((string)($e['name'] ?? ''));
|
||
if (!$hasS && !$hasE && $sname === '' && $ename === '') {
|
||
continue; // 坐标和地名一个都没有的节点不上地图
|
||
}
|
||
$imgId = (int)($nodeFirstImage[(int)$rn['id']] ?? 0);
|
||
// 批 19:标记图标优先用「出行方式」的图标;没选出行方式时回退到节点类型图标
|
||
//(赶路节点没选方式 → 仍是 🚗;选了飞机/火车/游船等 → 显示对应交通工具图标,不再一律汽车)。
|
||
$rnModeType = (string)($rn['travel_mode_type'] ?? '');
|
||
$rnIcon = ($rnModeType !== '' && isset($modeIcons[$rnModeType]))
|
||
? $modeIcons[$rnModeType]
|
||
: (string)($rn['node_type_icon'] ?: '📍');
|
||
|
||
$routeNodes[] = [
|
||
'id' => (int)$rn['id'],
|
||
'title' => (string)($rn['title'] ?: '(无标题节点)'),
|
||
'icon' => $rnIcon,
|
||
'img' => $imgId > 0 ? media_att($imgId) : '',
|
||
'url' => url('nodes/show', ['id' => (int)$rn['id']]),
|
||
'sname' => $sname, // 出发地名:没有坐标时交给高德地理编码兜底
|
||
'ename' => $ename, // 到达地名:同上
|
||
'saddr' => trim((string)($s['address'] ?? '')), // 详细地址:地理编码优先用它
|
||
'eaddr' => trim((string)($e['address'] ?? '')),
|
||
'start' => $hasS ? ['lng' => (float)$s['lng'], 'lat' => (float)$s['lat']] : null,
|
||
'end' => $hasE ? ['lng' => (float)$e['lng'], 'lat' => (float)$e['lat']] : null,
|
||
];
|
||
}
|
||
?>
|
||
<?php if ($routeNodes !== []): ?>
|
||
<div class="route-tabs" role="tablist" aria-label="行程地图与节点详情切换">
|
||
<button type="button" class="route-tab is-active" data-panel="route-panel-map"
|
||
aria-selected="true">🗺️ 行程地图</button>
|
||
<button type="button" class="route-tab" data-panel="route-panel-nodes"
|
||
aria-selected="false">📋 计划与实际</button>
|
||
</div>
|
||
<div class="route-panel" id="route-panel-map">
|
||
<section class="route-map-section">
|
||
<div class="route-map" id="route-map-canvas"
|
||
data-route="<?= e(json_encode($routeNodes, JSON_UNESCAPED_UNICODE)) ?>"></div>
|
||
<p class="muted route-map-hint">
|
||
按时间顺序连出的执行节点路线(节点间虚线衔接),圆标在每段行程的中间,点它可打开对应节点。
|
||
<span class="route-legend-end">红点</span> = 到达地。只有地名没坐标的地点会自动向高德查询定位,太偏僻的地名可能查不到。
|
||
</p>
|
||
</section>
|
||
</div>
|
||
<div class="route-panel" id="route-panel-nodes" hidden>
|
||
<?php endif; ?>
|
||
|
||
<div class="timeline-grid">
|
||
<section class="timeline-col">
|
||
<h2>📋 计划 (<?= count($planNodes) ?>)</h2>
|
||
<?php if (empty($planNodes)): ?>
|
||
<p class="muted">暂无计划节点</p>
|
||
<?php else: ?>
|
||
<?php foreach ($planNodes as $n): ?>
|
||
<?php $cardCategory = 'plan'; require __DIR__ . '/_node_card.php'; ?>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<section class="timeline-col">
|
||
<h2>🎬 实际 (<?= count($realityNodes) ?>)</h2>
|
||
<?php if (empty($realityNodes)): ?>
|
||
<p class="muted">暂无执行节点</p>
|
||
<?php else: ?>
|
||
<?php foreach ($realityNodes as $n): ?>
|
||
<?php $cardCategory = 'reality'; require __DIR__ . '/_node_card.php'; ?>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</section>
|
||
</div>
|
||
|
||
|
||
<?php
|
||
$commentTargetType = \App\Comments::TARGET_ACTIVITY;
|
||
$commentTargetId = (int)$activity['id'];
|
||
require dirname(__DIR__) . '/comments/_block.php';
|
||
?>
|
||
|
||
<?php if ($routeNodes !== []): ?>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
/* ── 标签页切换:行程地图 / 计划与实际 ──────────────────────
|
||
地图标签默认打开(地图是主视图);切回来时重新 setFitView,
|
||
因为容器隐藏期间尺寸没有意义。 */
|
||
var tabs = Array.prototype.slice.call(document.querySelectorAll('.route-tab'));
|
||
var panels = {
|
||
'route-panel-map': document.getElementById('route-panel-map'),
|
||
'route-panel-nodes': document.getElementById('route-panel-nodes'),
|
||
};
|
||
var map = null; // 下面初始化后挂进来,供切换标签时重新适配视野
|
||
tabs.forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
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 (id) {
|
||
if (panels[id]) panels[id].hidden = (id !== btn.getAttribute('data-panel'));
|
||
});
|
||
if (btn.getAttribute('data-panel') === 'route-panel-map' && map) {
|
||
map.setFitView(null, false, [50, 50, 50, 50]);
|
||
}
|
||
});
|
||
});
|
||
|
||
/* ── 路线地图(AMap 2.0,layout.php 已全局加载)────────────── */
|
||
var el = document.getElementById('route-map-canvas');
|
||
if (!el) return;
|
||
|
||
/* AMap 脚本加载失败(罕见)时给一句人话,不白屏 */
|
||
if (typeof AMap === 'undefined' || window.__AMAP_ERROR) {
|
||
el.innerHTML = '<p class="route-map-fallback">地图脚本没有加载成功,暂时无法显示路线。</p>';
|
||
return;
|
||
}
|
||
var route;
|
||
try {
|
||
route = JSON.parse(el.getAttribute('data-route') || '[]');
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
if (!route.length) return;
|
||
|
||
/* 批 21:地图不再铺满整个视口 —— 用户反馈「上面正文固定宽、下面地图铺满屏」
|
||
看着奇怪,改回与正文版心同宽(批 18 的负 margin 出血逻辑 bleed() 已删)。 */
|
||
|
||
map = new AMap.Map(el, { viewMode: '2D', zoom: 5 });
|
||
/* 挂到 window 供自动化测试(真浏览器量标记位置)与线上排查用;页面卸载即释放 */
|
||
window.__routeMap = map;
|
||
var overlays = []; // 当前画上去的所有覆盖物;地理编码补点后整体重画
|
||
|
||
/* ── 防重合(批 15 改法,批 20 收紧)─────────────────────────
|
||
只处理「经纬度完全相同」的重合,且只挪图标、不挪线、不改数据:
|
||
① 往返(A→B 与 B→A 是同一段):两段线重合(本就该重合),但两枚图标会
|
||
叠在同一中点 → 沿这条线**前后排开**,看上去就是同一条路上跑了两趟。
|
||
② 坐标不同、只是屏幕上挨着的密集节点(缩小时尤甚):**不再让开**,
|
||
一律留在真实经纬度上。批 20 之前按屏幕像素距离判碰撞、把挨着的标记
|
||
各甩 56px(低 zoom 下等于数公里),导致缩小时图标明显偏离真实位置。
|
||
判定从「屏幕像素中心距」改为「posKey 经纬度字符串是否相同」:只有
|
||
完全相同的坐标才偏移,否则零偏移。位移方向取线段屏幕方向,太短则退法线、再水平。
|
||
错开只改「怎么画」,不改数据 —— inPath / prevEnd 仍用原始坐标。 */
|
||
var MARK_W = 46; // 碰撞判定:圆标直径 40 + 6px 间隙(按中心距判,见下面的排布器)
|
||
var STEP = MARK_W + 10; // 让开一步 = 56px(圆标净间隙 16px,两枚挨着也分得清)
|
||
|
||
function segKey(p1, p2) {
|
||
var a = p1[0].toFixed(5) + ',' + p1[1].toFixed(5);
|
||
var b = p2[0].toFixed(5) + ',' + p2[1].toFixed(5);
|
||
return a < b ? a + '|' + b : b + '|' + a; // A→B 与 B→A 同一键
|
||
}
|
||
function posKey(p) {
|
||
return p[0].toFixed(5) + ',' + p[1].toFixed(5);
|
||
}
|
||
function markerPos(n) {
|
||
var s = n.start, en = n.end;
|
||
if (s && en) return [(s.lng + en.lng) / 2, (s.lat + en.lat) / 2];
|
||
if (s) return [s.lng, s.lat];
|
||
if (en) return [en.lng, en.lat];
|
||
return null;
|
||
}
|
||
/** 一段线在屏幕上的方向:单位向量 + 像素长度。
|
||
A→B 与 B→A 用同一个键 → 往返两枚图标朝同一头排队,不会各往一边跑。 */
|
||
function makeSegDir() {
|
||
var cache = {};
|
||
return function (p1, p2) {
|
||
var key = segKey(p1, p2);
|
||
if (cache[key]) return cache[key];
|
||
var d = { x: 1, y: 0, len: 0 }; // 拿不到就退化成水平方向
|
||
try {
|
||
var a = map.lngLatToContainer(new AMap.LngLat(p1[0], p1[1]));
|
||
var b = map.lngLatToContainer(new AMap.LngLat(p2[0], p2[1]));
|
||
var vx = b.x - a.x, vy = b.y - a.y;
|
||
var len = Math.sqrt(vx * vx + vy * vy);
|
||
if (len > 1) d = { x: vx / len, y: vy / len, len: len };
|
||
} catch (e) {
|
||
/* 地图还没量出尺寸:用水平方向兜底 */
|
||
}
|
||
cache[key] = d;
|
||
return d;
|
||
};
|
||
}
|
||
/** 排布:只有「经纬度完全相同」的标记才偏移(批 20 收紧)。
|
||
坐标不同 → 一律零偏移,留在真实位置,不再按屏幕像素甩开。
|
||
同坐标重复(如往返中点)沿这段线前后排开,线太短走法线、再水平兜底。 */
|
||
function makePlacer() {
|
||
var seen = {}; // posKey(realCoord) -> 已在该坐标放置的标记数
|
||
return {
|
||
place: function (dir, pos) {
|
||
var key = posKey(pos);
|
||
var n = seen[key] || 0;
|
||
if (n === 0) { // 该坐标第一枚:原地放,不偏移
|
||
seen[key] = 1;
|
||
return { x: 0, y: 0 };
|
||
}
|
||
/* 同坐标重复:沿这段线让开(图标仍排在这条线上) */
|
||
var maxAlong = Math.max(0, dir.len * 0.45 - MARK_W / 2);
|
||
var cand = [];
|
||
var k, step, sg;
|
||
for (k = 1; k <= 4; k++) { // ① 沿线前后交替(首选)
|
||
step = Math.ceil(k / 2) * STEP;
|
||
sg = (k % 2 === 1) ? 1 : -1;
|
||
if (step <= maxAlong) cand.push({ x: dir.x * step * sg, y: dir.y * step * sg });
|
||
}
|
||
for (k = 1; k <= 4; k++) { // ② 线太短:改走法线
|
||
step = Math.ceil(k / 2) * STEP;
|
||
sg = (k % 2 === 1) ? 1 : -1;
|
||
cand.push({ x: -dir.y * step * sg, y: dir.x * step * sg });
|
||
}
|
||
for (k = 1; k <= 6; k++) { // ③ 兜底:水平让开
|
||
step = Math.ceil(k / 2) * STEP;
|
||
sg = (k % 2 === 1) ? 1 : -1;
|
||
cand.push({ x: step * sg, y: 0 });
|
||
}
|
||
/* 第 n 次重复取第 n 个候选(跳过 cand[0]=原地),顺着线铺开;
|
||
候选用尽就取最后一个,总比乱飞好 */
|
||
var pick = cand.length ? cand[Math.min(n, cand.length - 1)] : { x: 0, y: 0 };
|
||
seen[key] = n + 1;
|
||
return pick;
|
||
},
|
||
reset: function () { seen = {}; },
|
||
};
|
||
}
|
||
|
||
/* 全量重画:先清掉旧覆盖物再按当前 route 数据画。
|
||
地理编码是异步的,一个点查回来就重画一次,数据是幂等的。
|
||
fromZoom=true 表示这次是「用户缩放了」触发的重排:只重排标记,
|
||
不再 setFitView —— 否则会和缩放事件互相触发,还会把用户手动
|
||
缩放好的视野又拉回去。 */
|
||
function drawRoute(fromZoom) {
|
||
overlays.forEach(function (o) { map.remove(o); });
|
||
overlays = [];
|
||
var inPath = []; // 节点内:出发地 → 到达地(实线)
|
||
var hopPath = []; // 节点之间:上一站到达地 → 下一站出发地(虚线)
|
||
var prevEnd = null;
|
||
var segDir = makeSegDir(); // 线段在屏幕上的方向:图标沿它排队
|
||
var placer = makePlacer(); // 标记排布:仅同坐标才偏移,否则留真实位置(批 20)
|
||
var dotSeen = {}; // 到达地红点:同一点位只画一个
|
||
|
||
route.forEach(function (n, i) {
|
||
var s = n.start, en = n.end;
|
||
|
||
/* 线按**原始坐标**画:往返两段重合是正常的,用户要的是图标不叠,
|
||
不是把线劈成两条(批 14 那样左右错开,偏移量太大看着像画错)。 */
|
||
if (s && en) {
|
||
inPath.push([s.lng, s.lat], [en.lng, en.lat]);
|
||
}
|
||
if (s && prevEnd) {
|
||
hopPath.push([prevEnd.lng, prevEnd.lat], [s.lng, s.lat]);
|
||
}
|
||
|
||
/* 圆形标记放这段行程线的中点(起终点都有才取中点),缺一头就放有的一头。
|
||
⚠️ position 必须是 [lng, lat] 数组 —— 把 {lng,lat} 对象直接传给
|
||
AMap 会抛 Invalid Object,forEach 当场中断、整张地图一个覆盖物
|
||
都不剩(批 11 上线后踩的坑,光秃秃地图就是它)。 */
|
||
var pos = markerPos(n);
|
||
if (pos) {
|
||
/* 只把「经纬度完全相同」的标记(如往返中点)沿各自线段错开;
|
||
坐标不同则一律留在真实位置,不再按屏幕像素甩开 —— 否则节点
|
||
密集、缩小时多枚标记会被甩出 56px(低 zoom 下=数公里),
|
||
图标明显偏离真实坐标(批 20)。 */
|
||
var dir = (s && en)
|
||
? segDir([s.lng, s.lat], [en.lng, en.lat])
|
||
: { x: 1, y: 0, len: 0 };
|
||
var off = placer.place(dir, pos);
|
||
|
||
var html ='<div class="route-marker" title="' + n.title.replace(/"/g, '"') + '">'
|
||
+ (n.img
|
||
? '<img src="' + n.img + '" alt="">'
|
||
: '<span class="route-marker-emoji">' + n.icon + '</span>')
|
||
+ '<span class="route-marker-num">' + (i + 1) + '</span>'
|
||
+ '</div>';
|
||
var opts = {
|
||
position: pos,
|
||
content: html,
|
||
anchor: 'center',
|
||
map: map,
|
||
};
|
||
/* 同坐标的标记才沿这条线挪开 —— 用像素偏移,谁也不挡谁 */
|
||
if (off.x !== 0 || off.y !== 0) {
|
||
opts.offset = new AMap.Pixel(off.x, off.y);
|
||
}
|
||
var marker = new AMap.Marker(opts);
|
||
marker.on('click', function () { window.location.href = n.url; });
|
||
overlays.push(marker);
|
||
}
|
||
|
||
/* 到达地小圆点(和出发地不是同一点时才画)。
|
||
同一点位只画一个:往返行程两端的红点本来就该是同一个。 */
|
||
if (en && (!s || en.lng !== s.lng || en.lat !== s.lat)) {
|
||
var dk = posKey([en.lng, en.lat]);
|
||
if (!dotSeen[dk]) {
|
||
dotSeen[dk] = 1;
|
||
var dot = new AMap.Marker({
|
||
position: [en.lng, en.lat],
|
||
content: '<div class="route-end-dot"></div>',
|
||
anchor: 'center',
|
||
map: map,
|
||
});
|
||
dot.on('click', function () { window.location.href = n.url; });
|
||
overlays.push(dot);
|
||
}
|
||
}
|
||
|
||
prevEnd = en || s;
|
||
});
|
||
|
||
if (inPath.length > 1) {
|
||
overlays.push(new AMap.Polyline({
|
||
path: inPath, strokeColor: '#3B82F6', strokeWeight: 5,
|
||
strokeOpacity: 0.9, lineJoin: 'round', showDir: true, map: map,
|
||
}));
|
||
}
|
||
if (hopPath.length) {
|
||
overlays.push(new AMap.Polyline({
|
||
path: hopPath, strokeColor: '#94A3B8', strokeWeight: 3,
|
||
strokeStyle: 'dashed', strokeOpacity: 0.8, map: map,
|
||
}));
|
||
}
|
||
if (overlays.length && !fromZoom) map.setFitView(null, false, [50, 50, 50, 50]);
|
||
}
|
||
drawRoute();
|
||
/* 缩放会改变标记之间的屏幕距离:缩小时原本分开的标记可能又叠在一起,
|
||
所以缩放结束后按新的像素位置重排一次(不重新适配视野)。 */
|
||
map.on('zoomend', function () { drawRoute(true); });
|
||
|
||
/* 改窗口大小 / 旋转屏幕 / 换分辨率:地图容器跟着版心变,容器一变
|
||
就要按新尺寸重新适配视野并重排标记。防抖 200ms。 */
|
||
var resizeTimer = null;
|
||
window.addEventListener('resize', function () {
|
||
clearTimeout(resizeTimer);
|
||
resizeTimer = setTimeout(function () {
|
||
if (!map) return;
|
||
map.resize();
|
||
drawRoute(); // 重新适配视野 + 按新像素重排标记
|
||
}, 200);
|
||
});
|
||
|
||
/* 只有地名、没有坐标的地点:向高德地理编码查坐标(插件按需加载)。
|
||
查到就补进数据并重画;查不到(地名太偏/写错)保持缺省,不硬造位置。 */
|
||
function geocode(name, cb) {
|
||
if (!name) return;
|
||
AMap.plugin('AMap.Geocoder', function () {
|
||
try {
|
||
new AMap.Geocoder({}).getLocation(name, function (status, result) {
|
||
if (status === 'complete' && result && result.geocodes && result.geocodes.length
|
||
&& result.geocodes[0].location) {
|
||
var loc = result.geocodes[0].location;
|
||
cb({ lng: Number(loc.getLng()), lat: Number(loc.getLat()) });
|
||
}
|
||
});
|
||
} catch (e) { /* 地理编码服务不可用时保持原状 */ }
|
||
});
|
||
}
|
||
/* 没有坐标的点:优先拿**详细地址**去查(比简化地名准得多),没记地址才用地名 */
|
||
route.forEach(function (n) {
|
||
if (!n.start) {
|
||
var qs = n.saddr || n.sname;
|
||
if (qs) geocode(qs, function (p) { n.start = p; drawRoute(); });
|
||
}
|
||
if (!n.end) {
|
||
var qe = n.eaddr || n.ename;
|
||
if (qe) geocode(qe, function (p) { n.end = p; drawRoute(); });
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
<?php endif; ?>
|