第8章 8.1 地图与定位

🎯 为什么要学这个?

你有没有遇到过这种场景——

朋友发来一个地址「XX 路 88 号」,你复制到地图里搜了半天,发现定位跑偏了两条街;或者你做一个「附近美食」的功能,后端返回一堆经纬度,你看半天也不知道对应哪些店……

学完这一章,你能:

  1. 用代码获取用户当前位置(不是手动点,是程序自己知道你在哪)
  2. 在地图上标点、画线、显示信息,像外卖 App 那样
  3. 计算两点之间的距离,判断用户有没有进入某个范围

上一章我们做了一个多端电商项目,里面有收货地址功能——但那时候地址是用户手动填的。这一章我们让程序自己知道用户在哪,做真正的「LBS(基于位置的服务)」。


🧱 基础:3 个核心概念

概念 1:uni.getLocation —— 手机的「眼睛」

是什么:获取设备的当前位置坐标(经度和纬度)。

生活类比:就像你打开外卖 App,第一步它会问你「允许获取位置吗」——这一步就是 getLocation,它告诉 App:\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n「你现在在东经 XX 度、北纬 XX 度」。

为什么用:没有位置,后续的地图展示、距离计算、周边搜索都没法做。

怎么用

uni.getLocation({
type: 'gcj02',  // 坐标系类型, 国内必须用 gcj02(高德/腾讯/微信都用这个)
success: (res) => {
    console.log('当前位置:', res.latitude, res.longitude)
    console.log('精度:', res.accuracy)
},
fail: (err) => {
    console.log('获取失败', err)
}
})

res.latitude 是纬度,res.longitude 是经度。注意国内地图必须用 gcj02 坐标系,GPS 原生的 wgs84 坐标在国内地图上会偏!

概念 2:map 组件 —— 地图的「画布」

是什么:在页面上画一张地图,可以放标记点、圆、线。

生活类比:就像一张白纸,map 是纸,markers 是你在纸上贴的便利贴,告诉别人「这里有一家奶茶店」「这里在堵车」。

为什么用:光有坐标没用,人看不懂经纬度,得画成地图才直观。

怎么用

<template>
<map 
    :latitude="centerLat" 
    :longitude="centerLng" 
    :markers="markers"
    :circles="circles"
    style="width: 100%; height: 400px;"
/>
</template>

<script>
export default {
data() {
    return {
        centerLat: 31.230416,   // 地图中心纬度(上海人民广场附近)
        centerLng: 121.473701,  // 地图中心经度
        markers: [              // 标记点数组
            {
                id: 1,
                latitude: 31.230416,
                longitude: 121.473701,
                title: '上海人民广场',
                iconPath: '/static/marker.png',
                width: 30,
                height: 30
            }
        ],
        circles: [              // 圆(用来画范围)
            {
                latitude: 31.230416,
                longitude: 121.473701,
                radius: 500,    // 半径,单位米
                fillColor: '#ff000033',  // 半透明红色
                color: '#ff0000',
                strokeWidth: 2
            }
        ]
    }
}
}
</script>

latitudelongitude 决定地图中心在哪。markers 是标记点列表,circles 是圆形区域,两者组合可以做「某个地点周围 500 米范围」这种功能。

概念 3:距离计算 —— 两点之间有多远

是什么:根据两个点的坐标,算出直线距离。

生活类比:就像你打开导航,它说「距离目标还有 3.2 公里」——程序拿到你的坐标和目标坐标,算了一下告诉你。

为什么用:判断用户有没有进入某个区域(比如「进场打卡」「到达提醒」),或者给用户排序「离我最近的餐厅」。

怎么用(用微信小程序的 uni.createMapContext):

// 定义一个辅助函数,算两点之间的距离(单位:米)
function calcDistance(lat1, lng1, lat2, lng2) {
// 地球半径,单位米
const R = 6371000
// 转为弧度
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
}

// 例子:计算上海人民广场到南京路步行街的距离
const distance = calcDistance(31.230416, 121.473701, 31.235183, 121.471890)
console.log('距离是', Math.round(distance), '米')
// 输出:距离是 543 米

