【系】微信小程序雲開發實戰堅果商城-前後端交互之購物車實現

第 4-5 課:前後端交互之購物車實現

目錄

1 邏輯處理

client 新建 models/CartModel.js

import { CloudRequest } from '../utils/cloud-request.js'
class CartModel extends CloudRequest {
    _storageKeyName = 'cart';
    constructor() {
        super()
    };

    /**
     * 獲取購物車數據
     * @param {*} callBack 
     */
    getCartData(callBack) {
        callBack(this.getCartDataFromLocal())
    }

    /**
     * 添加到購物車
     * @param {*} item 
     * @param {*} counts 
     * @param {*} callBack 
     */
    add(item, counts, callBack) {
        callBack(this._localAdd(item, counts))
    }

    /**
     * 增加商品數量
     * @param {*} id 
     * @param {*} callBack 
     */
    addCounts(id, callBack) {
        this._changeCounts(id, 1)
        callBack()
    }
    /**
     * 減少數量
     * @param {*} id 
     * @param {*} callBack 
     */
    cutCounts(id, callBack) {
        this._changeCounts(id, -1)
        callBack()
    }

    /**
     * 刪除商品
     * @param {*} ids 
     * @param {*} callBack 
     */
    delete(ids, callBack) {
        callBack(this._delete(ids))
    }

    /********************* 下面本地數據  ***************************/

    /*本地緩存 保存/更新*/
    localSetStorageSync(data) {
        wx.setStorageSync(this._storageKeyName, data);
    }

    /**
     * 加入購物車
     * @param {*} item 商品
     * @param {*} counts 數量
     */
    _localAdd(item, counts) {
        let cartData = this.getCartDataFromLocal();
        if (!cartData) {
            cartData = []
        }
        let isproduct = this._checkProduct(item._id, cartData)
        //新商品
        if (isproduct.index == -1) {
            item.counts = counts
            item.selectStatus = true  //默認在購物車中爲選中狀態
            cartData.push(item)
        }
        //已有商品
        else {
            cartData[isproduct.index].counts += counts
        }
        this.localSetStorageSync(cartData)  //更新本地緩存
        return cartData;

    }

    getCartTotalCounts() {
        let data = this.getCartDataFromLocal()
        let counts = 0
        for (let i = 0; i < data.length; i++) {
            if (data[i].selectStatus) {
                counts++
            }
        }
        return counts
    };




    /*
    * 修改商品數目
    * params:
    * id - {int} 商品id
    * counts -{int} 數目
    * */
    _changeCounts(id, counts) {
        let cartData = this.getCartDataFromLocal()
        let hasInfo = this._checkProduct(id, cartData)
        if (hasInfo.index != -1) {
            if (hasInfo.data.counts >= 1) {
                cartData[hasInfo.index].counts += counts
            }
        }
        this.localSetStorageSync(cartData);  //更新本地緩存
    };

    /*
    * 獲取購物車
    * param
    * flag - {boolean} 是否過濾掉不下單的商品
    */
    getCartDataFromLocal(flag) {
        let res = wx.getStorageSync(this._storageKeyName);
        if (!res) {
            res = []
        }
        //在下單的時候過濾不下單的商品,
        if (flag) {
            let newRes = [];
            for (let i = 0; i < res.length; i++) {
                if (res[i].selectStatus) {
                    newRes.push(res[i])
                }
            }
            res = newRes;
        }

        return res;
    };

    /*購物車中是否已經存在該商品*/
    _checkProduct(id, arr) {
        let item, result = { index: -1 };
        for (let i = 0; i < arr.length; i++) {
            item = arr[i];
            if (item._id == id) {
                result = {
                    index: i,
                    data: item
                }
                break;
            }
        }
        return result;
    }

