uni-app 地图与定位

章节概览

在移动应用开发中,地图与定位功能是非常重要的一部分,它可以帮助我们实现附近商家查询、路径规划、导航等功能,为用户提供位置相关的服务。本章节将详细介绍 uni-app 中的地图与定位功能,包括地图组件的使用、定位 API 的调用、路径规划等,帮助开发者实现附近商家查询等功能。

核心知识点

1. 地图组件

uni-app 提供了地图组件,用于在应用中显示地图,主要包括:

  • map 组件:显示地图,支持多种地图提供商(如高德地图、百度地图、腾讯地图等)
  • 地图控件:包括缩放控件、定位控件、指南针控件等
  • 地图覆盖物:包括标记点、折线、多边形、圆形等

2. 定位 API

uni-app 提供了定位相关的 API,用于获取用户的位置信息,主要包括:

  • **uni.getLocation()**:获取当前位置
  • **uni.chooseLocation()**:选择位置
  • **uni.openLocation()**:打开地图选择位置

3. 路径规划

uni-app 支持路径规划功能,用于计算从起点到终点的路线,主要包括:

  • 驾车路线规划:计算驾车路线
  • 步行路线规划:计算步行路线
  • 骑行路线规划:计算骑行路线
  • 公交路线规划:计算公交路线

实用案例:实现附近商家查询

案例描述

我们将创建一个附近商家查询功能,实现获取用户当前位置,然后查询附近的商家,并在地图上显示商家位置,点击商家可以查看详情。

案例实现

1. 创建地图页面

首先,我们创建一个地图页面,用于显示地图和附近的商家:

