移動應用APP購物車(店鋪系列二)

 

今天仍是說移動app開發,店鋪系列文章,咱們常常去超市使用購物車,即一個臨時的儲物空間,用完清空回收。我大兄弟說,前端

平時很忙,錄入訂單的目錄不少,臨時有事回來要能夠繼續填寫,提交訂單後纔算結束,這就是一個典型的購物車場景了。那vue

系統的購物車如何實現?如今就來實戰一把,作個如淘寶類的購物車。git

 

準備:es6

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

/Vue2.5/OSS/Hbuilder2.6.1web

難度: 新手--戰士--老兵--大師sql

目標:vuex

  1. 手機APP前端實現購物車功能
  2. async/await使用

步驟:數據庫

爲了碰見各類問題,同時保持時效性,我儘可能使用最新的軟件版本。代碼地址:https://github.com/xiexiaobiao/vehicle-shop-mobile.gitjson

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. 實際代碼和頁面極可能和我上面說到不同,由於需求在變,我代碼也一直在更新,我儘可能保留代碼痕跡。

全文完!


個人其餘文章:

  只寫原創,敬請關注

  

相關文章
相關標籤/搜索