拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于uniapp+Vue3的露营App开发实践:多端适配、地图定位与弱网优化

简介基于uniapp与Vue框架开发的《露营》App完整项目面向希望学习跨端应用开发及前后台系统设计的开发者与学生也非常适合作为课程设计或毕业设计的参考方案。项目基于HBuilder X平台构建用户端实现了首页、露营信息、露营教程、个人中心等模块支持对内容进行查看、编辑、修改、删除操作管理后台包含个人中心、类型管理、用户管理、露营信息管理、露营教程管理、订单信息管理、系统管理等完整功能前后台业务衔接顺畅。压缩包共1015个文件大小约17.75MB其中Vue文件承载各业务页面JavaScript实现前端交互逻辑Java负责后台服务接口XML与properties提供配置支持png/jpg等图片资源丰富视觉效果CSS定义整体样式目录划分清晰适合直接导入HBuilder X运行调试。目前已有199人学习参考。资源提供完整前后端源码可直接用于二次开发能有效帮助开发者掌握跨端App从页面搭建、功能实现到后台管理的整套流程。1. 露营不是景区为什么这套App要落在uniappVue上露营App有个其他项目不常见的问题使用者在营地、停车场、山腰上网络和终端都不确定。常见做法是一套代码覆盖App端、微信小程序、H5否则运营成本直接翻三倍。uniapp加Vue3的组合能同时满足多端编译和组合式API的开发体验地图、定位、扫码、分享这些露营场景的高频能力都有现成封装不用像纯原生那样维护两套iOS和Android。这个项目核心难点不在页面而在多端一致性、定位权限和弱网兜底下面按这条线展开先搭骨架再做地图和清单最后处理发布和体验降级。2. 用Vue3工程化把露营App骨架搭起来营地浏览、装备清单、活动组队、个人中心是露营类App最常见的四个功能域。先把工程骨架立好页面路径、全局状态、请求层才不会在后期互相牵制。uniapp在Vue3版本下的工程化方式已经和常规Vite项目很接近但仍有几个编译层面的特殊点需要提前说清。2.1 从CLI创建项目到Vue3的目录结构创建项目有两条常见路径HBuilderX可视化新建或者用Vite模板命令行创建。习惯git工作流的团队我会优先选后者因为可以完整纳入代码审查和自动化流水线npx degit dcloudio/uni-preset-vue#vite my-camping-app cd my-camping-app npm install npm run dev:mp-weixinnpx degit拉取的是Vue3加Vite的官方presetdev:mp-weixin是持续编译微信小程序。如果主要目标平台是App和H5可以先跑npm run dev:h5在浏览器里把页面调通再处理打包。环境上注意Node版本别太旧Vite系工程对Node的要求比vue-cli时代高一个大版本装依赖前先跑node -v确认。创建后的目录需要按功能域重组我会在src下区分四块组件不全部丢进全局componentssrc/ ├── pages/ # 页面一个业务域一个目录 │ ├── index/ # 首页推荐营地与天气 │ ├── campsite/ # 营地列表、详情、地图 │ ├── gear/ # 装备清单 │ ├── activity/ # 活动与组队 │ └── user/ # 个人中心、登录、收藏 ├── store/ # Pinia ├── utils/ │ ├── request.js # uni.request封装 │ └── location.js # 定位封装 ├── static/ # 图片、tabBar图标 ├── pages.json ├── manifest.json └── main.jscomponents目录下会自动注册全局组件对首包体积有影响。布局、卡片、骨架屏这类跨页面复用的组件才放全局营地卡片、装备勾选行这种业务组件放在对应页面目录里按需引入。2.2 pages.json里一次定好五个tab与路由参数露营App的主导航用tabBar承载。我在pages.json里配置五个主页面这里有个新手高频报错点tabBar的图标路径必须指向static下的png且iconPath和selectedIconPath不能缺否则编译不报错但真机上tab不显示。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 露营 } }, { path: pages/campsite/list, style: { navigationBarTitleText: 营地 } }, { path: pages/gear/list, style: { navigationBarTitleText: 装备 } }, { path: pages/activity/index, style: { navigationBarTitleText: 活动 } }, { path: pages/user/index, style: { navigationBarTitleText: 我的 } } ], tabBar: { color: #9a9a9a, selectedColor: #2b6b45, backgroundColor: #ffffff, list: [ { pagePath: pages/index/index, text: 首页, iconPath: static/tab/home.png, selectedIconPath: static/tab/home-active.png }, { pagePath: pages/campsite/list, text: 营地, iconPath: static/tab/camp.png, selectedIconPath: static/tab/camp-active.png }, { pagePath: pages/gear/list, text: 装备, iconPath: static/tab/gear.png, selectedIconPath: static/tab/gear-active.png }, { pagePath: pages/activity/index, text: 活动, iconPath: static/tab/activity.png, selectedIconPath: static/tab/activity-active.png }, { pagePath: pages/user/index, text: 我的, iconPath: static/tab/user.png, selectedIconPath: static/tab/user-active.png } ] } }pages数组的第一项是启动首页。tabBar的pagePath必须和pages完全一致多一个斜杠都会导致编译后白屏。页面间跳转用uni.navigateTo({ url: /pages/campsite/detail?id1024 })目标页面在onLoad(options)里接收参数注意参数到达时都是字符串数值型要用Number(options.id)转换。这里就是vue路由参数在uniapp里的等价写法不要试图用vue-router那套去管理页面栈。2.3 用Pinia管理用户与营地收藏状态Vue3版本的uniapp推荐用Pinia先安装再在main.js里注册一次// main.js import { createSSRApp } from vue import { createPinia } from pinia import App from ./App.vue export function createApp() { const app createSSRApp(App) app.use(createPinia()) return { app } }营地收藏是典型的全局状态详情页收藏、列表页要排序、我的页面要展示收藏列表。这个状态用storage直接驱动UI会非常别扭刷新时机很难控制交给Pinia更顺// store/campsite.js import { defineStore } from pinia export const useCampsiteStore defineStore(campsite, { state: () ({ favorites: uni.getStorageSync(camp_favorites) || [] }), actions: { toggleFavorite(item) { const index this.favorites.findIndex((c) c.id item.id) if (index -1) { this.favorites.splice(index, 1) } else { this.favorites.unshift(item) } uni.setStorageSync(camp_favorites, this.favorites) } } })这里把storage读写收敛到store内部页面只依赖响应式数据收藏按钮能做到即时变色。从vue2转vue3的方法也在这个点体现vuex的this.$store.commit换成pinia的store.toggleFavorite(item)选项式API里this.$emit(event, payload)换成组合式API的emit(event, payload)但uniapp的全局事件uni.$emit和uni.$on跨页面通知不受影响活动报名成功后的页面提示仍然可以用。3. 地图定位、营地marker与装备清单三块硬功能怎么落地露营App区别于普通电商App的地方在于它依赖真实地理位置并且大量使用发生在离线或弱网环境。定位、地图渲染、本地存储这三块功能做扎实了App的基本形态才成立。3.1 封装定位逻辑与权限兜底定位是露营App的起点营地推荐和距离排序都依赖用户当前位置。uni.getLocation在App端和微信小程序端的行为不同小程序端必须先在manifest.json的mp-weixin节点配置位置接口描述否则会直接返回授权失败App端Android需要动态申请定位权限iOS则依赖info.plist里的用途说明。封装一层便于统一兜底// utils/location.js export function getLocation() { return new Promise((resolve, reject) { uni.getLocation({ type: gcj02, isHighAccuracy: true, highAccuracyExpireTime: 5000, success: resolve, fail: (err) { if (err.errMsg err.errMsg.includes(auth deny)) { uni.showModal({ title: 需要定位权限, content: 定位权限未开启无法展示周边营地, confirmText: 去设置, success: (res) { if (res.confirm) uni.openAppAuthorizeSetting() } }) } reject(err) } }) }) }gcj02是国测局坐标map组件和高德地图都接收这个坐标系。不要直接拿wgs84经纬度去渲染marker地图上会偏移几百米。isHighAccuracy在部分Android机型上是功耗开关露营场景对电量敏感拿到一次定位后最好缓存5分钟不要每次进页面都走高精度链路。调用侧放在页面onLoad里import { getLocation } from /utils/location export default { data() { return { lat: 30.2741, lng: 120.1551 } }, async onLoad() { try { const { latitude, longitude } await getLocation() this.lat latitude this.lng longitude } catch (e) { // 降级使用默认城市 } } }3.2 营地列表的marker渲染与距离排序拿到坐标后把营地数据渲染到地图上uniapp内置的map组件即可实现。markers数组里的每条数据都有固定结构图标尺寸用宽高控制。template view classmap-wrap map :latitudecenter[0] :longitudecenter[1] :markersmarkerList :scale12 show-location markertaponMarkerTap / /view /template script setup import { computed } from vue const props defineProps({ campsites: { type: Array, default: () [] }, center: { type: Array, default: () [31.23, 121.47] } }) const markerList computed(() props.campsites.map((item, index) ({ id: index, latitude: item.lat, longitude: item.lng, title: item.name, iconPath: /static/map/camp-marker.png, width: 36, height: 44, callout: { content: ${item.name}\n${item.distanceText}, display: BYCLICK, borderRadius: 8, padding: 8 } })) ) /scriptmarker的id只需要在当前列表内唯一markertap事件的e.detail.markerId拿到的就是这个值。再用它去原数组查详情跳转时把id拼进路由参数。callout.display用BYCLICK露营地扎堆的时候别用ALWAYS否则地图上全是名字缩放和拖动都很卡。距离排序用haversine公式不用调接口// utils/distance.js export function haversine(lat1, lng1, lat2, lng2) { const rad (d) (d * Math.PI) / 180 const R 6371 const dLat rad(lat2 - lat1) const dLng rad(lng2 - lng1) const a Math.sin(dLat / 2) ** 2 Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dLng / 2) ** 2 return 2 * R * Math.asin(Math.sqrt(a)) }R6371是地球半径公里数返回结果也是公里。排序时把数值格式化成1.2km塞进callout的content。营地点位超过两千条时别在前端全量遍历接口做空间索引只返回当前视口内的数据前端按scale变化重新拉取。3.3 装备清单的本地持久化装备清单是露营场景里典型的离线工具出发前在营地添加物品、打勾确认整个过程网络可能极差。直接用uni.setStorageSync就能满足// 添加装备 function addGear(name, count 1) { const list uni.getStorageSync(gear_list) || [] list.push({ id: Date.now(), name, count, packed: false }) uni.setStorageSync(gear_list, list) } // 勾选已装包 function togglePacked(id) { const list uni.getStorageSync(gear_list) || [] const target list.find((g) g.id id) if (target) target.packed !target.packed uni.setStorageSync(gear_list, list) }setStorageSync全程同步执行装备清单的数据量不大时没有任何问题但别在循环里反复调用set批量修改后整体set一次。页面在onShow里重新读取storage保证每次从其他页面返回时勾选状态都是最新的。存储方式同步/异步适用场景注意点uni.setStorageSync同步装备清单、用户偏好单次写入避开大体积数据uni.setStorage异步批量写入、缓存接口响应写入失败要catchplus.sqlite异步离线营地点位、本地轨迹仅App端可用需要原生插件4. manifest配置、条件编译与安卓iOS打包对照功能开发完真正决定App能不能上线的是manifest配置和打包链路。uniapp项目在manifest.json里集中管理应用标识、原生模块和第三方SDK。4.1 manifest.json里决定生死的几项配置manifest.json是uniapp的中枢配置打包App、编译H5、关联微信小程序都靠它。露营App里最容易踩坑的是下面几项配置路径必须值用途appidDCloud申请的appid云打包和真机运行的标识app-plus.modulesMaps、Push、Share原生模块不打勾则调用直接失败app-plus.distribute.sdkConfigs.geolocation高德或腾讯地图keyApp端定位和地图SDKmp-weixin.appid微信小程序appid微信开发者工具关联app-plus.privacy隐私协议链接、弹窗文案安卓应用市场上架前置以Android端Maps模块为例manifest.json里的app-plus.modules需要显式声明{ app-plus: { modules: { Maps: {}, Push: {}, Share: {} }, distribute: { sdkConfigs: { geolocation: { system: true, android: { provider: amap } } } } } }Maps模块没勾选真机上调用uni.getLocation会报module not found这类错误在模拟器上不一定复现。geolocation里system: true表示使用系统定位也可以指定provider为amap或tencent选哪家要和你地图组件用的key保持一致混用会导致定位成功但地图偏转。4.2 条件编译把App端和微信小程序的差异关进括号露营App最常见的两端差异是分享。App端用系统分享面板微信小程序端必须用原生转发按钮。条件编译可以直接写在模板、脚本和样式中template view classcamp-detail !-- #ifdef MP-WEIXIN -- button open-typeshare分享给驴友/button !-- #endif -- !-- #ifdef APP-PLUS -- button clickonAppShare系统分享/button !-- #endif -- /view /template script export default { data() { return { campId: } }, // #ifdef MP-WEIXIN onShareAppMessage() { return { title: 这个营地太棒了, path: /pages/campsite/detail?id this.campId } }, // #endif methods: { // #ifdef APP-PLUS onAppShare() { uni.share({ provider: weixin, scene: WXSceneSession, title: 这个营地太棒了, summary: 周末一起露营, href: https://example.com/camp/ this.campId }) } // #endif } } /scriptMP-WEIXIN对应微信小程序APP-PLUS对应App端H5对应浏览器。条件编译块内的代码只参与对应平台构建所以平台私有API可以放心写在注释里。样式同理小程序端的底部安全区适配和App端写法不同/* #ifdef MP-WEIXIN */ .safe-bottom { padding-bottom: constant(safe-area-inset-bottom); } /* #endif */ /* #ifdef APP-PLUS */ .safe-bottom { padding-bottom: env(safe-area-inset-bottom); } /* #endif */这种写法比在js里判断平台更干净编译时直接裁掉不需要的代码不会把平台判断逻辑带进生产包。4.3 Android与iOS打包流程对照uniapp的主流打包路径是云打包不需要本机装Android Studio或Xcode。Android和iOS的差异集中在证书和签名处理项AndroidiOS签名证书keystore.jks.p12证书 .mobileprovision描述文件产物apk / aabipa真机安装直接装apk需先配udid走TestFlight上架前隐私政策、权限说明App Store Connect填写隐私条款云打包Android时keystore要自己生成并保管好丢失后无法更新已上架的App包名签名。国内安卓应用市场目前主要收apk但Google Play要求aab。iOS侧描述文件必须绑定测试设备udid否则真机装不上证书过期是所有flutter、uniapp开发者都遇到过的打包一时爽续期火葬场问题。打包完成后用真机测试定位、扫码、地图在模拟器上的行为和真机差异太大。5. 露营场景的弱网降级与分享获客技巧5.1 请求超时重试与缓存兜底营地的网络状况决定请求层必须做超时和重试。uni.request默认超时时间偏短需要显示指定function requestWithRetry(options, retry 2) { return new Promise((resolve, reject) { uni.request({ ...options, timeout: 10000, success: resolve, fail: (err) { if (retry 0) { setTimeout(() { requestWithRetry(options, retry - 1).then(resolve).catch(reject) }, 1500) } else { reject(err) } } }) }) }timeout单位是毫秒山区或停车场场景的网络rtt可能超过2秒10秒是底线。重试间隔1.5秒连续失败就提示用户不要无上限重试。缓存兜底的做法是页面加载先读uni.getStorageSync里的上次成功数据渲染再发请求刷新成功则覆盖缓存失败则保留旧数据并展示离线模式标记。营地列表、装备清单、收藏列表这三块最适合这个策略。5.2 扫码签到与自定义分享好友露营活动的现场签到用uni.scanCode实现function scanCheckIn() { uni.scanCode({ onlyFromCamera: true, success: (res) { const result res.result checkInApi(result).then(() { uni.showToast({ title: 签到成功, icon: success }) }) } }) }onlyFromCamera设为true避免弱光下相册扫码误读。分享好友的入口则在小程序端重写onShareAppMessage把分享文案做成离你最近的营地卡片上带距离和名称。太泛的文案在微信里没人点具体的地点加距离才是露营信息传播的驱动力。两个动作串起来之后活动组织者现场打印二维码参与者扫码签到签到成功后自动生成带昵称和距离的分享卡片转发到微信群就完成了活动传播整个链路不依赖额外的地推物料转发里天然带着签到人的社交关系。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门