    /*
   * 刪除某些商品
   */
    _delete(ids) {
        if (!(ids instanceof Array)) {
            ids = [ids];
        }
        let cartData = this.getCartDataFromLocal()
        for (let i = 0; i < ids.length; i++) {
            let hasInfo = this._checkProduct(ids[i], cartData)
            if (hasInfo.index != -1) {
                cartData.splice(hasInfo.index, 1)  //刪除數組某一項
            }
        }
        this.localSetStorageSync(cartData)
    }

}
export { CartModel }

在這裏採用本地緩存購物車,在設計的時候我採用跟之前的 model 相同的實現邏輯,只是在數據的處理沒有請求後臺,計算我們臨時需要把購物車數據放在後臺,我們也只是小動,只需要把 callBack 的回調換成 this.requset 修改一些數據的變化。

2 前臺數據處理

回到我們 pages/cart/cart.js

// pages/cart/cart.js
import { CartModel } from '../../models/CartModel.js'
let cartmodel = new CartModel()
Page({

  /**
   * 頁面的初始數據
   */
  data: {
    cartData: [],
    selectedCounts: 0, //總的商品數
    selectedCheckCounts: 0, //總的商品數  
    account: 0
  },

  /**
   * 生命週期函數--監聽頁面顯示
   */
  onShow: function () {
    this._init()
  },
  // 初始化數據
  _init: function () {
    cartmodel.getCartData(res => {
      this.setData({
        cartData: res,
        account: this._totalAccountAndCounts(res).account,
        selectedCheckCounts: this._totalAccountAndCounts(res).selectedCheckCounts
      })

    })
  },
  // 更新商品數量
  changeCounts: function (event) {
    let id = cartmodel.getDataSet(event, 'id')
    let type = cartmodel.getDataSet(event, 'type')
    let index = this._getProductIndexById(id)
    counts = 1
    if (type == 'add') {
      cartmodel.addCounts(id, res => { })
    } else {
      counts = -1
      cartmodel.cutCounts(id, res => { })
    }
    //更新商品頁面
    this.data.cartData[index].counts += counts
    this._resetCartData()
  },
  /*更新購物車商品數據*/
  _resetCartData: function () {
    let newData = this._totalAccountAndCounts(this.data.cartData) /*重新計算總金額和商品總數*/
    this.setData({
      account: newData.account,
      selectedCounts: newData.selectedCounts,
      selectedCheckCounts: newData.selectedCheckCounts,
      cartData: this.data.cartData
    })
  },
  /*離開頁面時,更新本地緩存*/
  onHide: function () {
    cartmodel.localSetStorageSync(this.data.cartData)
  },
  /*
  * 計算總金額和選擇的商品總數
  * */
  _totalAccountAndCounts: function (data) {
    let len = data.length
    let account = 0
    let selectedCounts = 0
    let selectedCheckCounts = 0
    let multiple = 100
    for (let i = 0; i < len; i++) {
      //避免 0.05 + 0.01 = 0.060 000 000 000 000 005 的問題,乘以 100 *100
      if (data[i].selectStatus) {
        account += data[i].counts * multiple * Number(data[i].product_sell_price) * multiple;
        selectedCounts += data[i].counts
        selectedCheckCounts++
      }
    }
    return {
      selectedCounts: selectedCounts,
      selectedCheckCounts: selectedCheckCounts,
      account: account / (multiple * multiple)
    }
  },
  /*根據商品id得到 商品所在下標*/
  _getProductIndexById: function (id) {
    let data = this.data.cartData
    let len = data.length
    for (let i = 0; i < len; i++) {
      if (data[i]._id == id) {
        return i
      }
    }
  },
  /*刪除商品*/
  delete: function (event) {
    let id = cartmodel.getDataSet(event, 'id')
    let index = this._getProductIndexById(id)
    this.data.cartData.splice(index, 1)//刪除某一項商品
    this._resetCartData()
    cartmodel.delete(id, res => { })  //內存中刪除該商品
  },

  /*選擇商品*/
  toggleSelect: function (event) {
    let id = cartmodel.getDataSet(event, 'id')
    let status = cartmodel.getDataSet(event, 'status')
    let index = this._getProductIndexById(id)
    this.data.cartData[index].selectStatus = !status
    this._resetCartData()
  },
  /*全選*/
  checkall: function (event) {
    let status = cartmodel.getDataSet(event, 'status') == 'true'
    let data = this.data.cartData
    for (let i = 0; i < data.length; i++) {
      data[i].selectStatus = !status
    }
    this._resetCartData()
  },
  /*提交訂單*/
  confirm: function () {
    wx.navigateTo({
      url: '/pages/order/order?account=' + this.data.account + '&from=cart'
    })
  },
  // 訂單詳情
  productDetail: function (event) {
    let product_id = cartmodel.getDataSet(event, "id")
    wx.navigateTo({
      url: '/pages/product/product?product_id=' + product_id,
    })
  }

})


  • _resetCartData : 每次對於購物車的操作,我們都需要重計算商品的數量價格
  • _totalAccountAndCounts : 計算總金額和選擇的商品總數 ,對於金錢的計算一定要精確 否則就會造成不必要的損失
  • _getProductIndexById : 爲了方便處理,我們根據商品的id,拿到數組的下表就能直接修改狀態等
  • 購物車的邏輯相對 比較複雜 業務的需求不一樣 可能很多是是實現也是不一樣的 大致的思路差不多,這裏大家把代碼跑起來

