第 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,
})
}
源碼地址
在搭建項目前,根據自己需要下載本系列文章的源代碼