zoukankan      html  css  js  c++  java
  • 移动应用APP购物车(店铺系列二)

    今天还是说移动app开发,店铺系列文章,我们经常去超市使用购物车,即一个临时的储物空间,用完清空回收。我大兄弟说,

    平时很忙,录入订单的目录很多,临时有事回来要可以继续填写,提交订单后才算结束,这就是一个典型的购物车场景了。那

    系统的购物车如何实现?现在就来实战一把,做个如淘宝类的购物车。

    作者原创文章,谢绝一切转载!

     本文只发表在"公众号"和"博客园",其他均属复制粘贴!如果觉得排版不清晰,请查看公众号文章。 

    准备:

    Idea2019.03/Gradle6.0.1/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Mysql8.0.11

    /Vue2.5/OSS/Hbuilder2.6.1

    难度: 新手--战士--老兵--大师

    目标:

    1. 手机APP前端实现购物车功能
    2. async/await使用

    步骤:

    为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。代码地址:https://github.com/xiexiaobiao/vehicle-shop-mobile.git

    1 本套系统大体情况

    后端代码量约1.5万,双前端约1.5万,技术还是很具代表性的,不然就不好意思拿出来说事了,详细可看Git库说明,下图是后端代码量分析:

    Web管理界面:需要密码的,请公众号留言。

    手机端:使用Hbuilder编码,Uniapp框架,再随手捡了几个UI拿来大改了几下,基本形状如下:

     

     

    2 购物车原理

    先说存储,有三套方案;一是直接数据库端存储,与后台交互多,会增加流量和业务复杂度;二是Localstorage存储,持久化到本地浏览器端,除非主动

    删除,否则永久存在;三是Session级别存储,使用vuex组件,会话级存储,app关闭即清空。再说vuex组件,是vue框架组件之一,其最常用的功能就是

    存储用户登录状态,因为系统很多地方的使用都需要进行登录验证,我们可以在用户登录之后,将登录状态写入vuex,那系统其他地方就可以随用随取,

    我这里即说第三套方案,使用vuex做缓存实现购物车。Vuex基础知识,略!请君自查!

     

    思路:建立一个vuex数组,即购物车存储空间,选择商品后,即加入该数组中,如果数量等属性有更新,也同步到该该数组,只要app不关闭就可以打开

    购物车继续编辑,直到提交订单时清空该数组。

     

    3 购物车存储

    vehicle-shop-app/store/index.js

    import Vue from'vue'
    import Vuex from'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
        state: {
            hasLogin: false,
            userInfo: {},
            // session周期有效
            items: [{
                      idItem: 3,
                      itemUuid: 'SP100034',
                      category: '保养',
                      classification: '',
                      itemName: '特色全合成机油',
                      sellPrice: 160.00,
                      discountPrice: 150.00,
                      brandName: '丰田',
                      description: '1.5升塑料瓶装',
                      shipment: true,
                      quantity: 3,
                      remark: '八折优惠5块钱',
                      alertQuantity: 5,
                      specification: '1.5升瓶装',
                      unit: '瓶',
                      sales: 20 ,
                      stock: 50,
                      checked: true, // 是否选中
                      picAddr:'http://biao-aliyun-oss-pic-bucket.oss-cn-shenzhen.aliyuncs.com/images/2020/03/08/1583628752948gv86t511pi.jpg',
                },
                ],
        },
        // 同步操作
        mutations: {
            login(state, provider) {
                state.hasLogin = true;
                state.userInfo = provider;
                // 将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口
                // 对比vuex,localstorage是永久存储,保存在本地浏览器中
                uni.setStorage({//缓存用户登陆状态
                    key: 'userInfo',
                    data: provider
                })
                console.log(state.userInfo);
            },
            logout(state) {
                state.hasLogin = false;
                state.userInfo = {};
                uni.removeStorage({
                    key: 'userInfo'
                })
            },
            // 添加进购物车
            addCartItems(state,provider){
                const cartItem = state.items.find(item => item.itemUuid === provider.itemUuid)
                if(!cartItem){
                    state.items.push(provider);
                }else{
                    cartItem.quantity ++;
                }
                        
            },
            // 清空
            emptyCart(state){
                state.items = [];
            },
            // 删除一个商品, 形参如果有多个,可使用{}
            deleteCartItem(state,idItem){
                // 注意es6语法 findIndex 和 find 使用
                let index = state.items.findIndex(item => item.idItem === idItem)
                state.items.splice(index,1);
            },
            // 解构
            incrementItemQuantity (state, { idItem }) {
                const cartItem = state.items.find(item => item.idItem === idItem)
                cartItem.quantity++;
            },
            decrementItemQuantity (state, { idItem }) {
                const cartItem = state.items.find(item => item.idItem === idItem)
                cartItem.quantity--;
            },
            setItemQuantity (state, {idItem,quantity }) {
                const cartItem = state.items.find(item => item.idItem === idItem)
                cartItem.quantity = quantity;
            },
        },
        // 异步
        actions: {
            //// {commit} 解构 context对象,context与store实例具有相同的属性和方法。这里commit 就是 store.commit
            emptyCartAsync({commit}){
                 setTimeout(()=>{ commit("emptyCart"),3000})
                 },
            addCartAsync: (context,provider) => {
                setTimeout(()=>{ context.commit('addCart',addCartItems),3000})
            },
            /* emptyCartAsync: context => {
                return context.commit('emptyCart')
            } */
        },
        getters:{
            cartItems: state => {
                return state.items;
            }
        }
    })
    
    exportdefault store

    以上代码解析:

    1. 文件头引入 import Vuex from 'vuex'
    2. states区是类变量和初始值,定义一个items: []用于存放购物车商品,这里我直接写了一个商品先放里面,可以直观看到数据结构,也方面后面测试,
    3. mutations: {}中属于”同步”方法,包含一些购物车操作的方法,比如addCartItems(state,provider)是添加商品进购物车,我设计成允许重复添加,如果想不重复,直接返回不同代码即可。
    4. actions: {}是属于”异步”方法区,可以调用mutations: {}同步区的方法,也可自己写,
    5. getters和setters属于vuex基础,略!

     

    4 全局声明

    vehicle-shop-app/ main.js中:

    import Vue from'vue'
    import store from'./store'// 全局存储
    import App from'./App'
    import Request from'./plugins/request/js/index'
    
    //测试用数据
    import Json from'./Json'
    
    import report from'./pages/report/home.vue'
    Vue.component('report',report)
    
    //这里全局引入,并注册为vue组件,相比单页面js引入,使用更方便
    /* import uniNavBar from "./components/uni-nav-bar/uni-nav-bar.vue"
    Vue.component('uniNavBar',uniNavBar) */
    
    /* import cuCustom from './colorui/components/cu-custom.vue'
    Vue.component('cu-custom',cuCustom) */
    
    import uniIcons from"@/components/uni-icons/uni-icons.vue"
    Vue.component('uniIcons',uniIcons)
    
    //设置全局的api地址
    Vue.prototype.websiteUrl = 'http://10.4.14.132:7000';
    
    const msg = (title, duration=1500, mask=false, icon='none')=>{
        //统一提示方便全局修改
        if(Boolean(title) === false){
            return;
        }
        uni.showToast({
            title,
            duration,
            mask,
            icon
        });
    }
    
    const hidemsg = ()=>{
        uni.hideToast()({
        });
    }
    
    const json = type=>{
        // 模拟异步请求数据
        returnnewPromise(resolve=>{
            setTimeout(()=>{
                resolve(Json[type]);
            }, 500)
        })
    }
    
    const prePage = ()=>{
        let pages = getCurrentPages();
        let prePage = pages[pages.length - 2];
        // #ifdef H5
        return prePage;
        // #endif
        return prePage.$vm;
    }
    
    
    Vue.config.productionTip = false
    Vue.prototype.$fire = new Vue();
    Vue.prototype.$store = store;
    Vue.prototype.$api = {msg, hidemsg, json, prePage};
    Vue.prototype.$http = Request;
    
    App.mpType = 'app'
    
    const app = new Vue({
        ...App
    })
    app.$mount()

    以上代码解析:

    1. import store from './store' 引入全局存储
    2. Vue.prototype.$store = store;这样,如果页面需要使用时,举例如下:

        如果使用同步方法:this.$store.commit("deleteCartItem",itemIdToDel)

        如果使用异步方法:this.$store.dispatch("addCartAsync",itemIdToAdd)

     

    5 添加进购物车

    vehicle-shop-app/pages/product/product.vue

    商品详细页面:

     

    js关键代码:

    // 加入购物车
    addCartItem(){
        // vuex保存
        this.$store.commit('addCartItems',this.product);
        uni.showToast({
            title: "加购物车成功!",
            icon: 'info'
        });    
    },

    6 购物车管理:

    vehicle-shop-app/pages/order/cart.vue

    这个物品就是前面vuex购物车默认的一个物品,

     展示下JS部分的代码:

    <script>
        import { mapGetters, mapState,mapActions,mapMutations } from'vuex'
        import uniNumberBox from'@/components/uni-number-box.vue'
        exportdefault {
            components: {
                uniNumberBox
            },
            data() {
                return {
                    total: 0, //总价格
                    allChecked: false, //全选状态  true|false
                    empty: false, //空白页现实  true|false
                    cartList: [],
                    hasLogin: true,
                };
            },
            activated() {
                /* 解决 由订单页返回购物车页,购物车却为空的问题  */
                /* 解决 由订单页返回购物车页,购物车却为空的问题  */
                // 只要进入该页面就进行刷新,因为onLoad()只加载一次,
                // https://blog.csdn.net/qq_27047215/article/details/98943080
                this.loadData();
            },
            onLoad(){
                this.loadData();
            },
            watch:{
                //显示空白页
                cartList(e){
                    let empty = e.length === 0 ? true: false;
                    if(this.empty !== empty){
                        this.empty = empty;
                    }
                }
            },
            computed:{
                // ...mapState(['hasLogin']),
                ...mapGetters(['cartItems'])
            },
            methods: {
                // 引入后可直接使用
                ...mapActions(['emptyCartAsync','addCartAsync']),
                ...mapMutations(['addCartItems','emptyCart','deleteCartItem']),
                //自动计算折扣价
                setDiscountPrice:function(item){
                    // item.discountPrice =
                    },
                //请求数据
                loadData(){            
                    // 从vuex中取缓存
                    // 这里因为cartItems放computed中,自动成为一个data,
                    let list = this.cartItems;
                    let cartList = list.map(item=>{
                        item.checked = true;
                        return item;
                    });
                    this.cartList = cartList;
                    this.calcTotal();  //计算总价
                },
                //监听image加载完成
                onImageLoad(key, index) {
                    this.$set(this[key][index], 'loaded', 'loaded');
                },
                //监听image加载失败
                onImageError(key, index) {
                    this[key][index].image = '/static/errorImage.jpg';
                },
                navToLogin(){
                    uni.navigateTo({
                        url: '/pages/login/login-home'
                    })
                },
                 //选中状态处理
                check(type, index){
                    if(type === 'item'){
                        this.cartList[index].checked = !this.cartList[index].checked;
                    }else{
                        const checked = !this.allChecked
                        const list = this.cartList;
                        list.forEach(item=>{
                            item.checked = checked;
                        })
                        this.allChecked = checked;
                    }
                    this.calcTotal(type);
                },
                //数量
                numberChange(data){
                    console.log(JSON.stringify(data))
                    // 修改缓存中的数量
                    this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number })                
                    this.cartList[data.index].quantity = data.number;
                    this.calcTotal();
                },
                //删除
                deleteCartItem(index){
                    let list = this.cartList;
                    let row = list[index];
                    let id = row.id;
                    // 删除vuex中对象
                    let itemIdToDel = this.cartList[index].id;
                    // this.deleteCartItem(0);
                    this.$store.commit("deleteCartItem",itemIdToDel)
                    this.cartList.splice(index, 1);
                    this.calcTotal();
                    uni.hideLoading();
                },
                //清空
                clearCart(){
                    uni.showModal({
                        content: '清空购物车?',
                        success: (e)=>{
                            if(e.confirm){
                                // vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效
                                this.emptyCart();
                                // this.$store.commit("emptyCart")
                                // this.$store.dispatch("emptyCartAsync");
                                this.cartList = [];
                            }
                        }
                    })
                },
                //计算总价
                calcTotal(){
                    let list = this.cartList;
                    if(list.length === 0){
                        this.empty = true;
                        return;
                    }
                    let total = 0;
                    let checked = true;
                    list.forEach(item=>{
                        if(item.checked === true){
                            total += item.discountPrice * Number(item.quantity);
                        }elseif(checked === true){
                            checked = false;
                        }
                    })
                    this.allChecked = checked;
                    this.total = Number(total.toFixed(2));
                },
                //创建订单
                createOrder(paidStatus){                
                let list = this.cartList;
                let goodsData = [];
                list.forEach(item=>{
                    if(item.checked){
                        goodsData.push({
                            attr_val: item.attr_val,
                            number: item.quantity
                        })
                    }
                })
    
                this.cartList = [];
                // this.$api.msg('跳转下一页 sendData');            
                uni.navigateTo({
                    url: `/pages/order/createOrder?paidStatus=${JSON.stringify(paidStatus)}`
                })
                }
            }
        }
    </script>

    以上代码解析:

    1. activated(){}和onLoad(){}都包含了this.loadData()做页面数据加载,为什么?这是vue生命周期决定的,因为onLoad()只加载一次,系统会自动缓存页面内容,如果你跑到商品页添加商品再返回购物车页,购物车却不显示,activated可以让页面每次进来都刷新一次,这样,购物车里就能实时更新了!
    2. computed:{...mapGetters(['cartItems'])}中,这是vuex语法糖,import { mapGetters, mapState,mapActions,mapMutations } from 'vuex'之后,就可以直接使用'cartItems'变量了,系统会自动生成,看loadData()中就是let list = this.cartItems;
    3. 数量修改:
    4. //数量
      numberChange(data){
              console.log(JSON.stringify(data))
              // 修改缓存中的数量
              this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number })                
              this.cartList[data.index].quantity = data.number;
              this.calcTotal();
      },
    5. 清空购物车方法clearCart(),这里演示了三种使用vuex的模式:一是配合import相关的map辅助函数,然后直接使用this.emptyCart(); 二是同步方法this.store.dispatch("emptyCartAsync"); 殊途同归!请君自选!
    6. clearCart(){
             uni.showModal({
                 content: '清空购物车?',
                 success: (e)=>{
                     if(e.confirm){
                         // vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效
                         this.emptyCart();
                         // this.$store.commit("emptyCart")
                         // this.$store.dispatch("emptyCartAsync");
                         this.cartList = [];
                     }
                 }
             })
      },

    这样,购物车打造完毕!只要用户不关闭app,打开购物车页面,里面商品就会存在,当然,别忘了,提交订单时,清空下购物车,

    因为出了超市,购物车得还给人家,不能带回家!

     

    7 async/await化异步为同步

    前面一篇,说到后台请求数据都是异步的,处理不好就是页面渲染完毕,结果后台数据才过来,这就尴尬了。所以这里我举个例子解决

    下这个问题:

    vehicle-shop-app/pages/product/list.vue

      

    async switchChange(item){
      item.checked = !item.checked;
      // console.log(JSON.stringify(item));
      if(item.checked){
        // 获取商品详细
        let requestItem={};
        await Request().request({
          url: 'stock/vehicle/stock/item/uid/'+ item.itemUuid,
          method: 'get',
          header: {},
          params: {}
        }).then(
            res => {
              // 返回的对象,多一层data封装,故写为response.data
              requestItem = res.data;
          }).catch(err => {
            console.error('is catch', err)
            this.err = err;
            })
                        
        // 设置数量默认值
        requestItem = Object.assign(requestItem,{
          discountPrice: requestItem.sellPrice,
        })
        requestItem.quantity = 1;
        //加入vuex缓存,commit是同步方法
        // this.$store.commit('addCartItems',requestItem);
        this.toAddItemList.push(requestItem);
        //修改角标值
        this.totalChecked += 1;
        this.setStyle(1,true,this.totalChecked);
        uni.showToast({
              title: "选择商品成功!",
            icon: 'info',
            duration: 300
            });
      }else{
        // this.$store.commit("deleteCartItem",item)
        // 删除临时数组中的值
        let index = this.toAddItemList.findIndex(item=>item.itemUuid === requestItem.itemUuid);
        this.toAddItemList.splice(index,10);
        this.totalChecked -= 1;
        this.setStyle(1,true,this.totalChecked);
        uni.showToast({
              title: "取消商品成功!",
            icon: 'info',
            duration: 300
            });
            }                    
    },

    代码解析:以上代码中switchChange()方法,用于响应商品勾选发生变化的,得先去后台找到这个数据,然后做处理,先使用 async 修饰,

    说明这个方法是个异步的方法,然后对异步的部分使用await 修饰,这样,系统发起阻塞,只有await后面的部分运行完毕,才会继续运行后面的代码!

    重点就是: await后面必须一定是返回Promise对象,不管你是封装的函数还是代码块,否则写了await无效果!如果君想试试效果,建议多写几个

    console.log(“A/B/C”)放不同位置,打印下,看谁先打印,就有印象了,其实async/await就是早期promise.then()语法的现代版本,

    补充:

    1. 实际代码和页面很可能和我上面说到不一样,因为需求在变,我代码也一直在更新,我尽量保留代码痕迹。

    全文完!


    我的其他文章:

      只写原创,敬请关注

      

  • 相关阅读:
    如何创建支持64位的安装程序
    SharePoint Server 2013开发之旅(四):配置工作流开发和测试环境
    SharePoint Server 2013开发之旅(三):为SharePoint Server配置App开发、部署、管理环境
    SharePoint Server 2013开发之旅(二):使用在线的开发人员网站进行SharePoint App开发
    SharePoint Server 2013开发之旅(一):新的开发平台和典型开发场景介绍
    在WPF应用程序中利用IEditableObject接口实现可撤销编辑的对象
    一个在ASP.NET中利用服务器控件GridView实现数据增删改查的例子
    关于未捕获异常的处理(WPF)
    牛刀小试:使用Reactive Extensions(Rx),对短时间内多次发生的事件限流
    如何对SharePoint网站进行预热(warmup)以提高响应速度
  • 原文地址:https://www.cnblogs.com/xxbiao/p/12482834.html
Copyright © 2011-2022 走看看