这个公式叫「球面余弦公式」,算的是地球表面的弧线距离。对于几公里内的短距离,误差很小,够用了。


🔥 实战:3 个项目

项目 1:5 分钟,「我在哪」基础版

目标:点击按钮,获取当前位置并在地图上标出来。

完整代码

<template>
<view class="container">
    <button type="primary" @click="getMyLocation">获取我的位置</button>

    <map 
        v-if="hasLocation"
        :latitude="myLat" 
        :longitude="myLng" 
        :markers="markers"
        style="width: 100%; height: 350px; margin-top: 20px;"
    />

    <view v-if="hasLocation" class="info">
        <text>纬度:{{ myLat.toFixed(6) }}</text>
        <text>经度:{{ myLng.toFixed(6) }}</text>
    </view>
</view>
</template>

<script>
export default {
data() {
    return {
        hasLocation: false,
        myLat: 0,
        myLng: 0,
        markers: []
    }
},
methods: {
    getMyLocation() {
        uni.getLocation({
            type: 'gcj02',
            success: (res) => {
                this.myLat = res.latitude
                this.myLng = res.longitude
                this.hasLocation = true
                this.markers = [{
                    id: 1,
                    latitude: res.latitude,
                    longitude: res.longitude,
                    iconPath: '/static/location.png',
                    width: 40,
                    height: 40
                }]
                uni.showToast({ title: '定位成功', icon: 'success' })
            },
            fail: () => {
                uni.showToast({ title: '定位失败', icon: 'none' })
            }
        })
    }
}
}
</script>

<style>
.container { padding: 20px; }
.info { margin-top: 15px; display: flex; flex-direction: column; }
.info text { margin: 5px 0; color: #666; }
</style>

预期输出:点击按钮 → 弹出「定位成功」→ 地图显示当前位置(蓝色标记点)

解释:核心就是 uni.getLocation 获取坐标,然后往 markers 数组里塞一个点,地图自动刷新显示。


项目 2:15 分钟,「附近餐厅」列表

目标:模拟从接口拿到一批餐厅数据,按「离我多远」排序,显示在地图上。

完整代码

// 模拟餐厅数据(实际项目从 API 获取)
const restaurants = [
{ name: '鼎泰丰', lat: 31.2315, lng: 121.4720, type: '中餐' },
{ name: '星巴克', lat: 31.2298, lng: 121.4755, type: '咖啡' },
{ name: '711便利店', lat: 31.2320, lng: 121.4710, type: '便利店' },
{ name: '绿茶餐厅', lat: 31.2285, lng: 121.4705, type: '中餐' },
{ name: '麦当劳', lat: 31.2330, lng: 121.4760, type: '快餐' },
]

// 计算距离的函数
function calcDistance(lat1, lng1, lat2, lng2) {
const R = 6371000
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) ** 2 +
          Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}

// 模拟获取用户位置
const userLat = 31.230416
const userLng = 121.473701

// 给每个餐厅加上距离,并按距离排序
const nearbyRestaurants = restaurants.map(r => ({
...r,
distance: calcDistance(userLat, userLng, r.lat, r.lng)
})).sort((a, b) => a.distance - b.distance)

// 输出结果
nearbyRestaurants.forEach(r => {
const distText = r.distance < 1000 
    ? `${Math.round(r.distance)}米` 
    : `${(r.distance / 1000).toFixed(1)}公里`
console.log(`${r.name} - ${r.type} - 距离${distText}`)
})

预期输出

鼎泰丰 - 中餐 - 距离89米
711便利店 - 便利店 - 距离179米
绿茶餐厅 - 中餐 - 距离262米
星巴克 - 咖啡 - 距离287米
麦当劳 - 快餐 - 距离421米

解释map 把每个餐厅加上计算出的距离,sort 按距离从小到大排序,最后 forEach 打印出来。这个模式在做「附近 XXX」类功能时非常常用。


项目 3:15 分钟,「打卡范围检测」工具

目标:用户到达某地 200 米范围内,就算打卡成功。模拟打卡场景。

完整代码

