news 2026/9/15 17:27:03

uniapp+Vue3实战:从0到1开发露营App完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uniapp+Vue3实战:从0到1开发露营App完整指南

简介:基于uni-app与Vue框架开发的《露营》App完整项目源码包,面向需要学习移动端与后台管理开发的初级、中级开发者。项目在HBuilder X平台下实现,分为用户前端和管理后台:前端覆盖首页、露营信息、露营教程、个人中心等模块,后台支持类型管理、用户管理、露营信息管理、订单信息管理及系统管理,并均可完成增删改查操作。压缩包共1015个文件,其中216个vue页面组件、139个js脚本、111个java接口类构成核心逻辑,配合231个png及162个svg处理界面视觉,整体大小约17.75MB,文件结构清晰,便于按模块查阅。已有199人学习浏览,适合作为课程设计、毕业设计或入门实战项目参考,有助于快速掌握uni-app跨端开发与后台协同的完整流程。

1. 露营这件小事,为什么用uniapp+Vue来写App

露营是这两年最具体的周末场景之一:找营地、查装备、约好友、看天气,每个需求单拎出来都不算复杂,但要把它们串成一个能用的 App,涉及页面路由、地图定位、多端打包一整套工程决策。用原生安卓写一遍,再给微信小程序重写一遍,成本直接翻倍,这正是 uniapp + Vue 组合存在的理由。

在这套技术栈里,Vue 负责组件化和状态组织,uniapp 把微信小程序、H5、安卓和 iOS 的差异收敛到同一套 API 后面。这篇文不是某个现成项目的说明书,而是顺着《露营》这类工具型 App 最常见的落地路径,把工程结构、核心功能、打包上架和真机验证讲清楚,哪怕你手上只有一个打包好的 zip 工程,也能拿它当排查地图用。

2. 露营App的Vue3页面骨架与pages.json路由配置

露营类 App 的页面通常不会太多:营地列表、营地详情、装备清单,再加一个个人中心。页面少反而要求把路由和目录一次定清楚,后面加“我的足迹”“路书”这类功能时,不用回头挪结构。这一章先把工程的骨架立住,再落到 Vue3 组合式 API 的页面写法上,顺带把 Vue2 老工程迁移时最容易踩的三个差异说透。

2.1 先分清 pages.json、manifest.json 和 static 的职责

uniapp 工程里,pages.json 管路由和导航栏配置,manifest.json 管应用级配置,static 目录放静态资源。新手最常犯的错是把页面跳转逻辑放在某个全局变量里,或者在 pages.json 里写业务字段。下表是这几个地方的职责边界,照着划分就不会乱:

文件/目录职责常见误用
src/pages/页面组件把业务组件也塞进 pages 目录,导致每个分包多编译一份
src/static/图片、字体、本地 JSON用字符串动态拼路径,小程序端编译后找不到资源
pages.json路由、tabBar、导航栏、分包在 style 里写 uniapp 不支持的字段,真机静默失效
manifest.jsonappid、模块权限、隐私声明改完不重新打包,以为热重载会带新配置
uni.scss全局 scss 变量页面里散落重复颜色值,换主题时逐个改

pages.json 是路由的唯一入口,下面的配置可以直接抄进工程:

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "营地列表" } }, { "path": "pages/checklist/checklist", "style": { "navigationBarTitleText": "装备清单" } }, { "path": "pages/detail/detail", "style": { "navigationBarTitleText": "营地详情" } } ], "globalStyle": { "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#3A6B35", "backgroundColor": "#F6F7F8" }, "tabBar": { "color": "#8C8C8C", "selectedColor": "#3A6B35", "list": [ { "pagePath": "pages/index/index", "text": "找营地" }, { "pagePath": "pages/checklist/checklist", "text": "装备" } ] } }

pages 数组里第一个页面就是冷启动首页;tabBar 的 pagePath 必须同时存在于 pages 数组,否则编译阶段直接报错。navigationBarBackgroundColor 只接受十六进制色值,写成 rgb() 在安卓端会不生效。tabBar 列表建议控制在 2 到 5 项,超过 5 项在部分安卓机型上会挤压文字区域。

2.2 用Vue3 script setup写营地列表,uniapp路由传参一次说清

