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:实时位置追踪应用

需求分析

  • 实时追踪用户位置
  • 在地图上显示用户移动轨迹
  • 计算移动距离和速度
  • 支持后台定位
  • 优化电池消耗

实现方案

  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. 后台定位

    • 配置后台定位权限
    • 处理后台定位的限制
    • 优化后台定位的电池消耗

案例 2:附近服务应用

需求分析

  • 显示用户当前位置
  • 搜索附近的服务(如餐厅、加油站、医院等)
  • 计算用户与服务点的距离
  • 显示服务点的详细信息

实现方案

  1. 附近服务搜索

    <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>
  2. 服务点详情

    • 显示服务点的详细信息
    • 提供导航功能
    • 显示用户评价和评分

案例 3:地理围栏应用

需求分析

  • 设置地理围栏
  • 当用户进入或离开围栏时触发通知
  • 管理多个地理围栏
  • 显示围栏状态

实现方案

  1. 地理围栏设置

    <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>
  2. 地理围栏管理

    • 编辑和删除地理围栏
    • 设置围栏半径和触发条件
    • 显示围栏在地图上的位置

学习目标

  1. 掌握 uni-app 中定位服务的核心 API 和使用方法
  2. 学会实现实时定位和位置追踪功能
  3. 了解定位权限的管理和申请方法
  4. 能够开发基于位置的应用,如附近服务、地理围栏等
  5. 掌握位置计算和地理围栏的实现方法
  6. 提升定位服务的准确性和可靠性

总结

定位服务是 uni-app 应用开发中的重要组成部分,通过合理使用定位 API、优化定位参数、处理权限问题,可以开发出功能丰富的基于位置的应用。在实际开发中,开发者应该根据具体场景选择合适的定位方式和参数,平衡定位精度和功耗,为用户提供最佳的定位体验。

同时,开发者还需要注意跨平台定位的差异,使用条件编译或平台特定的 API 来处理不同平台的定位问题,确保应用在所有平台上都能正常运行定位功能。对于需要高精度定位的应用,还可以考虑使用第三方定位服务,提升定位的准确性和可靠性。

« 上一篇 uni-app 地图高级功能 下一篇 » uni-app 支付集成