<template>
  <view class="map-page">
    <!-- 地图组件 -->
    <map 
      id="map" 
      class="map" 
      :latitude="latitude" 
      :longitude="longitude" 
      :scale="16" 
      :markers="markers"
      :controls="controls"
      @markertap="handleMarkerTap"
      @regionchange="handleRegionChange"
      @tap="handleMapTap"
    ></map>
    
    <!-- 商家列表 -->
    <view class="business-list" :class="{ 'show': showBusinessList }">
      <view class="list-header">
        <text class="title">附近商家</text>
        <text class="count">({{ businesses.length }})</text>
        <view class="close-btn" @click="showBusinessList = false">
          <text>×</text>
        </view>
      </view>
      <scroll-view class="list-content" scroll-y>
        <view 
          v-for="(item, index) in businesses" 
          :key="index"
          class="business-item"
          @click="selectBusiness(item)"
        >
          <image :src="item.image" mode="aspectFill"></image>
          <view class="business-info">
            <text class="business-name">{{ item.name }}</text>
            <text class="business-address">{{ item.address }}</text>
            <view class="business-footer">
              <text class="business-distance">{{ item.distance }}m</text>
              <text class="business-rating">{{ item.rating }}★</text>
            </view>
          </view>
        </view>
        <view v-if="businesses.length === 0" class="empty-state">
          <text>暂无附近商家</text>
        </view>
      </scroll-view>
    </view>
    
    <!-- 底部操作栏 -->
    <view class="bottom-bar">
      <view class="location-btn" @click="getLocation">
        <text class="icon">📍</text>
        <text class="text">定位</text>
      </view>
      <view class="business-btn" @click="showBusinessList = true">
        <text class="icon">🏪</text>
        <text class="text">商家</text>
      </view>
      <view class="route-btn" @click="planRoute">
        <text class="icon">🗺️</text>
        <text class="text">路线</text>
      </view>
    </view>
    
    <!-- 商家详情弹窗 -->
    <view v-if="selectedBusiness" class="business-detail" @click.stop>
      <view class="detail-content">
        <view class="detail-header">
          <text class="detail-title">{{ selectedBusiness.name }}</text>
          <view class="detail-close" @click="selectedBusiness = null">
            <text>×</text>
          </view>
        </view>
        <image :src="selectedBusiness.image" mode="aspectFill"></image>
        <view class="detail-info">
          <text class="detail-address">{{ selectedBusiness.address }}</text>
          <text class="detail-phone">{{ selectedBusiness.phone }}</text>
          <text class="detail-description">{{ selectedBusiness.description }}</text>
        </view>
        <view class="detail-actions">
          <view class="action-btn call" @click="callBusiness(selectedBusiness.phone)">
            <text class="icon">📞</text>
            <text class="text">电话</text>
          </view>
          <view class="action-btn route" @click="navigateToBusiness(selectedBusiness)">
            <text class="icon">🧭</text>
            <text class="text">导航</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      // 地图中心坐标
      latitude: 39.9042,
      longitude: 116.4074,
      // 标记点
      markers: [],
      // 控件
      controls: [
        {
          id: 1,
          position: {
            left: 20,
            top: 20,
            width: 50,
            height: 50
          },
          iconPath: '/static/location.png',
          clickable: true
        }
      ],
      // 商家列表
      businesses: [],
      // 显示商家列表
      showBusinessList: false,
      // 选中的商家
      selectedBusiness: null,
      // 是否正在定位
      isLocating: false
    };
  },
  
  onLoad() {
    // 获取当前位置
    this.getLocation();
  },
  
  methods: {
    // 获取当前位置
    getLocation() {
      if (this.isLocating) return;
      
      uni.showLoading({
        title: '定位中...',
        mask: true
      });
      
      this.isLocating = true;
      
      uni.getLocation({
        type: 'gcj02',
        altitude: true,
        success: (res) => {
          console.log('获取位置成功:', res);
          
          // 更新地图中心坐标
          this.latitude = res.latitude;
          this.longitude = res.longitude;
          
          // 添加当前位置标记
          this.markers = [
            {
              id: 0,
              latitude: res.latitude,
              longitude: res.longitude,
              iconPath: '/static/current-location.png',
              width: 30,
              height: 30,
              title: '我的位置'
            }
          ];
          
          // 查询附近商家
          this.searchNearbyBusinesses(res.latitude, res.longitude);
        },
        fail: (err) => {
          console.error('获取位置失败:', err);
          uni.showToast({
            title: '定位失败,请检查位置权限',
            icon: 'none'
          });
        },
        complete: () => {
          uni.hideLoading();
          this.isLocating = false;
        }
      });
    },
    
    // 查询附近商家
    searchNearbyBusinesses(latitude, longitude) {
      uni.showLoading({
        title: '查询中...',
        mask: true
      });
      
      // 模拟查询附近商家
      setTimeout(() => {
        // 模拟商家数据
        const businesses = [
          {
            id: 1,
            name: '星巴克咖啡',
            address: '北京市朝阳区建国路88号',
            phone: '010-12345678',
            latitude: latitude + 0.001,
            longitude: longitude + 0.001,
            distance: 100,
            rating: 4.5,
            image: 'https://img-cdn-tc.dcloud.net.cn/uni-app/images/uni@2x.png',
description: '全球连锁咖啡品牌,提供高品质咖啡和舒适的环境'
          },
          {
            id: 2,
            name: '肯德基',
            address: '北京市朝阳区建国路99号',
            phone: '010-87654321',
            latitude: latitude - 0.001,
            longitude: longitude - 0.001,
            distance: 150,
            rating: 4.0,
            image: 'https://img-cdn-tc.dcloud.net.cn/uni-app/images/uni@2x.png',
description: '全球连锁快餐品牌,提供美味的炸鸡和汉堡'
          },
          {
            id: 3,
            name: '麦当劳',
            address: '北京市朝阳区建国路100号',
            phone: '010-11223344',
            latitude: latitude + 0.002,
            longitude: longitude - 0.002,
            distance: 200,
            rating: 4.2,
            image: 'https://img-cdn-tc.dcloud.net.cn/uni-app/images/uni@2x.png',
description: '全球连锁快餐品牌,提供美味的汉堡和薯条'
          }
        ];
        
        this.businesses = businesses;
        
        // 添加商家标记
        this.addBusinessMarkers(businesses);
        
        uni.hideLoading();
      }, 1000);
    },
    
    // 添加商家标记
    addBusinessMarkers(businesses) {
      const markers = [
        {
          id: 0,
          latitude: this.latitude,
          longitude: this.longitude,
          iconPath: '/static/current-location.png',
          width: 30,
          height: 30,
          title: '我的位置'
        }
      ];
      
      businesses.forEach((business, index) => {
        markers.push({
          id: business.id,
          latitude: business.latitude,
          longitude: business.longitude,
          iconPath: '/static/business-marker.png',
          width: 30,
          height: 30,
          title: business.name,
          callout: {
            content: business.name,
            color: '#333',
            fontSize: 14,
            borderRadius: 4,
            bgColor: '#fff',
            padding: 8,
            display: 'BYCLICK'
          }
        });
      });
      
      this.markers = markers;
    },
    
    // 处理标记点点击
    handleMarkerTap(e) {
      const markerId = e.markerId;
      if (markerId === 0) return; // 跳过当前位置标记
      
      // 查找对应的商家
      const business = this.businesses.find(item => item.id === markerId);
      if (business) {
        this.selectedBusiness = business;
      }
    },
    
    // 处理地图区域变化
    handleRegionChange(e) {
      // 地图区域变化时,可以重新查询商家
      if (e.type === 'end' && e.causedBy === 'drag') {
        // 获取地图当前区域
        const mapContext = uni.createMapContext('map');
        mapContext.getRegion({
          success: (res) => {
            console.log('地图区域:', res);
            // 可以根据地图区域重新查询商家
          }
        });
      }
    },
    
    // 处理地图点击
    handleMapTap() {
      this.selectedBusiness = null;
    },
    
    // 选择商家
    selectBusiness(business) {
      // 更新地图中心到商家位置
      this.latitude = business.latitude;
      this.longitude = business.longitude;
      
      // 显示商家详情
      this.selectedBusiness = business;
      
      // 隐藏商家列表
      this.showBusinessList = false;
    },
    
    // 拨打电话
    callBusiness(phone) {
      uni.makePhoneCall({
        phoneNumber: phone,
        success: () => {
          console.log('拨打电话成功');
        },
        fail: (err) => {
          console.error('拨打电话失败:', err);
        }
      });
    },
    
    // 导航到商家
    navigateToBusiness(business) {
      uni.openLocation({
        latitude: business.latitude,
        longitude: business.longitude,
        name: business.name,
        address: business.address,
        success: () => {
          console.log('打开地图成功');
        },
        fail: (err) => {
          console.error('打开地图失败:', err);
        }
      });
    },
    
    // 路线规划
    planRoute() {
      if (this.businesses.length === 0) {
        uni.showToast({
          title: '请先查询附近商家',
          icon: 'none'
        });
        return;
      }
      
      // 选择第一个商家作为目的地
      const destination = this.businesses[0];
      
      // 打开地图进行路线规划
      uni.openLocation({
        latitude: destination.latitude,
        longitude: destination.longitude,
        name: destination.name,
        address: destination.address,
        success: () => {
          console.log('打开地图成功');
        },
        fail: (err) => {
          console.error('打开地图失败:', err);
        }
      });
    }
  }
};
</script>