页面骨架定好后,列表页是最有代表性的写法:请求数据、搜索过滤、点击跳详情。下面这段覆盖了 uniapp 里 Vue3 组合式 API 的主要用法:

<template> <view class="camp-list"> <input v-model="keyword" placeholder="搜索营地名称" class="search" /> <view v-for="site in filteredList" :key="site.id" class="camp-item" @click="goDetail(site.id)" > <image class="cover" :src="site.cover" mode="aspectFill" /> <view class="body"> <text class="name">{{ site.name }}</text> <text class="price">¥{{ site.price }}/晚</text> </view> </view> <view v-if="loading" class="loading">加载中...</view> </view> </template> <script setup> import { ref, computed } from 'vue' import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app' const list = ref([]) const keyword = ref('') const loading = ref(false) const filteredList = computed(() => keyword.value ? list.value.filter((item) => item.name.includes(keyword.value)) : list.value ) onLoad(async () => { loading.value = true try { const res = await uni.request({ url: 'https://api.camp.example.com/sites', method: 'GET', data: { page: 1, size: 20 } }) list.value = res.data.list } catch (e) { uni.showToast({ title: '营地列表拉取失败', icon: 'none' }) } finally { loading.value = false } }) onPullDownRefresh(async () => { await loadSites() uni.stopPullDownRefresh() }) const goDetail = (id) => { uni.navigateTo({ url: `/pages/detail/detail?id=${id}` }) } </script>

onLoad 是 uniapp 的页面生命周期,来源是 @dcloudio/uni-app 而不是 vue 包;页面里同时存在 Vue 自身生命周期时,两者按各自语义执行,不要混写。filteredList 用 computed 缓存过滤结果,避免每次渲染都重算全量数组。onPullDownRefresh 要生效,必须在 pages.json 对应页面的 style 里加 "enablePullDownRefresh": true。跳详情用 navigateTo 拼接查询参数,详情页在 onLoad(options) 里取 options.id,这就是 uniapp 形态的路由参数传递——vue-router 的 query 在这里变成 URL 查询串,传对象时要先 JSON.stringify,接收端再 parse。

2.3 从Vue2迁到Vue3:main.js、全局属性和过滤器的差异

很多露营小程序的存量工程还是 Vue2 写法。uniapp 对 Vue3 的支持已经稳定,迁移时不必重写所有页面,把入口、全局属性和过滤器处理好就能跑通。三处改动的对照如下:

// Vue2 时代的 main.js import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false Vue.prototype.$toast = (msg) => uni.showToast({ title: msg }) new Vue({ render: (h) => h(App) }).$mount('#app')
// Vue3 时代的 main.js import { createSSRApp } from 'vue' import App from './App.vue' export function createApp() { const app = createSSRApp(App) app.config.globalProperties.$toast = (msg) => uni.showToast({ title: msg }) return { app } }

uniapp 的 Vue3 入口要求导出 createApp,内部用 createSSRApp 保证 App 端渲染正常,这个函数名不能换成 createApp 后直接挂载。Vue2 的 prototype 挂载改为 globalProperties,页面里通过 getCurrentInstance().appContext.config.globalProperties 取用。Vue3 移除了 filters,价格格式化这类逻辑改成 computed 或独立函数,模板里直接调用即可。迁移后如果遇到样式错位,优先检查是否用了 Vue2 时代的深度选择器 /deep/,统一改成 :deep() 语法。

3. 露营核心功能落地:uniapp定位、本地存储与请求API的实战写法

露营 App 的差异化在“现场感”:营地在地图上的位置、出发前要带的装备、目的地天气。这三块分别对应定位、本地存储和网络请求,也是 uniapp 跨端差异最明显的地方。只要有户外使用场景,就不能假设手机永远在线,所以这一章的代码都按弱网和权限受限的情况做了兜底。

3.1 map组件和uni.getLocation的坐标陷阱

地图页是露营 App 的入口级功能。uniapp 内置 map 组件在微信小程序和 App 端都能直接用,但坐标体系有一个必须处理的细节:定位类型要选 gcj02,也就是高德和腾讯地图使用的国测局坐标。如果拿到 wgs84 原始坐标直接丢给 map 组件,在真实户外会偏移几十米,室内看不出来,一到空旷营地就露馅。

