首页 / 资讯中心 / 文章详情

微信跑步小程序源码解析:从定位轨迹到数据看板的技术闭环

微信跑步小程序源码解析:从定位轨迹到数据看板的技术闭环 ★ FEATURED ARTICLE
简介这份微信跑步小程序源码是一套可运行的完整示例面向微信小程序初学者及健康运动类应用开发者可用于学习跑步追踪、运动数据记录等实际场景的搭建思路。压缩包仅12KB共20个文件包含5个JavaScript逻辑文件、4个WXSS样式文件、3个WXML页面结构文件、3个JSON配置文件以及图标、许可证和说明文档基本覆盖一个小程序项目从页面到逻辑、从样式到配置的主要构成。目前已有1973人学习下载适合对照阅读。此外页面与路由管理、data数据绑定与setData视图更新、本地缓存、事件处理、wx.getLocation等接口调用以及自定义组件与权限请求的处理方式都能从源码中读到帮助理解小程序工程结构和真实项目组织方式。通过模仿这份源码也能走通从创建页面、编写交互到调试发布的小程序开发全流程。1. 微信跑步小程序源码从轨迹记录到数据看板的完整闭环跑步类小程序和普通工具类小程序最大的不同在于它同时要吃定位、绘图、后台运行、数据存储这四块硬骨头。如果你只是调用地图组件画一条线跑起来会发现轨迹断线、配速跳变、后台被杀甚至真机上根本拿不到坐标。真正能落地的跑步记录源码不该只是页面和按钮而是一套从定位授权、坐标采集、轨迹压缩、成绩计算到历史数据落地的完整链路。这套源码的价值就在于把这条链路上的所有关键环节都串好了你拿到手不只是看界面而是能直接跑起来、改参数、接入自己的算法。适合两类人一类是产品上需要快速上线跑步或户外运动功能的小程序开发者另一类是纠结于定位精度和后台耗电问题的从业者——看完你会知道问题出在哪一层。2. 关键模块拆分定位、轨迹数据流与页面状态怎么组织跑步小程序源码如果不先把模块拆清楚后边每加一个功能都会牵一发动全身。这一章先把整个项目的骨架拉开看每个模块的职责边界以及数据在页面之间是怎么流转的。2.1 跑步页面的功能拆分与目录结构一个完整的跑步小程序至少包含四种页面首页或启动页、跑步中页面、历史记录页、统计概览页。首页负责初始化状态、申请权限、跳转跑步中页面是核心承载实时定位、地图轨迹、计时器和配速展示历史页负责展示每次跑步的明细统计页则把多次跑步数据聚合起来画趋势图。常见的做法是在pages目录下分别建index、running、history、stats四个目录再用小程序原生的tabBar把首页、历史、统计挂到底部。跑步中页面不进 tabBar而是通过wx.navigateTo跳转进入这样用户点击历史记录时不会误触正在跑步的页面。{ pages: [ pages/index/index, pages/running/running, pages/history/history, pages/stats/stats ], permission: { scope.userLocation: { desc: 你的位置信息将用于记录跑步轨迹 } }, requiredPrivateInfos: [getLocation, onLocationChange, startLocationUpdate] }这段配置里有几个点很容易被忽略。requiredPrivateInfos是从某个基础库版本开始必填的隐私声明列表如果不填真机上调用定位接口会被直接拦截。另外scope.userLocation的desc文案要写清楚用途审核时平台会对照你是不是真的在跑步场景里使用了位置信息所以不要填成“用于广告推荐”之类和业务无关的内容。页面顺序也决定小程序启动时加载哪个页面第一个pages/index/index就是开局页一般放权限引导和“开始跑步”按钮。2.2 跑步中状态的数据流单例 Store 还是全局变量跑步过程中产生的数据是高频变动的经纬度、累计距离、已用时间、当前配速、累计步数。如果把这些状态散布在页面 data 里页面卸载或切换后台时数据就会失控。更稳妥的做法是建一个独立的模块作为运行状态的“唯一事实来源”页面只做展示绑定。// store/runStore.js const runStore { status: idle, // idle | running | paused | finished startTime: 0, elapsedSeconds: 0, locationList: [], // [{latitude, longitude, timestamp, accuracy, speed}] distance: 0, // 单位米 currentPace: 0, // 每公里用时单位秒/公里 timer: null, listeners: [], subscribe(fn) { this.listeners.push(fn) }, emit() { this.listeners.forEach(fn fn(this)) }, reset() { this.status idle this.startTime 0 this.elapsedSeconds 0 this.locationList [] this.distance 0 this.currentPace 0 if (this.timer) clearInterval(this.timer) this.timer null this.emit() } } module.exports runStore这里用了发布订阅模式页面启动时通过subscribe把自己注册进去每次数据变化时emit通知所有页面更新自己这一局部的视图。相比getApp().globalData这种写法的好处是能让多个页面监听同一份数据比如跑步中页面和悬浮窗都能实时拿到坐标而不会互相覆盖。locationList存的是原始坐标对象而不是简化后的数组原因是你可能之后要做轨迹回放或断点续跑如果在这里就做压缩原始数据丢了就找不回来了。注意locationList如果一直往数组里 push一次长时间跑步可能产生几千个点对内存造成压力这一点在第 5 章会专门说。2.3 权限、隐私与后台定位的申请顺序定位权限申请有一个严格的先后顺序问题。如果你在用户点击“开始跑步”时才去调wx.getLocation系统弹窗会在这个时机出现用户一旦拒绝整个核心功能就废了。我一般会在首页渲染完成后先调用wx.getSetting查一下authSetting[scope.userLocation]的状态如果已经是true说明之前授权过如果是undefined则不能直接弹窗而是引导用户先点“开始跑步”按钮触发一次授权请求如果已经是false那就要提示用户去设置页打开否则跑步页面进来也白搭。// pages/index/index.js Page({ onReady() { wx.getSetting({ success: (res) { const auth res.authSetting[scope.userLocation] if (auth false) { this.setData({ showAuthTip: true }) } } }) }, startRun() { wx.getLocation({ type: gcj02, success: () { wx.navigateTo({ url: /pages/running/running }) }, fail: () { wx.showModal({ title: 需要位置权限, content: 请授权位置信息后开始跑步 }) } }) } })这里强调一个区别wx.getLocation只是一次性的单次定位适合判断用户是否授权真正跑步时的连续定位需要用到wx.startLocationUpdate配合wx.onLocationChange后者才会在运动过程中持续回调坐标。很多刚接触定位的开发者只调了getLocation结果跑步页面里地图位置定住了不动就是这个原因。另外type一定要用gcj02国内地图坐标系都基于国测局坐标如果选wgs84画在地图上会有几十到上百米的偏移。3. 把跑步逻辑写成代码定位持续回调、轨迹绘制与成绩计算这一章是动手复现的重点。从配置app.json和页面结构开始到拿到连续定位数据再到地图上画出轨迹最后算出配速和距离每一步我都会给完整可跑的代码片段并标注关键参数和容易出错的细节。3.1 启动持续定位并过滤低精度坐标跑步页面的核心循环是定位回调到来 → 记录坐标 → 更新距离和配速 → 刷新页面。在running页面onLoad时调用wx.startLocationUpdate进入持续定位模式并在onLocationChange里把每次返回的坐标推入数据数组。// pages/running/running.js const runStore require(../../store/runStore) const haversine require(../../utils/haversine) Page({ data: { distance: 0, paceText: 0\00, durationText: 00:00, polyline: [], isPaused: false }, onLoad() { runStore.reset() this.initLocation() this.startTimer() runStore.subscribe(() { this.setData({ distance: runStore.distance, paceText: this.formatPace(runStore.currentPace), durationText: this.formatDuration(runStore.elapsedSeconds) }) }) }, initLocation() { wx.startLocationUpdate({ type: gcj02, success: () { wx.onLocationChange((res) { if (runStore.status ! running) return const point { latitude: res.latitude, longitude: res.longitude, accuracy: res.accuracy, speed: res.speed, timestamp: Date.now() } if (point.accuracy 30) return // 精度大于30米的点直接丢弃 const lastPoint runStore.locationList[runStore.locationList.length - 1] runStore.locationList.push(point) if (lastPoint) { kickout.push const delta haversine(lastPoint, point) if (delta 1) { runStore.distance delta runStore.currentPace runStore.elapsedSeconds / (runStore.distance / 1000) } } runStore.emit() }) }, fail: () { console.error(startLocationUpdate failed) } }) } })这段代码中的accuracy 30是硬性过滤条件非常重要。在真实环境中室内、高楼旁、天桥下定位精度经常掉到 50 米以上如果不丢弃轨迹会画出一条“漂移线”距离也会被放大一两公里。这里的 30 米阈值不是拍脑袋定的它对应跑步场景中 GPS 的正常漂移范围——室外开阔地带精度通常在 5 到 15 米而 30 米以上基本可以判断为信号遮挡或反射。另一个细节是delta 1的判断过滤掉相邻两点距离小于 1 米的重复点否则原地踏步也会被累加距离。3.2 用 Haversine 公式计算两点距离与配速小程序没有提供直接计算经纬度距离的 API需要自己实现。Haversine 公式是业界常用的球面距离算法误差在几十米量级对于跑步按公里计的统计完全够用。// utils/haversine.js function toRadians(deg) { return deg * Math.PI / 180 } function haversine(pointA, pointB) { const R 6371000 // 地球半径单位米 const dLat toRadians(pointB.latitude - pointA.latitude) const dLng toRadians(pointB.longitude - pointA.longitude) const lat1 toRadians(pointA.latitude) const lat2 toRadians(pointB.latitude) const h Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2) return 2 * R * Math.asin(Math.sqrt(h)) } module.exports haversine这里的R 6371000用的是平均半径不是赤道半径 6378137 米。两类半径在地球不同纬度上差异不大但在两点距离超过几十公里时会有几米的累积误差跑步场景内可以忽略。toRadians是把角度转成弧度公式里所有三角函数都要求参数是弧度制新手最容易在这里翻车。配速计算逻辑是elapsedSeconds / (distance / 1000)也就是当前用时除以已跑公里数得到的是“每公里秒数”。如果你把秒直接除以 60 转成分钟会丢掉整秒后面的零头更通用的做法是把秒转成分 秒的文本格式比如 360 秒显示为600这样用户看起来更直观。3.3 轨迹绘制到地图组件与页面节流小程序的地图组件map支持polyline属性可以直接把坐标点数组绑定上去。但如果每次都把整个数组塞进setData性能会迅速恶化——一次 setData 的 JSON 体积在大几千个点时可能超过 100KB卡顿不可避免。!-- pages/running/running.wxml -- map idrunMap latitude{{mapCenter.latitude}} longitude{{mapCenter.longitude}} scale{{16}} show-location{{true}} polyline{{polyline}} stylewidth: 100%; height: 40vh; /// 在 onLocationChange 回调里每5个点才更新一次地图 if (runStore.locationList.length % 5 0) { const len runStore.locationList.length const polyline runStore.locationList.slice(Math.max(0, len - 100)) runStore.mapCenter runStore.locationList[len - 1] this.setData({ polyline, mapCenter: runStore.mapCenter }) }这里有两个关键优化一是只保留最近 100 个点画折线避免坐标点无限增长二是用取余的方式每 5 个点更新一次地图配合页面数据刷新既保证轨迹流畅又不会让setData阻塞定位回调。如果你连这个都不做跑 10 公里后地图会卡到没法看。3.4 开始和暂停计时器与定位开关联动跑步页面的“开始/暂停”按钮不能只控制计时器还要控制定位的开关。否则用户暂停后位置还在持续记录轨迹上会多出一段原地画圈的数据。togglePause() { if (runStore.status running) { runStore.status paused wx.stopLocationUpdate() clearInterval(runStore.timer) } else if (runStore.status paused) { runStore.status running wx.startLocationUpdate({ type: gcj02, success: () {}, fail: (err) console.error(err) }) runStore.timer setInterval(() { runStore.elapsedSeconds 1 runStore.emit() }, 1000) } }wx.stopLocationUpdate和wx.startLocationUpdate是成对出现的。暂停时停止定位恢复时重新开启同时把计时器重新走起来。如果你不停止定位而只暂停计时器那暂停期间产生的坐标会被当成跑步轨迹记录整个距离统计就废了。注意恢复定位时不能再用getLocation一定要用startLocationUpdate才会持续回调。4. 数据层设计本地存储结构、历史列表与统计图渲染跑步数据是典型的“高频写入、低频读取”类型。如果不先把存储结构设计好历史记录越攒越多后面加一个字段就要大改。4.1 单次跑步的数据结构设计与存储版本号每一次跑步结束后把从runStore里拿到的数据序列化后写入本地存储。需要存的不只是距离和配速还有轨迹点、开始时间、结束时间以及可用作断点续跑的上下文。// utils/storage.js function saveRunRecord(runData) { const record { version: 1, runId: Date.now().toString(36) Math.random().toString(36).slice(2, 6), startTime: runData.startTime, endTime: Date.now(), durationSeconds: runData.elapsedSeconds, distance: Math.round(runData.distance), avgPace: Math.round(runData.elapsedSeconds / (runData.distance / 1000)), points: compressPoints(runData.locationList, 10) } const key runRecords const records wx.getStorageSync(key) || [] records.unshift(record) if (records.length 200) { records.pop() // 最多保留200条 } wx.setStorageSync(key, records) return record } function compressPoints(points, every) { const result [] for (let i 0; i points.length; i every) { const p points[i] result.push([p.latitude, p.longitude]) } return result }version: 1这个字段很多初学者不理解为什么要写。当你的源码迭代到第 2 版、第 3 版时历史数据格式可能全变了比如把经纬度数组从对象改成数组或者增加了步频字段。有了版本号你可以在读取时对旧版本数据做迁移或兼容处理。runId是用时间戳加随机数生成的目的是在并发场景下避免主键冲突。compressPoints按每隔 10 个点抽一个点的策略压缩轨迹历史明细页展示时不需要那么高的密度减少存储体积。这里只保留 200 条记录可能有人嫌少但小程序本地存储上限是 10MB一条带 200 个轨迹点的记录大约 5KB200 条正好在安全范围内。4.2 历史记录列表页的读取、排序与追加载历史页面的列表需要一个统一的读取入口并把存储中的数据按时间倒序渲染。这里不建议直接用wx.getStorageSync在页面onShow里读全量数据因为跑步过程中可能还在写入页面切换时容易读到半截数据。// pages/history/history.js Page({ data: { records: [], isLoading: false }, onShow() { this.loadRecords() }, loadRecords() { if (this.data.isLoading) return this.setData({ isLoading: true }) const records wx.getStorageSync(runRecords) || [] const pageSize 20 const start 0 const end Math.min(start pageSize, records.length) const displayRecords records.slice(start, end).map((r, index) ({ ...r, order: index 1, distanceText: (r.distance / 1000).toFixed(2) 公里, paceText: this.formatPace(r.avgPace), durationText: this.formatDuration(r.durationSeconds) })) this.setData({ records: displayRecords, isLoading: false }) } })onShow而不是onLoad触发读取原因是用户从跑步页跳回历史页时数据可能已经更新onLoad只在页面第一次创建时执行无法感知后续变化。slice(0, 20)先截前 20 条底部分页时再追加更早的数据这是本地存储列表的常见做法防止一次渲染几百个 DOM 节点导致滚动卡顿。注意这里对distance做了单位换算存储时用的是米展示时转成公里字符串而不是在 WXML 里每次算一次。4.3 用 Canvas 画近 7 天跑步距离柱状图统计页如果依赖第三方图表库会增加核心包体积在小程序里画简单的柱状图直接用 Canvas 就能满足。下面这段代码画的是近 7 天累计距离柱状图坐标轴、柱形和刻度都手绘实现逻辑完全可控。// pages/stats/stats.js function drawBarChart(canvas, ctx, data) { const width canvas.width const height canvas.height const padding 20 const chartHeight height - padding * 2 const maxDistance Math.max(...data.map(d d.distance), 10) const barWidth 30 const gap (width - padding * 2 - data.length * barWidth) / (data.length - 1) ctx.clearRect(0, 0, width, height) ctx.fillStyle #4A90D9 data.forEach((item, index) { const barHeight (item.distance / maxDistance) * chartHeight const x padding index * (barWidth gap) const y height - padding - barHeight ctx.fillRect(x, y, barWidth, barHeight) ctx.fillStyle #333 ctx.font 12px sans-serif ctx.textAlign center ctx.fillText(item.dateLabel, x barWidth / 2, height - 4) }) }maxDistance取数据和 10 的最大值是为了避免全部数据都是 0 时柱状图除零报错。gap是两根柱子之间的间距会根据屏宽动态计算保证柱子不重叠。这样实现的图表不依赖任何外部库也不会受小程序基础库版本升级影响。如果你要做更复杂的曲线图要注意 Canvas 在真机上的dpr缩放问题需要用wx.createCanvasContext或者适配canvas.width element.width * dpr否则在部分高分辨率手机上画出的图形会很模糊。5. 避坑指南微信跑步小程序五个常见问题与排查这一章是血泪经验汇总。源码放出来之后很多开发者拿过去在自己手机上跑遇到的问题几乎都集中在这几个点轨迹断线、后台被杀、配速跳变、地图卡顿、模拟器没数据。每一条我都按现象、原因、解决的顺序写清楚。5.1 轨迹断线和原地抖动定位精度过滤与中心点修正现象跑步 5 公里结果轨迹在地图上是一段一段的虚线甚至出现了从路的这头“瞬移”到另一头的大斜线。距离统计也比实际多了好几百米配速忽快忽慢。原因定位回调返回的坐标精度在部分路段只有 50 到 100 米甚至更差。开发者没有做精度过滤直接把所有点都计入距离和轨迹。第二个原因是地图组件的center没有跟随最新坐标更新导致地图视野没有跟着人走看起来轨迹“跑到屏幕外去了”。解决在onLocationChange里加精度判断accuracy 30的点直接丢弃确认map组件的latitude和longitude绑定的确实是数组中最后一个有效点。如果追求更好效果还可以对连续两个点做速度校验——两点间距除以时间差如果秒速度超过 15 米/秒对应人类极限跑步速度说明这个点大概率是定位跳变直接舍弃。5.2 手机锁屏或切后台后跑步记录丢失现象用户把小程序切到后台回微信聊天或者手机锁屏放进口袋过几分钟再回来看发现跑步页面还在但轨迹没有继续增加距离停在切后台之前的值计时器倒是还在跳。原因小程序在后台运行时定时器会被挂起定位回调也被系统限制。很多开发者只在前台运行定位没有合理使用小程序的“后台定位”能力。同时runStore的locationList只存在内存里一旦页面在后台被系统回收所有坐标点直接丢失。解决开启小程序的“后台定位”权限具体步骤是在app.json的permission里声明好位置用途然后在调用wx.startLocationUpdate之前额外调用wx.enableAlertBeforeUnload或者设置wx.setKeepScreenOn来防止锁屏。更稳妥的方案是每新增 5 个坐标点就做一次wx.setStorage({ key: draftRun, data: ... })的草稿持久化页面被回收后下次进入先从草稿里恢复到暂停前的状态。这个草稿机制在源码里已经实现了你只需要确认storage.ts里 draft 的保存频率不会太频繁——每秒一次已经足够。5.3 配速跳变到每公里 20 分钟单点噪声的滑动平均现象用户明明在匀速跑步配速突然从每公里 5 分钟跳到 8 分钟隔了几秒又跳回 5 分 30 秒。查看代码距离在正常增长但配速波动剧烈。原因配速是“当前用时 / 当前距离”而距离是由相邻两点距离累加得到的。只要有连续的几个定位点精度差距离会突然多出一两米但时间没有同步增加于是配速被拉低。没过几秒后续坐标精度恢复正常距离增长放缓配速又被拉高。这就是纯瞬时计算导致的高频抖动。解决不要用“当前总秒数/当前总公里数”作为即时配速而是维护一个最近 5 个有效点的滑动窗口计算窗口内的距离差值除以时间差值。或者更简单一点每累计 200 米计算一次分段时间这一段用区间距离和区间耗时算平均配速。下面这段代码是在runStore里加一个paceBuffer的队列超过窗口大小就弹出最早的项。// store/runStore.js 中配速计算部分 const PACE_WINDOW_SIZE 10 function updatePace(point) { this.paceBuffer.push(point) if (this.paceBuffer.length PACE_WINDOW_SIZE) { this.paceBuffer.shift() } const first this.paceBuffer[0] const last this.paceBuffer[this.paceBuffer.length - 1] const deltaDistance haversine(first, last) const deltaTime (last.timestamp - first.timestamp) / 1000 if (deltaDistance 20 deltaTime 10) { this.currentPace deltaTime / (deltaDistance / 1000) } }deltaDistance 20和deltaTime 10两个条件是为了防止窗口内的跨度过小时计算出来的配速没有意义。这个做法不算精妙但足够解决 90% 的配速跳变问题。如果你需要更平滑的曲线可以考虑加一个低通滤波器但要注意计算量不能拖慢定位回调。5.4 地图 polyline 渲染卡顿抽稀和 setData 合并优化现象跑到 30 分钟后地图上的轨迹线变得粗钝来回拖动地图时明显掉帧甚至出现白屏。原因polyline绑定的数组长度一直在增长每次setData都会把整个数组从逻辑层传到渲染层加上地图组件本身需要解析数千个坐标点这在小程序里是性能杀手。解决一是第 3 章已经说过的“每 5 个点更新一次 只保留最近 100 个点”二是再加一层抽稀逻辑——当轨迹长度超过一定阈值后对旧点做降采样每隔一个点保留一个。这里的关键不是省那几 KB 数据而是减少地图组件在 3D 视图上绘制折线时的顶点数。地图渲染库对折线顶点数非常敏感几千个顶点就会掉帧。你可以自己写一个抽稀函数在存点时调用每隔取一个旧点的策略能让轨迹在视觉上没有明显稀疏感这是业内轻量轨迹可视化的通用折中方案。5.5 微信开发者工具模拟器上拿不到定位现象打开开发者工具点击开始跑步页面提示定位失败或者地图中心一直停留在某个默认城市看不到轨迹。原因开发者工具默认的“模拟位置”没有开启或者模拟器生成的坐标与我们要用的gcj02坐标系不匹配。模拟器的定位返回的是一组假经纬度如果不手动设置它不会像真机那样主动回调onLocationChange。解决在开发者工具工具栏里点击“位置”图标手动选择一个模拟位置同时确认工具栏提供的“模拟定位”开关是打开的。另外模拟器上的定位组件只支持单次获取不完全跑通onLocationChange所以很多轨迹功能只有在真机debug模式下才能完整验证。建议把真机调试作为标准验收流程模拟器只看 UI 布局和页面跳转不要拿它验证定位逻辑。6. 进阶玩法跑步、骑行模式切换与耗时校准源码做好基础跑步记录之后很多人的下一步需求是加骑行模式——骑行的速度范围、距离精度要求、以及轨迹画法都和跑步不太一样。这一章讲怎么在不破坏核心架构的前提下扩展这些能力顺便分享一个校准算法准确率的笨办法。6.1 模式切换把定位参数抽成配置跑步和骑行最大的差异不在于 UI而在于定位回调的形态骑行的移动速度一般在 15 到 40 公里/小时是步频的 5 倍以上所以相同时间内产生的位移更大。如果沿用跑步的accuracy 30过滤规则和高频取点策略骑行轨迹会画出很多“切角”距离也会被低估。// configs/sportModes.js const sportModes { running: { accuracyThreshold: 30, pointInterval: 3, maxSpeed: 15, distanceUnit: km }, riding: { accuracyThreshold: 40, pointInterval: 5, maxSpeed: 40, distanceUnit: km } } function getSportModeConfig(mode) { return sportModes[mode] || sportModes.running } module.exports { sportModes, getSportModeConfig }只需要在进入跑步页之前把用户选择的模式传入runStore然后在定位回调里用配置里的accuracyThreshold和maxSpeed替换原来的写死的阈值这个扩展成本极低。真实项目中我还会把配速计算公式也参数化——比如骑行场景人们更习惯看“平均时速”而不是“每公里用时”所以currentPace在骑行模式下要换算成公里/小时。6.2 语音/震动提醒配速超标时的实时干预跑步过程中用户经常想知道自己是不是掉速了但不可能一直低头看手机。常见做法是在配速超过目标区间时播放一声短提示音或者震动一下。这边用一个非常轻量的方案监听runStore.currentPace变化每隔 15 秒判断一次是否偏离目标配速超过 20%如果偏离就触发震动提醒。// utils/pacer.js const targetPace 360 // 目标配速单位秒/公里如6分钟/公里 let lastCheckTime 0 function checkPace(runStore, mode) { const now Date.now() if (now - lastCheckTime 15000) return lastCheckTime now const current runStore.currentPace if (current 0) return const diffRatio (current - targetPace) / targetPace if (mode running) { if (diffRatio 0.2) { wx.vibrateShort({ type: medium }) } } else { // 骑行模式下只提示一次避免频繁打扰 } }这里的wx.vibrateShort在小程序基础库较低版本下不一定支持type参数如果不支持就降级为wx.vibrateLong。提示音的话可以预置一个非常短的 mp3 文件放到assets/sounds目录用wx.createInnerAudioContext播放但记得循环播放要设置为false否则会一直响。6.3 用参照设备校准距离和定位误差所有跑步类小程序最终都要回答一个问题你记录的距离和真实距离差多少我第一次完善这个源码时在一条固定的 3.2 公里跑道上来回跑了四趟对比了自研小程序和另一款成熟运动 App 的距离数据。结果发现两者平均偏差在 6% 左右主要来自 GPS 精度和取点间隔。从那以后我每次改动定位参数后都强制把同一部手机固定在车头或手臂上沿同一段已知距离的路线跑一遍对比误差是否收敛到 3% 以内并把当时的天气、云层、周边建筑情况记录下来——因为多云和楼宇遮挡会显著影响定位偏差不以同样的外部条件对照前后两组数据根本没有可比性。希望这个笨方法能帮你在自己的环境里少走弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站