<style scoped>
.map-page {
  min-height: 100vh;
  position: relative;
}

.map {
  width: 100%;
  height: 100vh;
}

.business-list {
  position: absolute;
  bottom: 120rpx;
  left: 0;
  right: 0;
  height: 500rpx;
  background-color: #fff;
  border-top-left-radius: 20rpx;
  border-top-right-radius: 20rpx;
  box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.1);
  transform: translateY(100%);
  transition: transform 0.3s ease;
}

.business-list.show {
  transform: translateY(0);
}

.list-header {
  height: 80rpx;
  padding: 0 30rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1rpx solid #e8e8e8;
}

.list-header .title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
}

.list-header .count {
  font-size: 24rpx;
  color: #999;
}

.list-header .close-btn {
  width: 40rpx;
  height: 40rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32rpx;
  color: #999;
}

.list-content {
  height: 420rpx;
  padding: 20rpx;
}

.business-item {
  display: flex;
  padding: 20rpx;
  margin-bottom: 20rpx;
  background-color: #f5f5f5;
  border-radius: 10rpx;
}

.business-item image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 8rpx;
}

.business-info {
  flex: 1;
  margin-left: 20rpx;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.business-name {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 10rpx;
}

.business-address {
  font-size: 24rpx;
  color: #666;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.business-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.business-distance {
  font-size: 22rpx;
  color: #999;
}

.business-rating {
  font-size: 22rpx;
  color: #ffb000;
}

.empty-state {
  height: 300rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24rpx;
  color: #999;
}

.bottom-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 120rpx;
  background-color: #fff;
  display: flex;
  align-items: center;
  justify-content: space-around;
  border-top: 1rpx solid #e8e8e8;
  /* 适配 iPhone X 等机型的底部安全区域 */
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}

.bottom-bar .location-btn,
.bottom-bar .business-btn,
.bottom-bar .route-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 24rpx;
  color: #666;
}