pages/cart/cart.wxml

<!--pages/cart/cart.wxml-->
<view class='container'>
  <view class='cart-container' wx:for="{{cartData}}" wx:key="index">
    <!-- 商品左邊 -->
    <view class='cart-left-container' bindtap="toggleSelect" data-id="{{item._id}}" data-status="{{item.selectStatus}}" >
        <view class="cart-select {{item.selectStatus?'selectActive':''}}"   >
            <icon  class='iconfont iconiconfontcheck'></icon>
        </view>   
    </view>
     <!-- 商品圖片 -->
    <view class='cart-middle-container' data-id="{{item._id}}" bind:tap="productDetail">
        <image src="{{item.product_img}}"></image>
    </view>
    <view class='cart-right-container'>
        <view class='product-basic'> 
          <view class='product-title'>
            <text>{{item.product_name}}</text>
          </view>
          <view class='product-price'>
            <text>¥{{item.product_sell_price}}</text>
          </view>
        </view>  
        <view class='edit-contianer'>
           <view class='edit-num'>
              <icon class="iconfont {{item.counts==1?'disabled':''}} iconjian  "  bind:tap="{{item.counts==1?'':'changeCounts'}}" data-id="{{item._id}}" data-type="cut"></icon>
              <view class='num'> <text>{{item.counts}}</text></view>
              <icon class='iconfont allow iconjia1' bindtap="changeCounts" data-id="{{item._id}}" data-type="add"></icon>
           </view>
           <view class='delete' data-id="{{item._id}}" bindtap="delete">
            <icon class='iconfont iconshanchu'></icon>
           </view>
        </view>
    </view>
  </view>

 <!-- 全選  wx:if="{{checkall?'selectActive':''}}" -->
    <view class='bottom-container'> 
      <view class='all-select'>
        <view class="all-select-icon {{selectedCheckCounts==cartData.length?'selectActive':''}}"  bind:tap='checkall'   data-status="{{selectedCheckCounts==cartData.length?'true':'false'}}" >
          <icon  class='iconfont iconiconfontcheck'></icon>
        </view>
        <view class='all-select-text'>
            <text >全選</text>
        </view>
      </view>
      <view class='total-container' >
        <view class='total-price'>
          <text>合計:</text>        
          <text class='price-symbol' >¥</text>
          <text class='price' >{{account}}</text>
        </view>
        <view class='accounts' bind:tap="confirm">
          <text>結算</text>
        </view>      
      </view>
    </view>
</view>

購物車這塊相比其他的功能要複雜的多,所以大家在寫代碼的時候需要每一個方法是怎麼實現的 ,一個一個方法去理解,不要一看就這麼複雜,然後不知所措,在每一個 js 中帶有_下劃線的開頭的都是在當前類和 js 中調用,因爲博客不必視頻所以很多東西沒有辦法一點一點的貼出來。更多的時候需要一點點的思考和理解。