// 目标地点:某公司前台
const targetLocation = {
name: 'XX科技公司',
lat: 31.230416,
lng: 121.473701,
radius: 200  // 有效打卡范围 200 米
}

// 计算距离
function calcDistance(lat1, lng1, lat2, lng2) {
const R = 6371000
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) ** 2 +
          Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}

// 模拟 3 个员工的位置
const employees = [
{ name: '小王', lat: 31.2295, lng: 121.4720 },   // 距离 107 米,在范围内
{ name: '小李', lat: 31.2350, lng: 121.4780 },   // 距离 647 米,在范围外
{ name: '小张', lat: 31.2308, lng: 121.4740 },   // 距离 45 米,在范围内
]

// 打卡检测
function checkIn(employee, target) {
const distance = calcDistance(employee.lat, employee.lng, target.lat, target.lng)
const success = distance <= target.radius

return {
    name: employee.name,
    distance: Math.round(distance),
    status: success ? '✅ 打卡成功' : '❌ 不在范围内',
    distanceText: distance <= target.radius 
        ? `距离${Math.round(distance)}米,在范围内`
        : `距离${Math.round(distance)}米,超出范围`
}
}

// 执行打卡检测
console.log(`目标:${targetLocation.name},有效范围${targetLocation.radius}米\n`)
employees.forEach(emp => {
const result = checkIn(emp, targetLocation)
console.log(`${result.name}:${result.distanceText}`)
console.log(`状态:${result.status}\n`)
})

预期输出

目标:XX科技公司,有效范围200米

小王:距离107米,在范围内
状态:✅ 打卡成功

小李:距离647米,超出范围
状态:❌ 不在范围内

小张:距离45米,在范围内
状态:✅ 打卡成功

解释:这就是「考勤打卡」「到场检测」功能的核心逻辑。calcDistance 算距离,if (distance <= radius) 判断是否在范围内。在真实项目里,这个 checkIn 函数可以配合 uni.getLocation 做成实时打卡。


💪 进阶:常见坑 + 小贴士

坑 1:坐标系混用导致位置跑偏

// ❌ 错误:GPS 原生坐标直接给国内地图用
uni.getLocation({
type: 'wgs84',  // 国内地图不支持!
success: (res) => {
    this.lat = res.latitude
    this.lng = res.longitude
    // 地图上显示会偏 300-500 米
}
})

// ✅ 正确:国内必须用 gcj02
uni.getLocation({
type: 'gcj02',
success: (res) => {
    this.lat = res.latitude
    this.lng = res.longitude
}
})

坑 2:markers 里的坐标写错

// ❌ 错误:latitude 和 longitude 写反了
markers: [{
latitude: 121.473701,  // 经度当纬度用了!
longitude: 31.230416,  // 纬度当经度用了!
}]

// ✅ 正确:先纬度,后经度(北纬 31 度,东经 121 度)
markers: [{
latitude: 31.230416,   // 纬度
longitude: 121.473701, // 经度
}]

坑 3:位置授权被拒绝后没处理

// ❌ 错误:不处理 fail 情况
uni.getLocation({
type: 'gcj02',
success: (res) => { /* ... */ }
// 用户点了「拒绝」,没有任何反馈
})

// ✅ 正确:引导用户开启权限
uni.getLocation({
type: 'gcj02',
success: (res) => { /* ... */ },
fail: () => {
    uni.showModal({
        title: '提示',
        content: '需要位置权限才能使用此功能,去设置开启?',
        success: (res) => {
            if (res.confirm) {
                uni.openSetting()
            }
        }
    })
}
})

坑 4:距离计算用错公式(平面直角 vs 球面)

// ❌ 错误:把经纬度当平面坐标算距离
const distance = Math.sqrt(
(lat2 - lat1) ** 2 + (lng2 - lng1) ** 2
)
// 经纬度 1 度对应的实际距离在赤道是 111km,在高纬度只有 50km
// 这个算法误差可以达到 50% 以上!

// ✅ 正确:用球面公式(见前面的 calcDistance 函数)

性能小贴士:减少地图重绘