.bottom-bar .icon {
  font-size: 36rpx;
  margin-bottom: 5rpx;
}

.business-detail {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 999;
}

.detail-content {
  width: 100%;
  max-height: 80vh;
  background-color: #fff;
  border-top-left-radius: 20rpx;
  border-top-right-radius: 20rpx;
  overflow: hidden;
}

.detail-header {
  height: 80rpx;
  padding: 0 30rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1rpx solid #e8e8e8;
}

.detail-header .detail-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
}

.detail-header .detail-close {
  width: 40rpx;
  height: 40rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32rpx;
  color: #999;
}

.detail-content image {
  width: 100%;
  height: 300rpx;
}

.detail-info {
  padding: 30rpx;
}

.detail-address {
  font-size: 24rpx;
  color: #666;
  margin-bottom: 15rpx;
}

.detail-phone {
  font-size: 24rpx;
  color: #1890ff;
  margin-bottom: 15rpx;
}

.detail-description {
  font-size: 24rpx;
  color: #666;
  line-height: 1.5;
}

.detail-actions {
  height: 100rpx;
  display: flex;
  align-items: center;
  justify-content: space-around;
  border-top: 1rpx solid #e8e8e8;
}

.detail-actions .action-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 24rpx;
  color: #666;
}

.detail-actions .action-btn .icon {
  font-size: 36rpx;
  margin-bottom: 5rpx;
}

.detail-actions .action-btn.call {
  color: #1890ff;
}

.detail-actions .action-btn.route {
  color: #52c41a;
}
</style>

2. 创建定位权限配置

在使用定位功能前,我们需要在 manifest.json 中配置定位权限:

{
  "name": "uni-app-map-demo",
  "appid": "",
  "description": "uni-app 地图与定位示例",
  "versionName": "1.0.0",
  "versionCode": "1",
  "transformPx": true,
  "app-plus": {
    "distribute": {
      "android": {
        "permissions": [
          {
            "name": "android.permission.ACCESS_COARSE_LOCATION"
          },
          {
            "name": "android.permission.ACCESS_FINE_LOCATION"
          }
        ]
      },
      "ios": {
        "infoPlist": {
          "NSLocationWhenInUseUsageDescription": "需要获取您的位置信息,以便为您提供附近的商家服务"
        }
      }
    }
  },
  "mp-weixin": {
    "appid": "",
    "setting": {
      "urlCheck": false
    },
    "permission": {
      "scope.userLocation": {
        "desc": "需要获取您的位置信息,以便为您提供附近的商家服务"
      }
    }
  }
}

3. 创建模拟数据服务

为了方便测试,我们可以创建一个模拟数据服务,用于模拟附近商家的数据:

// services/businessService.js

/**
 * 查询附近商家
 * @param {number} latitude - 纬度
 * @param {number} longitude - 经度
 * @param {number} radius - 半径(米)
 * @param {number} page - 页码
 * @param {number} pageSize - 每页数量
 * @returns {Promise} - 返回商家列表
 */
export const searchNearbyBusinesses = (latitude, longitude, radius = 5000, page = 1, pageSize = 20) => {
  return new Promise((resolve, reject) => {
    // 模拟网络请求延迟
    setTimeout(() => {
      // 生成模拟数据
      const businesses = [];
      for (let i = 0; i < pageSize; i++) {
        businesses.push({
          id: i + 1,
          name: `商家${i + 1}`,
          address: `北京市朝阳区建国路${88 + i}号`,
          phone: `010-123456${i.toString().padStart(2, '0')}`,
          latitude: latitude + (Math.random() - 0.5) * 0.01,
          longitude: longitude + (Math.random() - 0.5) * 0.01,
          distance: Math.floor(Math.random() * radius),
          rating: (Math.random() * 5).toFixed(1),
          image: 'https://img-cdn-tc.dcloud.net.cn/uni-app/images/uni@2x.png',
description: `这是商家${i + 1}的描述,提供优质的服务和产品。`
        });
      }
      
      resolve({
        list: businesses,
        total: 100,
        page: page,
        pageSize: pageSize
      });
    }, 1000);
  });
};

/**
 * 获取商家详情
 * @param {number} id - 商家 ID
 * @returns {Promise} - 返回商家详情
 */