3 代碼解析

3.1 model 層解析

在這裏我們主要有獲取購物車數據、添加到購物車、增加商品數量、減少數量、刪除商品功能,首先在 model 編寫他們的方法。

   /**
     * 獲取購物車數據
     * @param {*} callBack 
     */
    getCartData(callBack) {
       
    }

    /**
     * 添加到購物車
     * @param {*} item 
     * @param {*} counts 
     * @param {*} callBack 
     */
    add(item, counts, callBack) {
        
    }

    /**
     * 增加商品數量
     * @param {*} id 
     * @param {*} counts 
     * @param {*} callBack 
     */
    addCounts(id, callBack) {
      
    }
    /**
     * 減少數量
     * @param {*} id 
     * @param {*} counts 
     * @param {*} callBack 
     */
    cutCounts(id, callBack) {
        
    }

    /**
     * 刪除商品
     * @param {*} ids 
     * @param {*} callBack 
     */
    delete(ids, callBack) {
       
    }

3.1.1 獲取購物車數據

爲了接口後面的更好的使用,這裏我任然採用獲取後臺數據的形式,採用 callBack 將處理的數據返回調用者。 將本地購物車數據的抽離 getCartDataFromLocal() 方法 ,本地的數據使用 flag 標識是否篩選。

    /**
     * 獲取購物車數據
     * @param {*} callBack 
     */
    getCartData(callBack) {
        callBack(this.getCartDataFromLocal())
    }
    
   /*
    * 獲取購物車
    * param
    * flag - {boolean} 是否過濾掉不下單的商品
    */
    getCartDataFromLocal(flag) {
        // 獲取本地數據購物車
        let res = wx.getStorageSync(this._storageKeyName);
        if (!res) {
            res = []
        }
        //在下單的時候過濾不下單的商品,
        if (flag) {
            let newRes = [];
            for (let i = 0; i < res.length; i++) {
                if (res[i].selectStatus) {
                    newRes.push(res[i])
                }
            }
            res = newRes;
        }

        return res;
    };  

3.1.2 添加到購物車

添加商品需要傳入商品數據和商品數量,添加購物車的時候需要獲取本地數據,判斷本地是否有數據,如果爲空則需要一個空數組否則直接添加會報錯。檢查當前商品是否最新,如果是最新的商品設置數量和選擇的狀態(默認新商品選中),否則就直接修改就商品的數量,更新本地緩存,返回數據。

    /**
     * 添加到購物車
     * @param {*} item 
     * @param {*} counts 
     * @param {*} callBack 
     */
    add(item, counts, callBack) {
        callBack(this._localAdd(item, counts))
    }
    
    /**
     * 加入購物車
     * @param {*} item 商品
     * @param {*} counts 數量
     */
    _localAdd(item, counts) {
        // 獲取本地數據
        let cartData = this.getCartDataFromLocal()
        // 判斷是否有數據
        if (!cartData) {
            cartData = []
        }
        // 判斷是新商品
        let isproduct = this._checkProduct(item._id, cartData)
        //新商品
        if (isproduct.index == -1) {
        	// 商品數量
            item.counts = counts
            //默認在購物車中爲選中狀態
            item.selectStatus = true
            // 添加到數組
            cartData.push(item)
        }
        //已有商品 新舊數量相加
        else {
            cartData[isproduct.index].counts += counts
        }
        //更新本地緩存
        this.localSetStorageSync(cartData)  
        return cartData;

    }
    
    
      /*購物車中是否已經存在該商品*/
    _checkProduct(id, arr) {
        let item, result = { index: -1 };
        for (let i = 0; i < arr.length; i++) {
            item = arr[i];
            if (item._id == id) {
                result = {
                    index: i,
                    data: item
                }
                break;
            }
        }
        return result;
    }
    

3.1.3 修改商品數量

