uni-app 定位服务
核心知识点
1. 定位基础
- 定位原理:GPS、基站、WiFi、蓝牙等定位方式
- 坐标系统:WGS84、GCJ02、BD09等
- 定位精度:不同定位方式的精度差异
- 定位耗时:不同定位方式的响应时间
2. 定位API
- 获取位置:uni.getLocation()
- 监听位置:uni.watchLocation()
- 关闭监听:clearWatch()
- 位置设置:uni.openLocation()
- 地图选择:uni.chooseLocation()
3. 定位参数
- type:定位类型(wgs84、gcj02等)
- altitude:是否获取高度信息
- accuracy:定位精度(高精度、中等精度、低精度)
- timeout:定位超时时间
- maximumAge:位置缓存时间
4. 位置权限
- 权限类型:后台定位、前台定位
- 权限申请:使用 uni.authorize() 申请权限
- 权限检查:使用 uni.getSetting() 检查权限
- 权限引导:引导用户开启权限
- 平台差异:不同平台的权限处理
5. 位置监听
- 实时定位:使用 watchLocation 实现实时位置更新
- 监听参数:设置监听频率和精度
- 监听回调:处理位置更新事件
- 监听管理:及时关闭不需要的监听
6. 位置计算
- 距离计算:两点之间的距离
- 方向计算:两点之间的方位角
- 面积计算:多边形区域面积
- 路径计算:多点之间的路径距离
7. 定位优化
- 功耗优化:减少定位频率和精度
- 精度优化:根据场景选择合适的定位方式
- 缓存策略:合理使用位置缓存
- 错误处理:处理定位失败的情况
8. 跨平台定位处理
- 平台差异:不同平台的定位API和权限要求
- 条件编译:为不同平台提供不同的定位方案
- 原生能力:使用平台原生的定位能力
- 第三方服务:集成第三方定位服务
实用案例
案例 1:实时位置追踪应用
需求分析
- 实时追踪用户位置
- 在地图上显示用户移动轨迹
- 计算移动距离和速度
- 支持后台定位
- 优化电池消耗
实现方案
实时定位
<template> <view class="tracking-container"> <map id="tracking-map" :style="{ width: '100%', height: '500rpx' }" :latitude="currentLat" :longitude="currentLng" :scale="16" :polyline="polyline" :markers="markers" class="tracking-map" ></map> <view class="tracking-info"> <text class="info-item">当前位置:{{ formatLocation(currentLat, currentLng) }}</text> <text class="info-item">移动距离:{{ totalDistance }}米</text> <text class="info-item">当前速度:{{ currentSpeed }}km/h</text> <text class="info-item">定位精度:{{ accuracy }}米</text> </view> <view class="controls"> <button @click="toggleTracking" :type="isTracking ? 'warn' : 'primary'" class="control-btn" > {{ isTracking ? '停止追踪' : '开始追踪' }} </button> <button @click="saveTrack" type="primary" class="control-btn" v-if="!isTracking && trackPoints.length > 0"> 保存轨迹 </button> </view> </view> </template> <script> export default { data() { return { currentLat: 39.9042, currentLng: 116.4074, isTracking: false, watchId: null, trackPoints: [], polyline: [], markers: [], totalDistance: 0, currentSpeed: 0, accuracy: 0, lastPoint: null } }, onLoad() { this.checkPermission() }, onUnload() { if (this.watchId) { uni.clearWatch(this.watchId) } }, methods: { checkPermission() { uni.getSetting({ success: (res) => { if (!res.authSetting['scope.userLocation']) { uni.authorize({ scope: 'scope.userLocation', success: () => { console.log('授权成功') }, fail: () => { uni.showToast({ title: '需要位置权限才能使用定位功能', icon: 'none' }) } }) } } }) }, toggleTracking() { if (this.isTracking) { this.stopTracking() } else { this.startTracking() } }, startTracking() { this.isTracking = true this.trackPoints = [] this.totalDistance = 0 this.lastPoint = null this.watchId = uni.watchLocation({ type: 'gcj02', altitude: true, accuracy: 'high', interval: 5000, // 每5秒更新一次 success: (res) => { this.currentLat = res.latitude this.currentLng = res.longitude this.accuracy = res.accuracy this.currentSpeed = res.speed ? (res.speed * 3.6).toFixed(1) : 0 const point = { latitude: res.latitude, longitude: res.longitude, altitude: res.altitude, speed: res.speed, accuracy: res.accuracy, timestamp: res.timestamp } this.trackPoints.push(point) this.calculateDistance(point) this.updateMap() }, fail: (err) => { console.error('定位失败', err) uni.showToast({ title: '定位失败,请检查位置权限', icon: 'none' }) this.isTracking = false } }) }, stopTracking() { this.isTracking = false if (this.watchId) { uni.clearWatch(this.watchId) this.watchId = null } }, calculateDistance(currentPoint) { if (this.lastPoint) { // 使用Haversine公式计算两点之间的距离 const R = 6371e3 // 地球半径(米) const φ1 = this.lastPoint.latitude * Math.PI / 180 const φ2 = currentPoint.latitude * Math.PI / 180 const Δφ = (currentPoint.latitude - this.lastPoint.latitude) * Math.PI / 180 const Δλ = (currentPoint.longitude - this.lastPoint.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)) const distance = R * c this.totalDistance += distance } this.lastPoint = currentPoint }, updateMap() { if (this.trackPoints.length > 0) { // 更新轨迹线 this.polyline = [{ points: this.trackPoints, color: '#007AFF', width: 5, dottedLine: false }] // 更新标记 this.markers = [ { id: 1, latitude: this.currentLat, longitude: this.currentLng, title: '当前位置', iconPath: '/static/marker-current.png', width: 30, height: 30 } ] } }, saveTrack() { if (this.trackPoints.length < 2) { uni.showToast({ title: '轨迹太短,无法保存', icon: 'none' }) return } const track = { id: Date.now(), points: this.trackPoints, distance: Math.round(this.totalDistance), duration: Math.round((this.trackPoints[this.trackPoints.length - 1].timestamp - this.trackPoints[0].timestamp) / 1000), startTime: this.trackPoints[0].timestamp, endTime: this.trackPoints[this.trackPoints.length - 1].timestamp } // 保存轨迹到本地存储 let tracks = uni.getStorageSync('tracks') || [] tracks.push(track) uni.setStorageSync('tracks', tracks) uni.showToast({ title: '轨迹保存成功', icon: 'success' }) }, formatLocation(lat, lng) { return `${lat.toFixed(6)}, ${lng.toFixed(6)}` } } } </script>后台定位
- 配置后台定位权限
- 处理后台定位的限制
- 优化后台定位的电池消耗
案例 2:附近服务应用
需求分析
- 显示用户当前位置
- 搜索附近的服务(如餐厅、加油站、医院等)
- 计算用户与服务点的距离
- 显示服务点的详细信息
实现方案
附近服务搜索
<template> <view class="nearby-services"> <view class="location-header"> <text class="location-title">当前位置</text> <text class="location-info">{{ currentLocation }}</text> <button @click="refreshLocation" class="refresh-btn"> <text class="icon">🔄</text> </button> </view> <view class="service-categories"> <button v-for="(category, index) in categories" :key="index" @click="selectCategory(category)" :class="{ active: selectedCategory === category.id }" class="category-btn" > <text class="category-icon">{{ category.icon }}</text> <text class="category-name">{{ category.name }}</text> </button> </view> <view class="service-list"> <view v-for="(service, index) in services" :key="index" @click="showServiceDetail(service)" class="service-item" > <view class="service-icon">{{ getCategoryIcon(service.category) }}</view> <view class="service-info"> <text class="service-name">{{ service.name }}</text> <text class="service-address">{{ service.address }}</text> </view> <view class="service-distance"> <text class="distance">{{ service.distance }}米</text> </view> </view> </view> </view> </template> <script> export default { data() { return { currentLat: 39.9042, currentLng: 116.4074, currentLocation: '获取位置中...', selectedCategory: 'all', categories: [ { id: 'all', name: '全部', icon: '🏠' }, { id: 'restaurant', name: '餐厅', icon: '🍽️' }, { id: 'gas', name: '加油站', icon: '⛽' }, { id: 'hospital', name: '医院', icon: '🏥' }, { id: 'hotel', name: '酒店', icon: '🏨' }, { id: 'park', name: '公园', icon: '🌳' } ], services: [] } }, onLoad() { this.getCurrentLocation() }, methods: { getCurrentLocation() { uni.getLocation({ type: 'gcj02', success: (res) => { this.currentLat = res.latitude this.currentLng = res.longitude this.reverseGeocode(res.latitude, res.longitude) this.searchNearbyServices() }, fail: (err) => { console.error('获取位置失败', err) this.currentLocation = '获取位置失败' } }) }, refreshLocation() { this.getCurrentLocation() }, reverseGeocode(lat, lng) { // 调用逆地理编码服务获取地址 // 实际项目中需要使用第三方API this.currentLocation = '北京市朝阳区建国路' }, searchNearbyServices() { // 模拟附近服务数据 this.services = [ { id: 1, name: '麦当劳', address: '北京市朝阳区建国路88号', latitude: 39.9052, longitude: 116.4084, category: 'restaurant', distance: 200 }, { id: 2, name: '中石化加油站', address: '北京市朝阳区建国路90号', latitude: 39.9062, longitude: 116.4094, category: 'gas', distance: 350 }, { id: 3, name: '北京协和医院', address: '北京市东城区帅府园1号', latitude: 39.9142, longitude: 116.4174, category: 'hospital', distance: 1200 }, { id: 4, name: '北京国贸大酒店', address: '北京市朝阳区建国门外大街1号', latitude: 39.9072, longitude: 116.4104, category: 'hotel', distance: 500 }, { id: 5, name: '朝阳公园', address: '北京市朝阳区朝阳公园南路1号', latitude: 39.9342, longitude: 116.4574, category: 'park', distance: 3500 } ] // 计算距离 this.services.forEach(service => { service.distance = this.calculateDistance( this.currentLat, this.currentLng, service.latitude, service.longitude ) }) // 按距离排序 this.services.sort((a, b) => a.distance - b.distance) }, calculateDistance(lat1, lng1, lat2, lng2) { // 使用Haversine公式计算距离 const R = 6371e3 // 地球半径(米) const φ1 = lat1 * Math.PI / 180 const φ2 = lat2 * Math.PI / 180 const Δφ = (lat2 - lat1) * Math.PI / 180 const Δλ = (lng2 - lng1) * 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)) return Math.round(R * c) }, selectCategory(categoryId) { this.selectedCategory = categoryId if (categoryId === 'all') { this.searchNearbyServices() } else { const filteredServices = this.services.filter(service => service.category === categoryId) this.services = filteredServices } }, getCategoryIcon(categoryId) { const category = this.categories.find(c => c.id === categoryId) return category ? category.icon : '🏠' }, showServiceDetail(service) { uni.showModal({ title: service.name, content: `地址:${service.address}\n距离:${service.distance}米`, confirmText: '导航', cancelText: '取消', success: (res) => { if (res.confirm) { this.navigateToService(service) } } }) }, navigateToService(service) { uni.openLocation({ latitude: service.latitude, longitude: service.longitude, name: service.name, address: service.address, scale: 18 }) } } } </script>服务点详情
- 显示服务点的详细信息
- 提供导航功能
- 显示用户评价和评分
案例 3:地理围栏应用
需求分析
- 设置地理围栏
- 当用户进入或离开围栏时触发通知
- 管理多个地理围栏
- 显示围栏状态
实现方案
地理围栏设置
<template> <view class="geofence-app"> <view class="fence-list"> <text class="list-title">我的地理围栏</text> <view v-for="(fence, index) in fences" :key="index" class="fence-item" > <view class="fence-info"> <text class="fence-name">{{ fence.name }}</text> <text class="fence-status">{{ fence.status ? '在围栏内' : '在围栏外' }}</text> </view> <view class="fence-details"> <text class="fence-radius">半径:{{ fence.radius }}米</text> <text class="fence-address">{{ fence.address }}</text> </view> </view> </view> <button @click="addFence" type="primary" class="add-btn"> 添加地理围栏 </button> </view> </template> <script> export default { data() { return { fences: [ { id: 1, name: '家', latitude: 39.9042, longitude: 116.4074, radius: 100, address: '北京市朝阳区建国路88号', status: false }, { id: 2, name: '公司', latitude: 39.9142, longitude: 116.4174, radius: 150, address: '北京市朝阳区建国门外大街1号', status: false } ], watchId: null } }, onLoad() { this.startMonitoring() }, onUnload() { if (this.watchId) { uni.clearWatch(this.watchId) } }, methods: { startMonitoring() { this.watchId = uni.watchLocation({ type: 'gcj02', accuracy: 'medium', interval: 10000, // 每10秒更新一次 success: (res) => { this.checkGeofences(res.latitude, res.longitude) }, fail: (err) => { console.error('定位失败', err) } }) }, checkGeofences(lat, lng) { this.fences.forEach(fence => { const distance = this.calculateDistance( lat, lng, fence.latitude, fence.longitude ) const isInside = distance <= fence.radius if (isInside !== fence.status) { fence.status = isInside this.notifyFenceChange(fence, isInside) } }) }, calculateDistance(lat1, lng1, lat2, lng2) { const R = 6371e3 const φ1 = lat1 * Math.PI / 180 const φ2 = lat2 * Math.PI / 180 const Δφ = (lat2 - lat1) * Math.PI / 180 const Δλ = (lng2 - lng1) * 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)) return R * c }, notifyFenceChange(fence, isInside) { uni.showToast({ title: `您${isInside ? '进入' : '离开'}了${fence.name}`, icon: 'none' }) }, addFence() { uni.chooseLocation({ success: (res) => { const newFence = { id: Date.now(), name: '新围栏', latitude: res.latitude, longitude: res.longitude, radius: 100, address: res.address, status: false } this.fences.push(newFence) uni.showToast({ title: '地理围栏添加成功', icon: 'success' }) } }) } } } </script>地理围栏管理
- 编辑和删除地理围栏
- 设置围栏半径和触发条件
- 显示围栏在地图上的位置
学习目标
- 掌握 uni-app 中定位服务的核心 API 和使用方法
- 学会实现实时定位和位置追踪功能
- 了解定位权限的管理和申请方法
- 能够开发基于位置的应用,如附近服务、地理围栏等
- 掌握位置计算和地理围栏的实现方法
- 提升定位服务的准确性和可靠性
总结
定位服务是 uni-app 应用开发中的重要组成部分,通过合理使用定位 API、优化定位参数、处理权限问题,可以开发出功能丰富的基于位置的应用。在实际开发中,开发者应该根据具体场景选择合适的定位方式和参数,平衡定位精度和功耗,为用户提供最佳的定位体验。
同时,开发者还需要注意跨平台定位的差异,使用条件编译或平台特定的 API 来处理不同平台的定位问题,确保应用在所有平台上都能正常运行定位功能。对于需要高精度定位的应用,还可以考虑使用第三方定位服务,提升定位的准确性和可靠性。