专栏首页APICloud AVM多端开发教程APICloud AVM多端开发 | 手把手教外卖点餐App开发(下)
原创

APICloud AVM多端开发 | 手把手教外卖点餐App开发(下)

接上篇APICloud AVM多端开发 | 手把手教外卖点餐App开发(上)

菜单点餐页面

分类和菜品的双向滚动交互

这个页面是一个左右分栏的布局。左边是菜单分类,右边的菜品。 有一组比较常见的交互:

  1. 滑动右侧菜品,左侧分类高亮会随其更改。
  2. 点击左侧菜品分类,右侧菜品回滚到到对应区域。

其中第一个交互相关逻辑类似于在开发商家主页的滚动 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];
}

菜品和加购处理 (跨端特性处理)

右侧的菜品有一个 @click="openAdd(goods)" 事件,用于打开加购页面。

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}
        })
    }
    
}

这个函数中展示了端差异上的处理。因为小程序没有类似 APICloudframe 的概念, 所以新弹出的页面在小程序上,是一个页面内部组件实现的。

当然这种方式 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);

    })
}        

支付成功页面的跳转

下单支付后跳转到支付结果页面。(这个过程是模拟成功下单,中间可以参考微信登录过程嵌套第三方支付)

至此,所有的页面逻辑主线已经完成。应用中还有一些细节处理,可以参考源码和文档进一步学习研究。项目源码位于https://github.com/apicloudcom/ordering-food

为了让开发者更好的掌握APICloud 多端开发技术。APICloud会持续更新这样的项目案例,多多关注哦!

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • APICloud AVM多端开发 | 手把手教外卖点餐App开发(上)

    为了让开发者更加快速的学习和了解APICloud多端开发技术,APICloud平台特别推出一款多端源码-《外卖点餐App开发》,可以体验一套代码编译Androi...

    APICloud官方
  • Avm.js前端框架的优势

    AVM(Application-View-Model)是APICloud推出的一个跨端的高性能 JavaScript框架,更趋近于原生的编程体验,它提供简洁的模...

    APICloud官方
  • 这种技术能够替代 Android 原生开发?

    今天在浏览知乎的时候,看到这么一个问题,感觉很有意思,有点分享价值,如下: APICloud 能都替代 Android 原生开发吗? APICloud 现在好像...

    非著名程序员
  • 一周简报|一登沈洽金:用“刷脸”让登录变得更简单

    编辑导语 青云cloud:资源协作服务上线,实现更灵活的权限配置与资源共享;一登沈洽金:用“刷脸”让登录变得更简单;云适配:HTML5并非真正跨平台,想做移动办...

    BestSDK
  • 手把手带你开发一款云开发版点餐小程序,微信扫码点餐,用户端和后厨端都有

    我后面会教大家如何生成桌号二维码,只需要把对应桌号的二维码贴在餐桌上,用户点击 扫码点餐 识别二维码,即可获取到桌号信息。

    编程小石头
  • 最简单的混合开发教程资料汇总

    王小婷
  • 使用APICloud敏捷式开发总结,回顾开发一个完整APP过程。

    刚接触这个平台的新手,可以参考上方的新手指南,官方也有7天的入门视频教程,虽然视频及文档教程比较老,凑合着看,可以满足小白入门要求。

    孙叫兽
  • 在线教育app开发+小程序项目开发要点

    本项目的一个教育培训服务APP。提供在线浏览机构信息、名师风采和课程预约订购等功能。

    APICloud官方
  • 选型宝访谈:如何借助API生态平台,一站式快速搞定APP开发?

    ...

    选型宝

扫码关注云+社区

领取腾讯云代金券