增加商品數量和減少商品數量,每次點擊的數量只能增加和減少 1 ,通過 _changeCounts 方法判斷商品是否存在,修改數量,進行本地緩存更新。

     /**
     * 增加商品數量
     * @param {*} id 
     * @param {*} callBack 
     */
    addCounts(id, callBack) {
        this._changeCounts(id, 1)
        callBack()
    }
    /**
     * 減少商品數量
     * @param {*} id 
     * @param {*} callBack 
     */
    cutCounts(id, callBack) {
        this._changeCounts(id, -1)
        callBack()
    }
	
    
   /*
    * 修改商品數目
    * params:
    * id - {int} 商品id
    * counts - {int} 數目
    * */
    _changeCounts(id, counts) {
    	// 獲取商=購物車數據
        let cartData = this.getCartDataFromLocal()
        // 檢查商品是是否存在
        let hasInfo = this._checkProduct(id, cartData)
        // hasInfo.index 初始值爲 -1  
        if (hasInfo.index != -1) {
            if (hasInfo.data.counts >= 1) {
                cartData[hasInfo.index].counts += counts
            }
        }
         //更新本地緩存
        this.localSetStorageSync(cartData); 
    };

3.1.4 商品刪除

商品的刪除主要通過商品的 id ,這裏採用數組的形式這樣一個或者多個都能直接刪除。

     /**
     * 刪除商品
     * @param {*} ids 
     * @param {*} callBack 
     */
    delete(ids, callBack) {
        callBack(this._delete(ids))
    }
    
   /*
   * 刪除某些商品
   */
    _delete(ids) {
        if (!(ids instanceof Array)) {
            ids = [ids];
        }
        let cartData = this.getCartDataFromLocal()
        for (let i = 0; i < ids.length; i++) {
            let hasInfo = this._checkProduct(ids[i], cartData)
            if (hasInfo.index != -1) {
                cartData.splice(hasInfo.index, 1)  //刪除數組某一項
            }
        }
        this.localSetStorageSync(cartData)
    }

3.2 cart.js 數據分析

3.2.1 頁面初始化

頁面初始化數據購車的顯示的數據,選中的金額,選中的個數。通過 cartmodel 獲取數據,金額的計算和商品的數量 _totalAccountAndCounts 方法,對於價格我們在實際的處理過程中需要特別注意,因爲精度的丟失會影響整個價格的不一致。

// cart.js
// 獲取購物車數據
cartmodel.getCartData(res => {
  this.setData({
    // 訂單數據
    cartData: res,
    // 金額的計算
    account: this._totalAccountAndCounts(res).account,
    // 選中的商品數
    selectedCheckCounts: this._totalAccountAndCounts(res).selectedCheckCounts
  })
})

/*
* 計算總金額和選擇的商品總數
* */
_totalAccountAndCounts: function (data) {
  let len = data.length
  let account = 0
  let selectedCounts = 0
  let selectedCheckCounts = 0
  let multiple = 100
  for (let i = 0; i < len; i++) {
    //避免 0.05 + 0.01 = 0.060 000 000 000 000 005 的問題,乘以 100 *100
    if (data[i].selectStatus) {
      account += data[i].counts * multiple * Number(data[i].product_sell_price) * multiple;
      selectedCounts += data[i].counts
      selectedCheckCounts++
    }
  }
  return {
    selectedCounts: selectedCounts,
    selectedCheckCounts: selectedCheckCounts,
    account: account / (multiple * multiple)
  }
},

3.2.2 商品數量的修改