如果你的 markers 列表很长,每次更新只改了一个点,不要整个数组替换:

// ❌ 低效:每次更新都创建新数组,触发整个地图重绘
this.markers = newMarkersList

// ✅ 高效:只更新变化的点,用 this.$set
const index = this.markers.findIndex(m => m.id === targetId)
if (index !== -1) {
this.$set(this.markers, index, updatedMarker)
}

调试技巧:先打印坐标确认数据

地图问题 80% 是坐标数据的问题。画地图之前先确认数据是对的:

uni.getLocation({
type: 'gcj02',
success: (res) => {
    console.log('获取到的坐标:', {
        lat: res.latitude,
        lng: res.longitude
    })
    // 先看日志确认没问题,再画地图
    this.myLat = res.latitude
    this.myLng = res.longitude
}
})

✏️ 练习题

练习 1(2 分钟):改坐标
- 输入:把 calcDistance 函数里的两个点坐标改成「北京天安门」(39.9042, 116.4074)和「北京站」(39.9045, 116.4270)
- 预期输出:打印出两者距离(约 1.7 公里)
- 提示:直接把经纬度数字换掉就行

练习 2(3 分钟):加个判断
- 输入:在项目 1 的 getMyLocation 成功回调里,加一个 if 判断——如果精度(accuracy)大于 100,给用户提示「定位可能不准确」
- 预期输出:定位精度差的时候会弹出提示
- 提示:在 uni.showToast 之前加判断

练习 3(10 分钟):换个数据源
- 输入:用以下新数据代替项目 2 的餐厅数据,按距离重新排序

const newData = [
{ name: '北京烤鸭店', lat: 39.9085, lng: 116.3975 },
{ name: '故宫', lat: 39.9163, lng: 116.3972 },
{ name: '王府井', lat: 39.9146, lng: 116.4103 },
]
  • 预期输出:按距离排序后的列表
  • 提示:把新数据赋给 restaurants 变量,直接跑就行

练习 4(10 分钟):串起来
- 输入:把项目 2 的排序逻辑和项目 3 的打卡检测结合起来——先按距离排序,再判断哪些「在 500 米范围内」
- 预期输出:每个餐厅打印「距离 XXX 米,状态:在/不在 500 米范围内」
- 提示:复用一个 calcDistance,加一个 radius: 500 的判断

练习 5(5 分钟):找错
- 输入:以下代码运行后地图没显示标记点,找出问题并修复

// 问题代码
data() {
return {
    markers: [{
        latitude: '31.230416',  // 字符串,不是数字!
        longitude: '121.473701',
    }]
}
}
  • 预期输出:地图正确显示标记点
  • 提示:经纬度必须是数字类型,不是字符串

作业:做一个「周边兴趣点查询工具」

需求描述:做一个工具,输入一个地址名称(比如「复旦大学」),能查出附近 1 公里内的餐厅、酒店、超市三种类型的地点,并显示在地图上。

功能点
1. 点击按钮获取当前位置
2. 模拟 3 类兴趣点数据(餐厅、酒店、超市,每类 3-5 个点)
3. 按距离排序显示列表
4. 在地图上用不同颜色标记不同类型

加分项
1. 点击列表项,地图自动平移到那个点
2. 用圆形显示「1 公里搜索范围」

验收标准:能跑起来 + 有列表有地图 + 不同类型用不同颜色标记


📚 总结

本文学了 3 件事
1. uni.getLocation 获取设备当前位置
2. map 组件配合 markerscircles 在地图上标点画圈
3. 球面距离公式计算两点之间的距离

延伸资源
- uni-app 官方文档 - map 组件
- 微信小程序地图 API
- 高德地图 API 文档 - 真实项目里可以用高德 SDK 做周边搜索

互动钩子:你在项目里用过地图/定位功能吗?遇到过什么坑?评论区聊聊,老粉优先回复!


📌 下集预告:学完了地图和定位,下一章我们要学另一个「感知」能力——音视频。想象一下,一个 App 能知道「你说话的声音」和「你拍的照片」,下一章我们用 uni.createVideoContext 来做视频播放和录制……

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。