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);
});
};案例解析
- 地图页面:我们创建了一个地图页面,用于显示地图和附近的商家,包括地图组件、商家列表、底部操作栏和商家详情弹窗。
- 定位功能:我们实现了获取用户当前位置的功能,并在地图上显示当前位置标记。
- 附近商家查询:我们实现了查询附近商家的功能,并在地图上显示商家标记,点击商家可以查看详情。
- 商家详情:我们实现了商家详情弹窗,显示商家的详细信息,包括地址、电话、描述等,并提供拨打电话和导航功能。
- 权限配置:我们在 manifest.json 中配置了定位权限,确保应用可以获取用户的位置信息。
代码优化建议
- 添加定位失败处理:在定位失败时,提供默认位置或提示用户开启定位权限。
- 优化商家查询:使用防抖或节流,避免频繁查询商家。
- 添加缓存:对商家数据添加缓存,减少重复查询。
- 优化地图性能:当商家数量较多时,考虑使用集群标记,提高地图渲染性能。
- 添加错误处理:对网络请求、权限获取等可能出现的错误进行处理,提供友好的错误提示。
常见问题与解决方案
定位失败
- 问题:无法获取用户位置,可能是权限问题或网络问题
- 解决方案:
- 在 manifest.json 中配置定位权限
- 在应用启动时请求定位权限
- 处理权限被拒绝的情况
- 提供默认位置作为 fallback
地图显示异常
- 问题:地图不显示或显示异常
- 解决方案:
- 检查地图组件的配置是否正确
- 检查地图提供商的 SDK 是否正确集成
- 检查网络连接是否正常
- 测试不同平台的地图显示效果
路径规划失败
- 问题:路径规划失败,可能是起点或终点无效
- 解决方案:
- 检查起点和终点的坐标是否有效
- 检查网络连接是否正常
- 处理路径规划失败的情况
- 提供替代路线或提示
跨域问题
- 问题:在 H5 端,调用地图 API 可能会遇到跨域问题
- 解决方案:
- 使用代理服务器
- 在 HBuilderX 中配置跨域代理
- 使用地图提供商的 JS API
兼容性问题
- 问题:不同平台的地图 API 实现存在差异
- 解决方案:
- 使用 uni-app 提供的统一 API
- 针对不同平台进行适配
- 测试不同平台的表现
学习总结
通过本章节的学习,我们掌握了以下知识:
- 地图组件:了解了 uni-app 提供的地图组件,包括 map 组件、地图控件、地图覆盖物等。
- 定位 API:了解了 uni-app 提供的定位相关 API,包括
uni.getLocation()、uni.chooseLocation()、uni.openLocation()等。 - 路径规划:了解了 uni-app 支持的路径规划功能,包括驾车、步行、骑行、公交路线规划等。
- 实际应用:通过实际案例,掌握了如何实现附近商家查询功能,包括获取用户位置、查询附近商家、在地图上显示商家位置、查看商家详情等。
在实际开发中,我们需要根据具体需求,选择合适的地图提供商和 API,并结合项目特点进行适当的封装和优化,以提高应用的性能和用户体验。同时,我们也需要注意不同平台的兼容性问题,确保应用在各种设备上都能正常运行。