简介:这是一份基于uni-app开发的社区团购类APP源码模板,面向前端开发者与跨端应用学习者,聚焦社区生鲜电商场景,提供开箱即用的购物流程、拼团机制与多端适配能力。资源包为ZIP格式,大小935KB,虽未提供具体文件总数与类型明细,但作为完整可运行项目,典型包含pages页面组件、components自定义模块、store状态管理、utils工具函数及uni-app标准配置文件,覆盖登录注册、商品浏览、购物车、拼团下单、订单管理、地理位置提货点匹配等核心业务模块。已有330人学习下载,适合中初级开发者快速掌握uni-app实战架构、Vue语法在多端环境中的差异化处理,以及社区团购类应用的UI交互设计逻辑与前后端联调思路。
1. 社区团购类 APP 的真实落地难点:为什么“周鲜生拼拼”必须用 uni-app 而不是纯小程序或原生开发?
你刚接到一个需求:“做个社区团菜 APP,支持团长开团、居民下单、次日达、自提点核销,还要能上架 iOS/Android/微信小程序三端。”——这时如果选 React Native,iOS 审核卡在「非即时配送类应用不得使用后台定位」;如果选 Flutter,团长端扫码核销时调用系统相机频繁卡顿,安卓低端机首屏加载超 3 秒;如果只做微信小程序,用户无法添加桌面图标、不能发本地通知、团长无法离线查看待核销订单。而“周鲜生拼拼”这类项目,核心矛盾从来不是功能多寡,而是履约确定性与渠道渗透率的平衡:既要让 50 岁以上团长用手机扫个码就能完成核销(要求 UI 稳定、操作路径极短),又要让平台方能统一管理商品库存、拼团规则、分润逻辑(要求业务逻辑集中、热更新可控)。uni-app 正是当前唯一能同时满足「H5 可嵌入公众号菜单」「App 端可调用原生蓝牙/NFC 核销硬件」「小程序端复用同一套 Vue 语法且不触发 wxs 限制」的技术栈。它不是“写一次到处跑”的理想化方案,而是用一套 Vue 模板 + 条件编译 + 原生插件桥接,在真实交付中把三端差异压缩到可维护阈值内的务实选择。
2. 从零初始化“周鲜生拼拼”项目:uni-app 多端适配的最小可行结构设计
2.1 创建项目并确认基础编译目标
uni-app 官方 CLI 工具链已迭代至@dcloudio/vue-cli-plugin-uni@3.4.0+,新项目必须使用 Vue 3 Composition API 模式(Options API 在 App 端存在生命周期钩子错位问题)。执行以下命令创建带 TypeScript 支持的模板:
npx degit dcloudio/uni-preset-vue#vite my-zhouxiansheng cd my-zhouxiansheng npm install npm run dev:mp-weixin # 启动微信小程序调试 npm run dev:app-android # 启动安卓 App 调试(需配置 Android Studio SDK)提示:
dev:app-android依赖本地已安装 Android SDK 29+ 和 JDK 17;若报错Failed to find Build Tools revision 34.0.0,需在 Android Studio → SDK Manager → SDK Tools 中勾选对应版本。iOS 编译需 macOS 环境,此处暂不展开。
项目根目录下vue.config.js必须显式声明多端能力开关:
// vue.config.js module.exports = { configureWebpack: { resolve: { alias: { '@': path.resolve(__dirname, 'src') } } }, // 关键:启用 App 端原生能力注入 chainWebpack: config => { config.plugin('html').tap(args => { args[0].title = '周鲜生拼拼' return args }) }, // 必须开启条件编译支持 transpileDependencies: ['@dcloudio/uni-ui'] }2.2 目录结构按业务域而非技术层划分:避免“pages/index.vue”式反模式
社区团购 APP 的核心状态流是「开团 → 加入 → 支付 → 配送 → 核销」,但传统 uni-app 目录常按页面堆砌,导致团长端和居民端逻辑混杂。正确做法是按角色+状态域组织:
src/ ├── api/ # 所有请求封装,自动注入 token 和环境前缀 │ ├── group.ts # 团购相关接口(开团、查团、参团) │ ├── order.ts # 订单生命周期(创建、支付回调、核销) │ └── user.ts # 用户体系(登录、地址、身份切换) ├── store/ # Pinia 状态管理,按模块切分 │ ├── group.ts # 团购状态(当前团ID、参团人数、截止时间) │ ├── order.ts # 订单状态(待支付/已发货/待核销) │ └── user.ts # 用户身份(isCaptain: boolean) ├── components/ # 可复用 UI 组件,带平台适配标记 │ ├── captain/ # 仅团长可见组件(如核销扫码框) │ │ └── ScanQr.vue # 条件编译:#ifdef APP-PLUS || MP-WEIXIN │ └── common/ # 全端通用组件(商品卡片、拼团倒计时) ├── pages/ # 页面路由,仅保留入口级页面 │ ├── index.vue # 首页(根据 user.isCaptain 动态加载不同 TabBar) │ └── captain/ # 团长专属页(独立路由,App 端可设为首页) └── utils/ # 平台差异工具 ├── platform.ts # 判断当前环境:uni.getSystemInfoSync().platform └── bluetooth.ts # App 端蓝牙连接封装(iOS/Android API 差异处理)注意:
#ifdef APP-PLUS是 uni-app 条件编译指令,仅在 App 端生效;#ifdef MP-WEIXIN仅小程序生效。禁止在pages/下直接写业务逻辑,所有数据获取必须经api/层,状态变更必须走store/。
2.3 配置 manifest.json 实现三端差异化能力声明
manifest.json不是静态配置文件,而是三端能力的契约声明。例如“周鲜生拼拼”需在 App 端调用蓝牙核销设备,但在小程序端禁用该功能:
{ "name": "周鲜生拼拼", "appid": "", "description": "社区团购服务平台", "versionName": "1.2.0", "versionCode": "120", "transformPx": true, "app-plus": { "usingComponents": true, "nvueStyleCompiler": "uni-app", "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": true, "delay": 0 }, "modules": { "Bluetooth": {}, // 必须显式声明启用蓝牙模块 "Share": {}, "Payment": {} }, "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>", "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>" ] }, "ios": { "UIBackgroundModes": ["bluetooth-central"], "NSBluetoothAlwaysUsageDescription": "用于连接核销设备进行订单核销" } } }, "mp-weixin": { "appid": "wx1234567890abcdef", "setting": { "urlCheck": false }, "usingComponents": true } }提示:iOS 上蓝牙后台模式需在
distribute.ios.UIBackgroundModes中声明bluetooth-central,否则 App 进入后台后无法维持连接;安卓端需在permissions中添加BLUETOOTH_ADMIN,否则uni.openBluetoothAdapter()会静默失败。
3. 实现团长端蓝牙核销:uni-app App 端连接指定 DeviceID 设备的完整链路
3.1 初始化蓝牙适配器并搜索目标设备
社区团购核销场景中,设备 ID(如CC:22:3D:E3:CE:30)由硬件厂商预烧录,APP 需跳过通用扫描,直连指定设备。关键在于uni.createBLEConnection()前必须完成设备发现与服务发现:
// src/utils/bluetooth.ts import { ref } from 'vue' export const useBluetooth = () => { const connectedDeviceId = ref<string | null>(null) // 1. 初始化适配器(App 端必须先调用) const initAdapter = () => { return new Promise<void>((resolve, reject) => { uni.openBluetoothAdapter({ success: () => resolve(), fail: (err) => reject(new Error(`蓝牙适配器打开失败: ${err.errMsg}`)) }) }) } // 2. 直连指定 DeviceID(跳过扫描,适用于已知设备) const connectToDevice = async (deviceId: string) => { try { await initAdapter() // 关键:iOS 要求先调用 createBLEConnection,Android 可直接 connect if (uni.getSystemInfoSync().platform === 'ios') { await new Promise<void>((resolve, reject) => { uni.createBLEConnection({ deviceId, success: () => resolve(), fail: (err) => reject(new Error(`iOS 连接失败: ${err.errMsg}`)) }) }) } else { // Android 可直接 connect await new Promise<void>((resolve, reject) => { uni.connectBLEDevice({ deviceId, success: () => resolve(), fail: (err) => reject(new Error(`Android 连接失败: ${err.errMsg}`)) }) }) } connectedDeviceId.value = deviceId console.log(`成功连接设备: ${deviceId}`) return true } catch (error) { console.error('连接设备失败', error) throw error } } return { connectedDeviceId, initAdapter, connectToDevice } }3.2 发送核销指令并监听响应:特征值读写与错误重试
核销设备通常通过 BLE 的Write Without Response特征值接收指令,再通过Notify特征值返回结果。需严格遵循 GATT 协议流程:
// src/api/order.ts import { useBluetooth } from '@/utils/bluetooth' export const verifyOrder = async (orderId: string, deviceId: string) => { const { connectedDeviceId, connectToDevice } = useBluetooth() // 确保已连接 if (!connectedDeviceId.value || connectedDeviceId.value !== deviceId) { await connectToDevice(deviceId) } try { // 1. 获取设备服务列表(需提前知道 service UUID) const services = await new Promise<UniApp.GetConnectedBluetoothDevicesSuccessCallbackResult['services']>( (resolve, reject) => { uni.getConnectedBluetoothDevices({ success: (res) => resolve(res.services), fail: (err) => reject(err) }) } ) const targetService = services.find(s => s.serviceId.includes('0000fff0')) // 示例服务 UUID if (!targetService) throw new Error('未找到核销服务') // 2. 获取特征值(需提前约定 characteristic UUID) const characteristics = await new Promise<UniApp.GetBLEDeviceCharacteristicsSuccessCallbackResult['characteristics']>( (resolve, reject) => { uni.getBLEDeviceCharacteristics({ deviceId, serviceId: targetService.serviceId, success: (res) => resolve(res.characteristics), fail: (err) => reject(err) }) } ) const writeChar = characteristics.find(c => c.uuid.includes('0000fff1')) // 写入特征值 const notifyChar = characteristics.find(c => c.uuid.includes('0000fff2')) // 通知特征值 if (!writeChar || !notifyChar) throw new Error('未找到核销特征值') // 3. 启用 Notify(iOS 必须先 enable,Android 可选) await new Promise<void>((resolve, reject) => { uni.notifyBLECharacteristicValueChange({ state: true, deviceId, serviceId: targetService.serviceId, characteristicId: notifyChar.uuid, success: () => resolve(), fail: (err) => reject(err) }) }) // 4. 发送核销指令(订单号转 hex 字符串) const hexOrderId = orderId.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('') const buffer = new ArrayBuffer(16) const dataView = new DataView(buffer) for (let i = 0; i < hexOrderId.length; i += 2) { const byte = parseInt(hexOrderId.substr(i, 2), 16) dataView.setUint8(i / 2, byte) } await new Promise<void>((resolve, reject) => { uni.writeBLECharacteristicValue({ deviceId, serviceId: targetService.serviceId, characteristicId: writeChar.uuid, value: buffer, success: () => resolve(), fail: (err) => reject(err) }) }) // 5. 监听 Notify 返回(设置超时) return new Promise<string>((resolve, reject) => { const timeout = setTimeout(() => { uni.offBLECharacteristicValueChange() reject(new Error('核销超时')) }, 5000) const handleNotify = (res: UniApp.OnBLECharacteristicValueChangeSuccessCallbackResult) => { clearTimeout(timeout) uni.offBLECharacteristicValueChange() const result = String.fromCharCode(...new Uint8Array(res.value)) if (result.includes('SUCCESS')) { resolve(result) } else { reject(new Error(`核销失败: ${result}`)) } } uni.onBLECharacteristicValueChange(handleNotify) }) } catch (error) { console.error('核销执行失败', error) throw error } }注意:
uni.writeBLECharacteristicValue的value参数必须是ArrayBuffer,不能传字符串;iOS 对 Notify 特征值必须先调用notifyBLECharacteristicValueChange启用,否则收不到回调;Android 端需确保设备广播包中包含服务 UUID,否则getConnectedBluetoothDevices可能返回空数组。
3.3 处理 App 端蓝牙连接的典型异常场景
| 异常现象 | 根本原因 | 解决方案 |
|---|---|---|
openBluetoothAdapter:fail | 安卓 12+ 需要BLUETOOTH_CONNECT权限 | 在manifest.json的distribute.android.permissions中追加<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> |
createBLEConnection:fail not found | iOS 设备未在蓝牙列表中(未被系统缓存) | 调用uni.startBluetoothDiscovery()后立即uni.getConnectedBluetoothDevices(),确保设备出现在连接列表 |
writeBLECharacteristicValue:fail | 特征值未启用 Notify 或未发现服务 | 在connectToDevice后强制调用uni.getBLEDeviceServices()和uni.getBLEDeviceCharacteristics(),不依赖缓存 |
| 连接后无法发送指令 | 设备要求配对(Pairing) | 在connectBLEDevice后调用uni.createBLEConnection,iOS 会自动弹出配对框 |
4. 微信小程序端与 H5 页面的深度集成:解决 WebView 通信断层问题
4.1 小程序内嵌 H5 的通信机制重构
“周鲜生拼拼”需在小程序中嵌入团长业绩报表(由 Vue3 + ECharts 渲染),但原生web-view组件存在两大缺陷:无法调用微信支付 API、无法同步用户登录态。解决方案是放弃web-view,改用WebView组件 +postMessage双向通信:
<!-- pages/captain/report.vue --> <template> <view class="container"> <!-- 使用 uni-app 官方 WebView 组件(非 web-view) --> <web-view :src="reportUrl" @message="onWebViewMessage" @load="onWebViewLoad" @error="onWebViewError" /> </view> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { useUserStore } from '@/store/user' const userStore = useUserStore() const reportUrl = ref<string>('https://zxs-report.example.com/?token=' + userStore.token) const onWebViewLoad = () => { // 页面加载完成后,主动向 H5 发送登录态 const webView = uni.getCurrentWebview() webView?.postMessage({ type: 'INIT_USER', data: { userId: userStore.userId, token: userStore.token, role: 'captain' } }) } const onWebViewMessage = (e: any) => { const { data } = e.detail if (data.type === 'PAYMENT_REQUEST') { // H5 请求支付,由小程序端调起 uni.requestPayment({ provider: 'wxpay', orderInfo: data.orderInfo, success: () => { webView?.postMessage({ type: 'PAYMENT_SUCCESS' }) }, fail: (err) => { webView?.postMessage({ type: 'PAYMENT_FAIL', error: err.errMsg }) } }) } } </script>4.2 H5 页面接收消息并触发对应逻辑
H5 页面需监听uni.postMessage事件(uni-app WebView 注入了全局uni对象):
// https://zxs-report.example.com/index.html document.addEventListener('UniAppJSBridgeReady', () => { // 接收小程序发来的初始化消息 uni.addInterceptor('onMessage', (res) => { const { type, data } = res.data if (type === 'INIT_USER') { // 存储用户态,初始化图表 window.currentUser = data initChart() } else if (type === 'PAYMENT_SUCCESS') { alert('支付成功!') location.reload() } }) // 向小程序发起支付请求 window.requestPayment = () => { uni.postMessage({ data: { type: 'PAYMENT_REQUEST', orderInfo: { ... } } }) } })提示:
UniAppJSBridgeReady事件确保uni对象已就绪;uni.addInterceptor('onMessage')是 uni-app WebView 特有的消息拦截方式,比window.addEventListener('message')更可靠;H5 页面域名必须在小程序后台配置为业务域名,否则postMessage无效。
4.3 解决 iOS 端 WebView 白屏与滚动失效问题
iOS 微信内置浏览器对web-view渲染有特殊限制,常见白屏源于 CSSheight: 100%失效。必须使用vh单位并禁用缩放:
/* pages/captain/report.vue 的样式 */ .container { width: 100vw; height: 100vh; /* 禁用 height: 100% */ overflow: hidden; } web-view { width: 100%; height: 100%; /* iOS 专用修复:防止白屏 */ -webkit-overflow-scrolling: touch; } /* H5 页面需添加 viewport */ /* <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> */5. App 发布前的关键验证清单:绕过苹果审核与安卓商店拒审的硬性检查点
5.1 iOS 审核避坑:蓝牙、定位、隐私政策的合规组合
苹果对社区团购类 App 的审核重点在「后台能力滥用」和「隐私披露完整性」。针对“周鲜生拼拼”需逐项验证:
| 检查项 | 合规要求 | 验证方法 |
|---|---|---|
| 蓝牙后台模式 | 仅允许bluetooth-central,且必须在Info.plist中声明NSBluetoothAlwaysUsageDescription | 检查manifest.json的distribute.ios部分是否包含UIBackgroundModes和NSBluetoothAlwaysUsageDescription |
| 定位权限 | 若仅用于「附近自提点」,必须声明NSLocationWhenInUseUsageDescription,禁用NSLocationAlwaysAndWhenInUseUsageDescription | manifest.json中distribute.ios的NSLocationWhenInUseUsageDescription文案需明确说明用途(例:“用于显示您附近的自提点位置”) |
| 隐私政策链接 | App Store Connect 中必须填写有效 HTTPS 链接,且页面需包含蓝牙、定位、相册权限的单独说明 | 在pages/index.vue的「设置」页中,点击「隐私政策」跳转至https://zxs.example.com/privacy.html,页面需有独立章节描述各权限用途 |
注意:若 App 在后台持续扫描蓝牙设备(如监听核销设备上线),会被拒审。正确做法是仅在团长进入「核销页」时开启蓝牙,离开页面时调用
uni.closeBluetoothAdapter()。
5.2 安卓商店上架必备材料清单(以华为、小米、OPPO 为例)
| 商店 | 必需材料 | 特殊要求 |
|---|---|---|
| 华为应用市场 | APK 包、软著证书、ICP 备案号、《用户协议》《隐私政策》PDF 文件 | APK 包名必须与manifest.json中appid一致;软著证书需体现“周鲜生拼拼”名称 |
| 小米应用商店 | APK、软著、ICP 备案、《儿童个人信息保护声明》(即使无儿童用户) | 必须在AndroidManifest.xml中声明android:exported="true"的启动 Activity |
| OPPO 开放平台 | APK、软著、ICP、《敏感权限调用说明》文档 | 需单独提交文档说明蓝牙权限调用场景(例:“仅在团长核销订单时,连接指定设备 ID 的蓝牙打印机”) |
5.3 真机测试必须覆盖的 5 类极端场景
- 弱网下单:在 2G 网络下提交订单,验证
uni.showToast是否正常显示「订单提交中」,且网络恢复后自动重试; - 蓝牙设备离线:连接核销设备后拔掉电源,APP 应在 3 秒内提示「设备已断开,请重新连接」,而非无限 loading;
- 小程序分享裂变:用户点击「邀请邻居参团」生成带参数的分享卡片,新用户打开后自动识别
?ref=xxx并绑定推荐关系; - App 后台进程被杀:杀掉 App 进程后,通过系统通知点击「今日开团提醒」,应直接跳转至对应团购页而非首页;
- iOS 17 新增限制:在「设置 → 隐私与安全性 → 蓝牙」中关闭 App 蓝牙权限,APP 再次调用
uni.openBluetoothAdapter()时应弹出系统权限申请框,而非静默失败。
验证通过后,执行最终构建命令:
# 构建正式版 App npm run build:app-plus # 构建微信小程序(上传前需替换 appId) npm run build:mp-weixin # 构建 H5 版本(用于公众号菜单) npm run build:h5构建产物位于unpackage/dist/build/目录,其中app-android.apk和app-ios.ipa即为上架包。
本文还有配套的精品资源,点击获取