<template> <map id="campMap" class="map" :latitude="center.lat" :longitude="center.lng" :markers="markers" :scale="12" @markertap="onMarkerTap" /> </template> <script setup> import { reactive, ref } from 'vue' import { onLoad } from '@dcloudio/uni-app' const center = reactive({ lat: 39.9042, lng: 116.4074 }) const markers = ref([]) const onMarkerTap = (e) => { const site = markers.value.find((m) => m.id === e.detail.markerId) if (site) uni.navigateTo({ url: `/pages/detail/detail?id=${site.id}` }) } onLoad(async () => { try { const loc = await uni.getLocation({ type: 'gcj02', isHighAccuracy: true, highAccuracyExpireTime: 4000 }) center.lat = loc.latitude center.lng = loc.longitude const res = await uni.request({ url: 'https://api.camp.example.com/nearby', data: { lat: center.lat, lng: center.lng, radius: 20000 } }) markers.value = res.data.map((s) => ({ id: s.id, latitude: s.lat, longitude: s.lng, title: s.name, iconPath: '/static/marker.png', width: 28, height: 34 })) } catch (e) { uni.showToast({ title: '定位失败,请检查权限', icon: 'none' }) } }) </script>

isHighAccuracy 和高精度超时只在部分安卓机型生效,低端机可能直接走普通定位,失败时要用上一次缓存的坐标做降级,而不是弹窗打断用户。markertap 事件的 detail.markerId 是数字,和构造 markers 时写入的 id 对应,不要拿数组索引去匹配。H5 端的 map 组件支持不完整,常见做法是用平台判断:小程序和 App 走内置 map,H5 引入腾讯地图 JS SDK 单独渲染。

提示:微信小程序端必须在 manifest.json 的 mp-weixin 节点里声明 requiredPrivateInfos: ["getLocation"],否则真机一进来就报 getLocation:fail the api need to be declared,这个报错直接搜错误文案就能定位到问题。

3.2 装备清单用uni.setStorageSync实现离线可用

营地的信号盲区是真实痛点,装备清单必须离线可用。uniapp 的本地存储 API 在四端行为一致,同步接口在小数据量场景下性能足够,不需要引入额外的存储库:

<template> <view class="gear"> <input v-model="draft" placeholder="输入装备,例如:防潮垫" @confirm="addItem" /> <view v-for="item in items" :key="item.id" class="row" @click="toggle(item)" > <text :class="{ done: item.done }">{{ item.name }}</text> </view> </view> </template> <script setup> import { ref, watch } from 'vue' import { onShow } from '@dcloudio/uni-app' const STORAGE_KEY = 'camp_gear' const items = ref(uni.getStorageSync(STORAGE_KEY) || []) const draft = ref('') watch( items, (val) => { uni.setStorageSync(STORAGE_KEY, val) }, { deep: true } ) const addItem = () => { const name = draft.value.trim() if (!name) return items.value.push({ id: Date.now(), name, done: false }) draft.value = '' } const toggle = (item) => { item.done = !item.done } onShow(() => { items.value = uni.getStorageSync(STORAGE_KEY) || [] }) </script>

watch 深监听数组变化后同步写入存储,比每次增删手动调 setStorageSync 少写出错分支。onShow 里重新读取,是为了处理从其他页面返回后的数据同步,避免多页面共用一份清单时看到旧数据。storage 适合放 JSON 小对象,图片、音频这类资源不要往里塞,Android 端单 key 超过 1MB 后读写延迟会明显上升。如果清单数据还要多人协作,再把读写逻辑抽进 pinia store,用 defineStore 管理,本地存储只做兜底缓存。

维度本地存储pinia适用场景
读取速度磁盘级内存级单人装备清单用本地存储足够
跨页面共享需自己监听同步天然响应式多页面同改一份数据用 pinia
持久化默认持久不持久pinia 需要搭配 persist 插件再落盘

3.3 天气请求的timeout与缓存兜底

露营计划页里通常会放一张天气卡片,常见做法是接和风天气或高德天气这类接口。第三方接口在弱网下最容易超时,uni.request 的 timeout 默认 60000 毫秒,对户外场景太长了,必须显式调小:

function fetchWeather(lat, lng) { return uni.request({ url: 'https://api.camp.example.com/weather', data: { lat, lng, key: WEATHER_KEY }, timeout: 8000, method: 'GET' }) } async function safeLoadWeather() { try { const res = await fetchWeather(center.lat, center.lng) return res.data.now } catch (e) { return { text: '晴', temp: '--' } } }