export const getBusinessDetail = (id) => {
  return new Promise((resolve, reject) => {
    // 模拟网络请求延迟
    setTimeout(() => {
      resolve({
        id: id,
        name: `商家${id}`,
        address: `北京市朝阳区建国路${88 + id}号`,
        phone: `010-123456${id.toString().padStart(2, '0')}`,
        latitude: 39.9042 + (Math.random() - 0.5) * 0.01,
        longitude: 116.4074 + (Math.random() - 0.5) * 0.01,
        distance: Math.floor(Math.random() * 5000),
        rating: (Math.random() * 5).toFixed(1),
        image: 'https://img-cdn-tc.dcloud.net.cn/uni-app/images/uni@2x.png',
description: `这是商家${id}的详细描述,提供优质的服务和产品。营业时间:9:00-22:00。`,
        hours: '9:00-22:00',
        categories: ['餐饮', '娱乐', '购物'],
        features: ['免费WiFi', '停车位', '外卖']
      });
    }, 500);
  });
};

案例解析

  1. 地图页面:我们创建了一个地图页面,用于显示地图和附近的商家,包括地图组件、商家列表、底部操作栏和商家详情弹窗。
  2. 定位功能:我们实现了获取用户当前位置的功能,并在地图上显示当前位置标记。
  3. 附近商家查询:我们实现了查询附近商家的功能,并在地图上显示商家标记,点击商家可以查看详情。
  4. 商家详情:我们实现了商家详情弹窗,显示商家的详细信息,包括地址、电话、描述等,并提供拨打电话和导航功能。
  5. 权限配置:我们在 manifest.json 中配置了定位权限,确保应用可以获取用户的位置信息。

代码优化建议

  1. 添加定位失败处理:在定位失败时,提供默认位置或提示用户开启定位权限。
  2. 优化商家查询:使用防抖或节流,避免频繁查询商家。
  3. 添加缓存:对商家数据添加缓存,减少重复查询。
  4. 优化地图性能:当商家数量较多时,考虑使用集群标记,提高地图渲染性能。
  5. 添加错误处理:对网络请求、权限获取等可能出现的错误进行处理,提供友好的错误提示。

常见问题与解决方案

  1. 定位失败

    • 问题:无法获取用户位置,可能是权限问题或网络问题
    • 解决方案
      • 在 manifest.json 中配置定位权限
      • 在应用启动时请求定位权限
      • 处理权限被拒绝的情况
      • 提供默认位置作为 fallback
  2. 地图显示异常

    • 问题:地图不显示或显示异常
    • 解决方案
      • 检查地图组件的配置是否正确
      • 检查地图提供商的 SDK 是否正确集成
      • 检查网络连接是否正常
      • 测试不同平台的地图显示效果
  3. 路径规划失败

    • 问题:路径规划失败,可能是起点或终点无效
    • 解决方案
      • 检查起点和终点的坐标是否有效
      • 检查网络连接是否正常
      • 处理路径规划失败的情况
      • 提供替代路线或提示
  4. 跨域问题

    • 问题:在 H5 端,调用地图 API 可能会遇到跨域问题
    • 解决方案
      • 使用代理服务器
      • 在 HBuilderX 中配置跨域代理
      • 使用地图提供商的 JS API
  5. 兼容性问题

    • 问题:不同平台的地图 API 实现存在差异
    • 解决方案
      • 使用 uni-app 提供的统一 API
      • 针对不同平台进行适配
      • 测试不同平台的表现

学习总结

通过本章节的学习,我们掌握了以下知识:

  1. 地图组件:了解了 uni-app 提供的地图组件,包括 map 组件、地图控件、地图覆盖物等。
  2. 定位 API:了解了 uni-app 提供的定位相关 API,包括 uni.getLocation()uni.chooseLocation()uni.openLocation() 等。
  3. 路径规划:了解了 uni-app 支持的路径规划功能,包括驾车、步行、骑行、公交路线规划等。
  4. 实际应用:通过实际案例,掌握了如何实现附近商家查询功能,包括获取用户位置、查询附近商家、在地图上显示商家位置、查看商家详情等。

在实际开发中,我们需要根据具体需求,选择合适的地图提供商和 API,并结合项目特点进行适当的封装和优化,以提高应用的性能和用户体验。同时,我们也需要注意不同平台的兼容性问题,确保应用在各种设备上都能正常运行。

« 上一篇 uni-app 媒体功能 下一篇 » uni-app 支付功能