商品數量的修改分爲二步,第一步直接修改 data 的數據,第二步修改緩存的數據,每次修改數據之後則需要重新計算。每次 wxml 需要傳入商品的 id 和 商品操作 type ,通過 _getProductIndexById 方法獲取當前商品在數組的中的下標,直接修改數組的數量,也需要重置本地緩存保證數據的一致性,修改數據完成通過 _resetCartData 方法重新計算。

  // 更新商品數量
  changeCounts: function (event) {
    // 獲取修改數量的 id
    let id = cartmodel.getDataSet(event, 'id')
    // type : 增加  減少
    let type = cartmodel.getDataSet(event, 'type')
    // 獲取商品在數組的下標
    let index = this._getProductIndexById(id)
    // 默認爲 1 
    let counts = 1
    if (type == 'add') {
      cartmodel.addCounts(id, res => { })
    } else {
      counts = -1
      cartmodel.cutCounts(id, res => { })
    }
    //更新商品頁面
    this.data.cartData[index].counts += counts
    // 更新購物車
    this._resetCartData()
  },
  /*更新購物車商品數據*/
  _resetCartData: function () {
    let newData = this._totalAccountAndCounts(this.data.cartData) /*重新計算總金額和商品總數*/
    this.setData({
      account: newData.account,
      selectedCounts: newData.selectedCounts,
      selectedCheckCounts: newData.selectedCheckCounts,
      cartData: this.data.cartData
    })
  },
   /*根據商品id得到 商品所在下標*/
  _getProductIndexById: function (id) {
    // data 數據
    let data = this.data.cartData
    // 數組的長度
    let len = data.length
    for (let i = 0; i < len; i++) {
      if (data[i]._id == id) {
        return i
      }
    }
  },

3.2.3 商品刪除

獲取商品的 id ,通過 cartmodel.delete 修改內存中緩存的數據,_getProductIndexById 方法獲取 data 的購物車數據,直接從當前的數組移除,移除完成通過 _resetCartData 方法重新計算。

 /*刪除商品*/
  delete: function (event) {
    let id = cartmodel.getDataSet(event, 'id')
    let index = this._getProductIndexById(id)
    this.data.cartData.splice(index, 1)//刪除某一項商品
    this._resetCartData()
    cartmodel.delete(id, res => { })  //內存中刪除該商品
  },

3.2.4 選擇商品

獲取商品的 id 和 商品的狀態 ,通過下標直接修改本地的選擇狀態,每次操作完成都需要調用 _resetCartData 方法重新計算購物車。

 /*選擇商品*/
  toggleSelect: function (event) {
    // 獲取商品 id
    let id = cartmodel.getDataSet(event, 'id')
    // 是否選擇的狀態
    let status = cartmodel.getDataSet(event, 'status')
    // 獲取數組下標
    let index = this._getProductIndexById(id)
    // 當前狀態取反
    this.data.cartData[index].selectStatus = !status
    // 重新計算購物車
    this._resetCartData()
  },

3.2.5 購物車全選

購物車全選首先判斷是否選中,每次取反之後在重新計算購物車。

 /*全選*/
  checkall: function (event) {
     // 判斷是否全選
    let status = cartmodel.getDataSet(event, 'status') == 'true'
    // data 數據
    let data = this.data.cartData
    for (let i = 0; i < data.length; i++) {
      data[i].selectStatus = !status
    }
    // 重新計算
    this._resetCartData()
  },

3.2.6 提交訂單

提交訂單的過程我們需要將總價格,這樣在訂單實現的過程中則不需要重新計算。

 /*提交訂單*/
  confirm: function () {
    wx.navigateTo({
      url: '/pages/order/order?account=' + this.data.account + '&from=cart'
    })
  },
  // 訂單詳情
  productDetail: function (event) {
    let product_id = cartmodel.getDataSet(event, "id")
    wx.navigateTo({
      url: '/pages/product/product?product_id=' + product_id,
    })
  }

3.2.6 訂單詳情

點擊購物車的圖片,獲取商品的 id ,跳轉商品詳情。

  // 訂單詳情
  productDetail: function (event) {
    let product_id = cartmodel.getDataSet(event, "id")
    wx.navigateTo({
      url: '/pages/product/product?product_id=' + product_id,
    })
  }

源碼地址

在搭建項目前,根據自己需要下載本系列文章的源代碼

本項目源碼地址:https://gitee.com/mtcarpenter/nux-shop

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章