timeout 参数对大多数平台生效,但微信小程序的基础库对超时支持有差异,真机弱网测试时如果发现超时不生效,就用 Promise.race 包一层做兜底。天气数据变化频率低,拉取成功后缓存 15 分钟,避免每次进页面都发请求。WEATHER_KEY 这类密钥放前端会被直接抓包看到,常见做法是让服务端代理天气请求,小程序端只传坐标,由后端拼 key。

4. manifest配置、uniapp多端打包与3个高频坑

功能写完,打包和上架才是真正的分岔路。uniapp 打包微信小程序、H5 和安卓 App 走的是三条链路,manifest.json 是这三条链路的统一入口。这一章把配置、命令和三个高频报错一起讲清楚,打包阶段少走弯路。

4.1 manifest.json里的定位权限与隐私声明

manifest.json 在 HBuilderX 里可以可视化编辑,但多人协作的仓库里应该以源码为准。定位权限和隐私声明是打包前必须确认的两项:

{ "name": "露营", "appid": "__UNI__CAMP001", "mp-weixin": { "appid": "wx你的小程序appid", "requiredPrivateInfos": ["getLocation", "chooseLocation"], "permission": { "scope.userLocation": { "desc": "用于在营地地图上显示你的位置" } } }, "app-plus": { "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>" ] } } } }

mp-weixin 的 requiredPrivateInfos 是微信收紧隐私接口后必须补的声明,2023 年后更新过一轮规则,真机报 getLocation 相关错误时,第一件事就是回这里检查,而不是反复改代码。app-plus 的 permissions 是安卓权限列表,云打包时 HBuilderX 会合并这些声明,不要在页面里动态申请未声明的权限。改完 manifest 必须重新打包,热重载不会带上新配置。

注意:appid 属于应用标识,提交到公开仓库前确认是否要脱敏。HBuilderX 的测试包用公共证书即可,上架安卓应用市场必须换成自己的 keystore 签名。

4.2 微信小程序、H5和安卓的打包链路与产物

用 vue-cli 或 vite 方式创建的 uniapp 工程,可以用命令行出包,对 CI 友好:

# 安装依赖,注意 node 版本要和工程要求匹配 npm install # 微信小程序,产物在 dist/build/mp-weixin npm run build:mp-weixin # H5,产物在 dist/build/h5 npm run build:h5

微信小程序产物用微信开发者工具导入 dist/build/mp-weixin 目录,上传前在详情里确认 appid 正确。H5 产物是纯静态文件,可以部署到任意静态服务器,接口跨域需要在服务端配置 CORS。安卓 App 的常见做法是 HBuilderX 菜单栏“发行 -> 原生App-云打包”,不需要本地安卓环境;测试包选公共证书,上架时必须用自己生成的 keystore,并保持各市场包名一致。

目标端命令/入口产物位置最常见的失败原因
微信小程序npm run build:mp-weixindist/build/mp-weixin没填小程序 appid,编译后页面空白
H5npm run build:h5dist/build/h5接口跨域,需后端加 CORS 头
AndroidHBuilderX 云打包apk/aab证书过期或权限声明冲突

vue2 老工程迁到 vue3 后首次打包,容易遇到 HBuilderX 版本过老导致编译中断,Vue3 模板需要 HBuilderX 3.2.5 以上的版本,升级前先备份工程。

4.3 打包后布局异常和app is not defined的排查

打包后的报错和开发期不完全一样,下面这几个是社区里出现频率最高的:

报错或现象常见根因处理办法
打包后样式错位、字体大小不一致px 和 rpx 混用,或字体文件没走 static 目录统一用 rpx,字体放 /static/fonts 并在 App.vue 引入
app is not defined在微信小程序端引用了 App 全局对象或 window改用 uni.getApp(),或把逻辑放进 APP-PLUS 条件编译
修改刚进入的加载页面不生效改的是 pages.json 的 globalStyle,冷启动页是原生层App 端换启动图去 manifest 的启动图配置,H5 端改 index.html
微信小程序定位失败requiredPrivateInfos 未声明补声明后在开发者工具重新编译

条件编译是解决这类平台差异的核心手段,写法如下:

