高德地图添加轨迹规划和重放功能

This commit is contained in:
2026-06-25 10:30:45 +08:00
parent 0089e5c107
commit 91afc9d81c
7 changed files with 2740 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
# 前端面试题:地图路线规划 + 轨迹动画系统
## 题目描述
在 Vue 3 + 高德地图的养老/医疗微前端应用中,实现一个**护理员轨迹系统**
```
操作流程:
[📍标记起点] → 点击地图放 📍
[🏁标记终点] → 点击地图放 🏁
[➕添加护理员] → 在起点创建 👩‍⚕️
[🚗规划路线] → AMap.Driving 计算驾车路线 → 蓝色折线
[▶出发] → 护理员沿路线动画移动
不管路径 500km 还是 2000km动画始终 ~10 秒丝滑播完
```
**核心要求:**
1. 起点和终点通过**点击地图手动标记**(独立 `AMap.Marker`
2. 驾车路线使用 `AMap.Driving` 规划(真实道路路径,非直线)
3. 护理员标记沿路线**动画移动**,无论路径多长都流畅无卡顿
4. 支持暂停、回到起点、全部清除
5. 护理员不存在时点击"出发"要**自动创建**(容错 UX
---
## 架构设计
```
useRouteTrack(mapInstance)
├─ enterSetMode('origin'|'dest') ← 点击地图放标记
├─ addCaregiver() ← 在起点创建护理员
├─ planRoute() ← AMap.Driving 路线规划
│ └─ downsamplePath(full, 250) ← ★ 路径下采样
├─ startAnimation() ← setInterval 帧动画
└─ clearAll() ← 完整清理
```
---
## 核心技术点
### 1. 路径下采样 — 丝滑动画的秘密
```
AMap.Driving 返回原始路径5000+ 个点(每几米一个)
↓ downsamplePath(fullPath, 250)
动画路径250 个点(均匀间隔)
↓ setInterval(40ms) → 25fps
总时长250 × 40ms = 10 秒(恒定)
```
**为什么需要下采样?**
| 不用下采样 | 用下采样 |
|------------|----------|
| 5000 点 × 40ms = **200 秒**(太慢) | 250 点 × 40ms = **10 秒**(刚好) |
| 短距离只有 200 点 = 8 秒(不一致) | 始终 250 点 = **恒定 10 秒** |
| 每个点间隔 ~1m动画抖动 | 均匀间隔,**视觉丝滑** |
**下采样算法:**
```ts
function downsamplePath(path: [number, number][], target: number): [number, number][] {
if (path.length <= target) return [...path]
const result: [number, number][] = []
const step = (path.length - 1) / (target - 1) // 5000/249 ≈ 20.08
for (let i = 0; i < target; i++) {
result.push(path[Math.min(Math.round(i * step), path.length - 1)])
}
return result
}
```
**为什么选 250**
- 250 × 40ms = 10 秒,不短不长,刚好展示完整轨迹
- 25fps 是人眼感知流畅的最低帧率,再低会卡顿
- 250 个 `setPosition` 调用对 DOM 压力极小
### 2. 帧动画引擎
```ts
async function startAnimation(): Promise<void> {
// 🔧 容错:护理员不存在 → 自动在起点创建
if (!caregiverMarker.value) {
if (!originPoint.value) { alert('请先标记起点'); return }
await addCaregiver()
}
const total = animPath.value.length - 1 // 249 步
animTimer = setInterval(() => {
if (animStep.value >= total) {
stopAnimation() // 到达终点自动停
animProgress.value = 1
return
}
animStep.value++
animProgress.value = animStep.value / total // 0→1 驱动进度条
caregiverMarker.value?.setPosition(animPath.value[animStep.value])
}, 40) // 25fps
}
```
| 参数 | 值 | 说明 |
|------|-----|------|
| 帧间隔 | 40ms | 25fps人眼流畅感知阈值 |
| 总步数 | 249 | 250 个路径点,首点即起始位置 |
| 总时长 | ~10s | 恒定,不受实际距离影响 |
| 进度计算 | `step/total` | 0→1驱动 CSS 进度条宽度 |
### 3. 三种 status 的完整错误处理
```ts
driving.search(originLngLat, destLngLat, (status, result) => {
if (status === 'complete' && result.routes?.length > 0) {
// ✅ 成功:提取路径 → 下采样 → 绘制折线
} else if (status === 'error') {
// ❌ 参数/权限问题INVALID_USER_SCODE缺安全密钥
reject(new Error(`路线规划失败: ${result.info || result.message}`))
} else if (status === 'no_data') {
// ⚠️ 起终点之间确实无路(如海岛 → 内陆无桥梁)
reject(new Error('起终点之间无可用路线'))
} else {
// ⁉️ 未知状态,不应发生
reject(new Error(`路线规划失败 (${status})`))
}
})
```
### 4. 点击地图放标记 — 模式切换设计
```
enterSetMode('origin' | 'dest')
→ exitSetMode() // 先退出当前模式(互斥)
→ settingMode = mode // origin 或 dest
→ map.setDefaultCursor('crosshair')
→ map.on('click', handler)
→ 点击地图:
→ 获取经纬度
→ mode === 'origin' ? applyOrigin() : applyDestination()
→ exitSetMode() // 单次点击后自动退出
```
**三个关键设计决策:**
| 决策 | 理由 |
|------|------|
| 单次点击后自动退出 | 避免用户误放多个标记 |
| origin 和 dest 互斥 | 同一时间只能设一种点,防止状态混乱 |
| 退出时恢复光标 + 解绑事件 | `exitSetMode()` 统一清理,防止事件泄漏 |
---
## 考察点汇总
| 层级 | 考察点 | 难度 |
|------|--------|------|
| **算法** | 路径下采样(均匀间隔重采样) | ⭐⭐⭐ |
| **动画** | setInterval 帧动画 + 进度归一化 | ⭐⭐ |
| **API 集成** | AMap.Driving 三种 status 的完整处理 | ⭐⭐⭐ |
| **交互设计** | 模式切换 + 点击地图放标记 | ⭐⭐ |
| **容错设计** | 护理员缺失时自动创建 | ⭐ |
| **资源管理** | setInterval 清理 + 事件解绑 | ⭐⭐ |
| **调试** | INVALID_USER_SCODE 问题排查 | ⭐⭐⭐⭐ |
## 加分项
- **能解释为什么 250 个点** — 250×40ms=10s 恒定,与距离无关
- **能提出变速方案** — 起步加速/到站减速easeInOutCubic用动态 interval 替代固定 40ms
- **能指出 `new AMap.LngLat(lng, lat)` 的必要性** — 数组 `[lng, lat]` 在某些版本不被 Driving 接受
- **能说明 `map: undefined` 会导致 Driving 失败** — Driving 依赖 map 做投影计算
- **能分析不同距离下的优化策略** — 短距离提高 target 点数(更细腻),长距离降低(防卡顿)
## 追问方向
### 追问 1如果路线有 10 万个点250 的下采样会丢失多少细节?
**期望回答:** 10 万 → 250每 400 个点取 1 个城市街道级别的拐弯会被抹平。但视觉上因为标记移动只显示一个点250 个关键帧足以模拟沿途运动。如果需要更高精度,可以动态调整 target短距离<50km用 500 点,长距离(>500km用 150 点。
### 追问 2为什么用 `setInterval` 而不用 `requestAnimationFrame`
**期望回答:**
- `rAF` 60fps → 250 点只需 4 秒(太快)
- `rAF` 帧率不稳定(后台标签页降到 1fps→ 动画时间不可预测
- `setInterval(40)` 稳定 25fps后台自动暂停浏览器节流回来时从当前位置继续
- 需要配合进度条时需要可知的总时长,`setInterval` 更可控
### 追问 3`startAnimation` 中为什么要 `await addCaregiver()`
**期望回答:** `addCaregiver()` 内部调用 `loadAMap()` 是异步的。如果护理员不存在,需要等标记创建完成再开始动画。`await` 确保 `caregiverMarker.value` 已被赋值,后续 `setInterval` 中才能正常调用 `setPosition`
### 追问 4如何让动画支持「可调速」
**期望回答:** 将 40ms 提取为变量 `speed`,暴露给外部。速度加倍 = interval 减半:
```ts
const speedMap = { '1x': 40, '2x': 20, '4x': 10 }
// 或动态计算,进度条相应调整
```