uni-app 地图高级功能
核心知识点
1. 地图基础
- 地图组件:使用
<map>组件 - 地图配置:中心坐标、缩放级别、地图类型等
- 地图初始化:设置初始参数和监听地图加载
- 地图API:使用 uni.createMapContext() 获取地图上下文
2. 地图标记
- 标记类型:普通标记、自定义标记、聚合标记
- 标记属性:位置、图标、标题、内容等
- 标记事件:点击、拖动、显示、隐藏等
- 标记动画:添加、删除、移动等动画效果
- 标记管理:批量添加、删除、更新标记
3. 路线规划
- 路线类型:驾车、步行、骑行、公交等
- 路线参数:起点、终点、途经点等
- 路线计算:调用地图服务计算路线
- 路线绘制:在地图上绘制路线
- 路线信息:距离、时间、步骤等
4. 地理编码
- 正向编码:地址转坐标
- 反向编码:坐标转地址
- 地理编码服务:使用第三方地图服务
- 编码缓存:缓存编码结果,减少请求
5. 地图事件
- 地图交互事件:点击、拖动、缩放、旋转等
- 标记事件:点击标记、拖动标记等
- 覆盖物事件:点击覆盖物、修改覆盖物等
- 定位事件:位置更新、定位失败等
6. 地图覆盖物
- 覆盖物类型:多边形、圆形、折线、矩形等
- 覆盖物属性:位置、样式、层级等
- 覆盖物操作:添加、删除、更新覆盖物
- 覆盖物交互:点击、拖动覆盖物等
7. 地图定位
- 定位API:使用 uni.getLocation() 获取位置
- 定位精度:设置定位精度和超时时间
- 定位权限:需要获取位置权限
- 持续定位:实现位置实时更新
8. 跨平台地图处理
- 平台差异:不同平台对地图的支持
- 条件编译:为不同平台提供不同的地图实现
- 第三方地图:集成高德、百度、腾讯等地图
- 地图服务:使用地图SDK提供的服务
实用案例
案例 1:导航应用
需求分析
- 支持起点到终点的路线规划
- 支持驾车、步行、骑行等多种出行方式
- 显示路线详情和预计时间
- 支持实时导航和路线偏离提醒
- 支持地图缩放和旋转
实现方案
地图初始化
<template> <view class="map-container"> <map id="navi-map" :style="{ width: '100%', height: '100%' }" :latitude="centerLat" :longitude="centerLng" :scale="16" :markers="markers" :polyline="polyline" @load="mapLoad" @click="mapClick" class="navi-map" ></map> <view class="controls"> <button @click="getLocation" class="control-btn">定位</button> <button @click="calculateRoute" class="control-btn">规划路线</button> <button @click="toggleMode" class="control-btn">{{ travelMode === 'driving' ? '驾车' : travelMode === 'walking' ? '步行' : '骑行' }}</button> </view> </view> </template> <script> export default { data() { return { mapContext: null, centerLat: 39.9042, centerLng: 116.4074, markers: [], polyline: [], startPoint: { lat: 39.9042, lng: 116.4074 }, endPoint: { lat: 39.9142, lng: 116.4174 }, travelMode: 'driving', routeInfo: {} } }, onLoad() { this.initMap() }, methods: { initMap() { this.mapContext = uni.createMapContext('navi-map', this) }, mapLoad() { console.log('地图加载完成') this.addMarkers() }, getLocation() { uni.getLocation({ type: 'gcj02', altitude: true, success: (res) => { this.centerLat = res.latitude this.centerLng = res.longitude this.startPoint = { lat: res.latitude, lng: res.longitude } this.mapContext.moveToLocation({ latitude: res.latitude, longitude: res.longitude }) this.addMarkers() }, fail: (err) => { console.error('获取位置失败', err) } }) }, addMarkers() { this.markers = [ { id: 1, latitude: this.startPoint.lat, longitude: this.startPoint.lng, title: '起点', iconPath: '/static/marker-start.png', width: 30, height: 30 }, { id: 2, latitude: this.endPoint.lat, longitude: this.endPoint.lng, title: '终点', iconPath: '/static/marker-end.png', width: 30, height: 30 } ] }, calculateRoute() { // 这里调用地图服务计算路线 // 实际项目中需要使用第三方地图API this.simulateRoute() }, simulateRoute() { // 模拟路线数据 const routePoints = [ { latitude: this.startPoint.lat, longitude: this.startPoint.lng }, { latitude: 39.9082, longitude: 116.4094 }, { latitude: 39.9102, longitude: 116.4114 }, { latitude: 39.9122, longitude: 116.4134 }, { latitude: this.endPoint.lat, longitude: this.endPoint.lng } ] this.polyline = [{ points: routePoints, color: '#007AFF', width: 5, dottedLine: false }] this.routeInfo = { distance: '2.5公里', duration: '15分钟', steps: [ '从起点出发,向东行驶', '左转进入建国路', '直行1.5公里', '右转进入朝阳路', '到达终点' ] } uni.showModal({ title: '路线规划', content: `距离:${this.routeInfo.distance}\n时间:${this.routeInfo.duration}`, showCancel: false }) }, toggleMode() { const modes = ['driving', 'walking', 'riding'] const currentIndex = modes.indexOf(this.travelMode) this.travelMode = modes[(currentIndex + 1) % modes.length] }, mapClick(e) { console.log('点击地图', e) } } } </script>实时导航
- 监听位置更新,实时调整路线
- 计算与路线的偏差,提供偏离提醒
- 显示当前位置和目的地的实时距离
案例 2:附近商家应用
需求分析
- 显示当前位置附近的商家
- 支持按分类筛选商家
- 显示商家详情和距离
- 支持商家导航
- 支持地图和列表切换查看
实现方案
附近商家查询
<template> <view class="nearby-container"> <view class="header"> <text class="title">附近商家</text> <view class="filter"> <button v-for="(category, index) in categories" :key="index" @click="selectCategory(category)" :class="{ active: selectedCategory === category }" class="filter-btn" > {{ category }} </button> </view> </view> <view class="content"> <map id="nearby-map" :style="{ width: '100%', height: '500rpx' }" :latitude="centerLat" :longitude="centerLng" :scale="16" :markers="markers" @markertap="markerTap" class="nearby-map" ></map> <view class="list-section"> <view v-for="(shop, index) in shops" :key="index" @click="showShopDetail(shop)" class="shop-item" > <image :src="shop.image" mode="aspectFill" class="shop-image"></image> <view class="shop-info"> <text class="shop-name">{{ shop.name }}</text> <text class="shop-address">{{ shop.address }}</text> <view class="shop-meta"> <text class="shop-distance">{{ shop.distance }}米</text> <text class="shop-rating">{{ shop.rating }}分</text> </view> </view> </view> </view> </view> </view> </template> <script> export default { data() { return { centerLat: 39.9042, centerLng: 116.4074, categories: ['全部', '餐饮', '购物', '娱乐', '酒店'], selectedCategory: '全部', shops: [], markers: [] } }, onLoad() { this.getLocation() }, methods: { getLocation() { uni.getLocation({ type: 'gcj02', success: (res) => { this.centerLat = res.latitude this.centerLng = res.longitude this.searchNearbyShops() }, fail: (err) => { console.error('获取位置失败', err) this.searchNearbyShops() } }) }, searchNearbyShops() { // 模拟附近商家数据 this.shops = [ { id: 1, name: '麦当劳', address: '北京市朝阳区建国路88号', distance: 200, rating: 4.5, image: 'https://example.com/mcdonalds.jpg', latitude: 39.9052, longitude: 116.4084, category: '餐饮' }, { id: 2, name: '星巴克', address: '北京市朝阳区建国路89号', distance: 300, rating: 4.7, image: 'https://example.com/starbucks.jpg', latitude: 39.9062, longitude: 116.4094, category: '餐饮' }, { id: 3, name: '国贸商城', address: '北京市朝阳区建国门外大街1号', distance: 500, rating: 4.8, image: 'https://example.com/icc.jpg', latitude: 39.9072, longitude: 116.4104, category: '购物' }, { id: 4, name: '万达影城', address: '北京市朝阳区建国路93号', distance: 800, rating: 4.6, image: 'https://example.com/wanda.jpg', latitude: 39.9082, longitude: 116.4114, category: '娱乐' } ] this.updateMarkers() }, updateMarkers() { this.markers = this.shops.map(shop => ({ id: shop.id, latitude: shop.latitude, longitude: shop.longitude, title: shop.name, snippet: `${shop.distance}米 | ${shop.rating}分`, iconPath: '/static/marker-shop.png', width: 30, height: 30 })) }, selectCategory(category) { this.selectedCategory = category // 根据分类筛选商家 if (category === '全部') { this.searchNearbyShops() } else { const filteredShops = this.shops.filter(shop => shop.category === category) this.shops = filteredShops this.updateMarkers() } }, markerTap(e) { const markerId = e.markerId const shop = this.shops.find(shop => shop.id === markerId) if (shop) { this.showShopDetail(shop) } }, showShopDetail(shop) { uni.showModal({ title: shop.name, content: `地址:${shop.address}\n距离:${shop.distance}米\n评分:${shop.rating}分`, confirmText: '导航', cancelText: '取消', success: (res) => { if (res.confirm) { this.navigateToShop(shop) } } }) }, navigateToShop(shop) { // 跳转到导航页面 uni.navigateTo({ url: `/pages/navi/navi?startLat=${this.centerLat}&startLng=${this.centerLng}&endLat=${shop.latitude}&endLng=${shop.longitude}&endName=${encodeURIComponent(shop.name)}` }) } } </script>商家详情和导航
- 点击商家标记或列表项,显示商家详情
- 提供导航功能,跳转到导航页面
- 显示商家的详细信息和用户评价
案例 3:地图轨迹应用
需求分析
- 记录用户的运动轨迹
- 在地图上显示轨迹路线
- 计算轨迹的距离和时间
- 支持轨迹回放和分享
实现方案
轨迹记录
<template> <view class="track-container"> <map id="track-map" :style="{ width: '100%', height: '500rpx' }" :latitude="centerLat" :longitude="centerLng" :scale="16" :polyline="polyline" :markers="markers" class="track-map" ></map> <view class="controls"> <button @click="toggleRecord" :type="isRecording ? 'warn' : 'primary'" class="record-btn" > {{ isRecording ? '停止记录' : '开始记录' }} </button> <button @click="saveTrack" type="primary" class="save-btn" v-if="!isRecording && trackPoints.length > 0"> 保存轨迹 </button> </view> <view class="track-info" v-if="trackInfo"> <text class="info-item">距离:{{ trackInfo.distance }}米</text> <text class="info-item">时间:{{ trackInfo.duration }}分钟</text> <text class="info-item">速度:{{ trackInfo.speed }}km/h</text> </view> </view> </template> <script> export default { data() { return { mapContext: null, centerLat: 39.9042, centerLng: 116.4074, isRecording: false, trackPoints: [], polyline: [], markers: [], startTime: 0, endTime: 0, trackInfo: null, locationTimer: null } }, onLoad() { this.initMap() }, onUnload() { if (this.locationTimer) { clearInterval(this.locationTimer) } }, methods: { initMap() { this.mapContext = uni.createMapContext('track-map', this) }, toggleRecord() { if (this.isRecording) { this.stopRecord() } else { this.startRecord() } }, startRecord() { this.isRecording = true this.trackPoints = [] this.startTime = Date.now() this.trackInfo = null // 开始定位 this.locationTimer = setInterval(() => { this.getLocation() }, 5000) // 每5秒获取一次位置 this.getLocation() }, getLocation() { uni.getLocation({ type: 'gcj02', altitude: true, success: (res) => { const point = { latitude: res.latitude, longitude: res.longitude, timestamp: Date.now() } this.trackPoints.push(point) this.centerLat = res.latitude this.centerLng = res.longitude this.updateMap() }, fail: (err) => { console.error('获取位置失败', err) } }) }, updateMap() { if (this.trackPoints.length > 0) { // 更新轨迹线 this.polyline = [{ points: this.trackPoints, color: '#FF0000', width: 5, dottedLine: false }] // 更新标记 this.markers = [ { id: 1, latitude: this.trackPoints[0].latitude, longitude: this.trackPoints[0].longitude, title: '起点', iconPath: '/static/marker-start.png', width: 30, height: 30 }, { id: 2, latitude: this.trackPoints[this.trackPoints.length - 1].latitude, longitude: this.trackPoints[this.trackPoints.length - 1].longitude, title: '终点', iconPath: '/static/marker-end.png', width: 30, height: 30 } ] } }, stopRecord() { this.isRecording = false this.endTime = Date.now() if (this.locationTimer) { clearInterval(this.locationTimer) this.locationTimer = null } // 计算轨迹信息 this.calculateTrackInfo() }, calculateTrackInfo() { if (this.trackPoints.length < 2) return // 计算距离(简化计算,实际应使用更精确的算法) let distance = 0 for (let i = 1; i < this.trackPoints.length; i++) { const p1 = this.trackPoints[i - 1] const p2 = this.trackPoints[i] // 使用Haversine公式计算两点之间的距离 const R = 6371e3 // 地球半径(米) const φ1 = p1.latitude * Math.PI / 180 const φ2 = p2.latitude * Math.PI / 180 const Δφ = (p2.latitude - p1.latitude) * Math.PI / 180 const Δλ = (p2.longitude - p1.longitude) * Math.PI / 180 const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) distance += R * c } const duration = Math.round((this.endTime - this.startTime) / 60000) // 分钟 const speed = distance > 0 && duration > 0 ? (distance / 1000 / (duration / 60)).toFixed(1) : 0 this.trackInfo = { distance: Math.round(distance), duration: duration, speed: speed } }, saveTrack() { if (this.trackPoints.length < 2) { uni.showToast({ title: '轨迹太短,无法保存', icon: 'none' }) return } const track = { id: Date.now(), points: this.trackPoints, distance: this.trackInfo.distance, duration: this.trackInfo.duration, speed: this.trackInfo.speed, startTime: this.startTime, endTime: this.endTime } // 保存轨迹到本地存储 let tracks = uni.getStorageSync('tracks') || [] tracks.push(track) uni.setStorageSync('tracks', tracks) uni.showToast({ title: '轨迹保存成功', icon: 'success' }) } } } </script>轨迹回放
- 加载保存的轨迹数据
- 在地图上动态显示轨迹
- 控制回放速度和进度
学习目标
- 掌握 uni-app 中地图的高级功能和 API
- 学会实现地图标记、路线规划、地理编码等功能
- 了解地图事件和覆盖物的使用方法
- 能够开发复杂的地图应用,如导航、附近商家、轨迹记录等
- 掌握跨平台地图处理的技巧
- 提升地图应用的用户体验
总结
地图高级功能是 uni-app 应用开发中的重要组成部分,通过合理使用地图标记、路线规划、地理编码等功能,可以开发出功能丰富的地图应用。在实际开发中,开发者应该根据具体场景选择合适的地图功能,结合第三方地图服务,提供最佳的地图体验。
同时,开发者还需要注意跨平台地图处理的差异,使用条件编译或平台特定的 API 来处理不同平台的地图问题,确保应用在所有平台上都能正常运行地图功能。对于需要高精度地图服务的应用,还可以考虑集成第三方地图 SDK,提升地图功能的性能和准确性。