移动应用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. 实际代码和页面很可能和我上面说到不一样,因为需求在变,我代码也一直在更新,我尽量保留代码痕迹。

全文完!


我的其他文章:

  只写原创,敬请关注

  

posted @ 2020-03-12 21:45  甲由崽  阅读(1145)  评论(0编辑  收藏  举报