上周,咱们分享了用APICloud AVM多端开发技术,开发一款《外卖app开发》项目源码解析上篇,如今把下篇补足,但愿能帮助开发者快速体验一套代码编译Android和iOS app+小程序。编程
这篇主要讲解菜单点餐页双向滚动交互、菜品加购处理、购物车和付款逻辑处理,以及用户中心开发。查看这篇时,能够先复习上篇:小程序
APICloud AVM多端开发案例源码《外卖app开发》深度解析(上)api
一 菜单点餐页面
分类和菜品的双向滚动交互
这个页面是一个左右分栏的布局。左边是菜单分类,右边的菜品。 有一组比较常见的交互:数组
- 滑动右侧菜品,左侧分类高亮会随其更改。
- 点击左侧菜品分类,右侧菜品回滚到到对应区域。
其中第一个交互相关逻辑相似于在开发商家主页的滚动scroll-view
触发头部透明度 的逻辑。 因此一样地为右侧的scroll-view
绑定上@scroll="onScroll"
函数。安全
具体逻辑请参考源码的实现部分,获取滚动高度等和主页相似。服务器
重点关注第二个交互的核心在于点击对应分类,右侧的scroll-view
须要滚动到指定位置。 使用属性来进行位置绑定:scroll-top={scrollTo}
。此时只须要在左边的分类点击事件@click="switchCategory(index)"
计算出正确的scrollTo
便可实现。微信
function switchCategory(index) { this.data.categoryIndex = index; this.data.CD = new Date().getTime() + 500; // 手动切换分类后须要锁定500毫秒 避免右侧scroll-view滚动时带来次生问题 this.data.scrollTo = this.offsetList[index]; }
菜品和加购处理 (跨端特性处理)app
右侧的菜品有一个 @click="openAdd(goods)"
事件,用于打开加购页面。ide
function openAdd(goods) { if (isMP()) { this.data.currentGoods = goods; wx.hideTabBar(); } else { api.openFrame({ name: 'goods_add', url: '../goods_add/goods_add.stml', pageParam: {goods} }) } }
这个函数中展现了端差别上的处理。由于小程序没有相似 APICloud
的 frame
的概念, 因此新弹出的页面在小程序上,是一个页面内部组件实现的。函数
固然这种方式 APP
原生端也是支持的。若是须要进一步提升性能,发挥原生优点,则可使用原生端的frame
来完成。 此时,将目标页面封装在一个自定义组件中,并把当前菜品数据传递进去。
目前组件和 frame
页面的获参形式暂时不一样。在 goods_add
这个组件中的 installed
生命周期中能够看到以下的兼容片断:
this.data.goods = this.props.goods ? this.props.goods : api.pageParam.goods;
在新展开的加购浮层上,看到了以前定义的 goods_action
,因此大体逻辑也是获取商品数据和加购数,并实现一下addCart函数。 实际上这个页面很相似商品详情页,只是展现UI不太相同。
沉浸式状态栏 safe-area
在这个页面中,本身实现了一个顶部导航栏。沉浸式状态栏通常会须要获取状态栏高度等处理能力。 在 avm.js
中提供一个 safe-area
组件,用于自动处理异形屏的边界问题。
<safe-area> <view class="header"> <text class="title">菜单</text> </view> </safe-area>
在主页中,也看到相关编程式获取安全区域数据的代码:
this.data.safeAreaTop = api.safeArea ? api.safeArea.top : 0;
二 购物车页面 computed 计算和v-if的条件渲染
购物车页面是一个比较经典的展现相关页面内部逻辑的案例。
在页面初始化的时候, this.getCartData()
拿到本地存储的购物车全部的数据。
function getCartData() { let cartData = api.getPrefs({sync: true, key: 'CART-DATA'}); if (cartData) { cartData = JSON.parse(cartData); this.data.cartData = cartData; this.generateCartList(); setTabBarBadge(2, Object.keys(cartData).length); } }
其中还混合了一个 generateCartList
逻辑。
function generateCartList() { let cartData = this.data.cartData; let arr = []; for (let i in cartData) { arr.push({checked: true, ...cartData[i]}); } this.data.cartList = arr; }
这是一个生成函数,是将保存的对象构建为页面所须要的数组结构,同时增长每个元素的 checked
属性。 而后再页面部分经过 v-for
来循环当前购物车的数据。
<view class="main-cart-goods-item" v-for="item in cartList"> <radio-box class="main-cart-radio-box" :checked="item.checked" onChange={this.radioToggle.bind(this)} :item="item"></radio-box> <img class="main-cart-goods-pic" mode="aspectFill" src={{item.goods.thumbnail}} alt=""/> <view class="main-cart-goods-info"> <text class="main-cart-goods-name">{{ item.goods.name }}</text> <view class="main-cart-flex-h"> <text class="main-cart-goods-price-signal">¥</text> <text class="main-cart-goods-price-num">{{ item.goods.curt_price }}</text> <goods-counter onCountChange={this.countChange.bind(this)} :count="item.count" :item="item"></goods-counter> </view> </view> </view>
注意到每个条目的开头嵌套了一个 <radio-box/>
自定义组件。 这个组件担负的任务很简单,就是使用自定的样式来渲染一个单选框。固然 avm.js
自带的系统组件 radio
也是能够实现的。
computed 的使用
下面有一个全选按钮,用于控制是否全选。
function checkAll() { const checked = !this.allChecked; for (let i = 0; i < this.data.cartList.length; i++) { this.data.cartList[i].checked = checked; } }
而这个函数第一行以来的 this.allChecked
则是一个计算属性。在 computed
中能找到它的实现:
function allChecked() { return !this.cartList.some((item) => { // 也可使用 every 来修改相反逻辑实现 return !item.checked; }) }
紧接着它下面还有另一个计算属性: totalPrice
:
function totalPrice() { // 先筛选出选中项 let list = this.data.cartList.filter(item => { return item.checked; }) // 再计算总和而且格式化结果 return (list.length ? list.reduce((total, item) => { return total + item.goods.curt_price * item.count; }, 0) : 0).toFixed(2); }
而后再模板中直接使用这个结果,便可完成总价的显示:
<view class="text-group"> <text class="main-cart-footer-text">合计</text> <text class="main-cart-footer-price">¥{{ totalPrice }}</text> </view>
能够看到,计算属性 computed
是能够经过一些逻辑计算出须要的结果,而且会暴露给实例自己, 在模板中可以同数据同样绑定。 同时可以自动处理所依赖的数据变化,作出实时的更新。
v-if 条件渲染
在页面中,有一个变量标记 isEdit
,用来表示当前页面是不是在处于编辑状态。
<view @click="toggleEdit"> <text class="main-cart-finnish-text" v-if="isEdit">完成</text> <view v-else class="main-cart-action"> <img class="main-cart-action-icon" src="../../image/icon/icon-cart-edit.png" alt=""/> <text class="main-cart-action-text">编辑</text> </view> </view>
根据编辑状态的切换,右上角的按钮文案变化为“完成”和“编辑”两种状态。这个时候就能够经过 v-if
来判断渲染。 下面的结算、移除按钮也是同样,只不过是在模板中使用了三元表达式来作显示。
<text class="main-cart-footer-btn-text">{{ isEdit ? '移除' : '去结算' }}</text>
三 用户页面
这个页面主要有两个要点:头部用户信息区域和订单列表。
头部用户信息
头部的用户信息须要在初始化的时候读取本地用户数据。
/** * 获取用户信息 * @returns {boolean|any} */ function getUser() { let user = api.getPrefs({ sync: true, key: 'USER' }); if (user) { return JSON.parse(user) } return false; }
把获取到的用户数据做为一个普通的页面数据,用来渲染用户信息面板。 若是用户数据不存在,也就是未登陆模式,则须要使用 v-if
条件渲染来展现登陆界面。
<view class="user-info flex flex-h flex-center-v" v-if="userInfo" @click="logout"> <img class="user-avatar" src={{userInfo.avatarUrl}} alt=""/> <text class="user-name">{{ userInfo.nickName }}</text> </view> <view class="user-info flex flex-h flex-center-v" v-else @click="wxLogin"> <img class="user-avatar" src="../../image/icon/icon-user-avatar.png" alt=""/> <text class="user-name">使用微信登陆</text> </view>
登陆逻辑
在未登陆的状况下,上面的第二块会展现,点击触发 wxLogin
方法:
function wxLogin() { if (isMP()) { this.mpLogin(); } else { this.doLogin({ssid: getDeviceId()}); } }
这里依然须要对特性平台差别化处理。由于原生端和小程序端使用微信登陆是两个不一样的逻辑。 源代码 /widget/pages/main_user/main_user.stml
中还展现了一些使用原生模块来调用微信来登陆的逻辑。
登陆成功之后,开始执行 loginSuccess
,能够保存相关用户信息和会话信息,以备之后的使用。同时还须要刷新用户的购物列表。 若是在真实项目中其余已经打开的页面也须要监测用户状态变化,能够借助广播事件来处理详细的逻辑。
function loginSuccess(userInfo) { api.setPrefs({ key: 'USER', value: userInfo }); this.data.userInfo = userInfo; this.getOrderList(); }
页面的下拉刷新
页面下拉刷新和触底加载依赖于 scroll-view
的相关事件绑定和实现。
<scroll-view scroll-y class="flex-1 main-user-scroll-view" enable-back-to-top refresher-enabled refresher-triggered={{loading}} @refresherrefresh="onRefresh"> <view v-if="orderList.length"> <order-item :order="order" v-for="order in orderList" onOrderAction={this.orderAction.bind(this)}></order-item> </view> <view class="empty-block" v-else> <empty-block text="暂无订单哦~" type="order"></empty-block> </view> </scroll-view>
其中 @refresherrefresh="onRefresh"
就是在下拉刷新须要触发的逻辑。 refresher-triggered={{loading}}
就是下拉刷新的状态。(用于通知回弹和设置刷新中)。
function onRefresh() { this.data.loading = true; // 设置正在刷新 if (this.data.userInfo) { //有用户信息了才刷新 this.getOrderList(); } else { setTimeout(_ => { this.data.loading = false; api.toast({ msg: '请登陆后查看历史订单' }) }, 1000) } }
主页的开发大体就完成了,下面关注一下付款下单的过程。
四 待付款页面 (表单数据)
该页面也比较简单,大多数实现的逻辑在前面的页面已经说起。 此外有一个输入框表单 ,用来收集用户的输入备注信息。
<view class="order-note"> <text class="order-note-key">备注</text> <input class="order-note-input" placeholder="如需备注请输入" onBlur="onBlur" maxlength="30" id="remark"/> </view>
经过失去焦点事件 onBlur="onBlur"
来动态获取数据。
function onBlur(e) { this.data.remark = e.target.value; }
获取数据也还有其余多种方式,能够进一步参考组件 input
以及其余表单组件文档。
开始提交订单,和服务器通讯下单而且支付。下单完成后作一些联动处理:
function addOrder() { POST('orders/app_addorder', this.formData).then(data => { // 打开结果页 api.openWin({ name: 'pay_result', url: '../pay_result/pay_result.stml' }); // 通知支付成功 刷新订单页面 api.sendEvent({ name: 'PAY-SUCCESS' }) // 清空购物车 api.setPrefs({ key: 'CART-DATA', value: {} }); setTabBarBadge(2, 0); }) }
支付成功页面的跳转
下单支付后跳转到支付结果页面。(这个过程是模拟成功下单,中间能够参考微信登陆过程嵌套第三方支付)
-----------
至此,全部的页面逻辑主线已经完成。应用中还有一些细节处理,能够参考源码和文档进一步学习研究。 想快速上手APICloud AVM多端开发,能够查看快速上手教程:https://docs.apicloud.com/apicloud3/?uzchannel=7