// #ifdef APP-PLUS console.log('只在 App 端编译执行') // #endif // #ifdef MP-WEIXIN console.log('只在微信小程序端编译执行') // #endif

条件编译按平台注释在编译期剔除代码,不是运行时判断,写在 template、script、style 里都生效。布局异常还有一个高发点:iPhone 底部被安全区横条遮挡,tabBar 页面要加 padding-bottom: constant(safe-area-inset-bottom),否则列表最后一项会被顶住。

5. 用uniapp自定义分享和扫码把露营闭环搭完

露营 App 真正的闭环不是看营地,而是“创建活动、分享好友、到场签到”。这一章把 uniapp 的自定义分享和扫码串起来,这也是上架前最值得真机验证的两个能力。

<script setup> import { ref } from 'vue' import { onShareAppMessage, onLoad } from '@dcloudio/uni-app' const activityId = ref('') onLoad((options) => { activityId.value = options.id || '' }) // 自定义分享:好友点进来直接落在活动页 onShareAppMessage(() => ({ title: '一起露营,地点我已选好', path: `/pages/activity/activity?id=${activityId.value}` })) // 现场签到:扫场地二维码核销 const scanCheckIn = () => { uni.scanCode({ onlyFromCamera: false, scanType: ['qrCode'], success: (res) => { const code = res.result uni.request({ url: 'https://api.camp.example.com/checkin', method: 'POST', data: { code } }) } }) } </script>

onShareAppMessage 在微信小程序端生效,返回的 path 必须带完整页面路径和参数,漏了 id 会导致好友打开后看不到对应活动。H5 端要接各平台的分享 SDK,常见做法是页面右上角引导复制链接,不依赖微信的分享回调。uni.scanCode 的 res.result 是二维码原始内容,建议把营地 id 编码进 URL,签到接口只收 code,前端扫码组件不需要引入三方库。

把这个页面在真机上按四项自测:户外关 Wi-Fi 跑定位,marker 与实际位置偏差控制在 30 米内;微信开发者工具里点右上角转发,确认好友打开后能落到活动页;打印二维码贴到营位,扫码后接口能返回核销成功;杀掉进程重新冷启动,加载页之后 2 秒内能进首页。如果场地不允许贴二维码,就把签到码改成口令输入框,uni.scanCode 换成 input 的 confirm 事件,核销接口不用改。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 17:26:58

智能柜物联网小程序模板源码解析与二次开发实践指南

简介&#xff1a;面向小程序开发者与物联网爱好者的智能柜物联网微信小程序模板源码&#xff0c;压缩包采用zip格式&#xff0c;约1.19MB&#xff0c;适合用于快速搭建智能储物柜、自助取件、快递柜管理等轻量级应用的基础框架。源码以微信小程序核心技术编写&#xff0c;涵盖W…

作者头像 李华
网站建设 2026/9/15 17:26:35

边缘安全加速:从CDN割裂架构到一体化防护新范式

1. 为什么今天必须重新理解“边缘安全加速”——从CDN老思路到EdgeOne新范式我第一次在客户现场听到“我们已经上了CDN&#xff0c;安全应该没问题了”这句话&#xff0c;是在2021年。当时对方是一家做在线教育的SaaS公司&#xff0c;前端用React&#xff0c;后端是Java微服务&…

作者头像 李华
网站建设 2026/9/15 17:25:10

新手入门选wordpress微信模板的5个避坑指南

新手入门选wordpress微信模板的5个避坑指南 网站做好了没人访问,这是很多新手老板最头疼的事。你花大价钱请人做站,结果上线三个月,后台看数据,每天就几个IP,还全是自己人点的。这时候才想起来,当初选模板太随意,根本没考虑手机端的体验。对于西北地区的中小企业来说,客户大多在手机上刷朋友圈、逛微信…

作者头像 李华
网站建设 2026/9/15 17:24:30

AD域用户无法改密码?账户属性与密码策略排查指南

域账号密码改不了这事&#xff0c;在AD环境里几乎每周都能遇到。用户一脸茫然地跑过来说“密码明明没错&#xff0c;就是改不动”&#xff0c;Helpdesk查了一圈也不知道卡在哪。问题看起来简单&#xff0c;但背后的原因可以排出一长串——账户属性勾错了、策略不达标、复制没同…

作者头像 李华