思路分析:
为了方便后续进行购物车模块的开发,我们在这一节将购物车所有的接口封装成接口 API 函数
落地代码:
import http from '../utils/http'
/**
* @description 获取购物车列表数据
* @returns Promise
*/
export const reqCartList = () => {
return http.get('/mall-api/cart/getCartList')
}
/**
* @description 加入购物车
* @param {*} data
* @returns Promise
*/
export const reqAddCart = (data) => {
return http.get(`/cart/addToCart/${data.goodsId}/${data.count}`, data)
}
/**
* @description 更新商品的选中状态
* @param {*} goodsId 商品 id
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqUpdateChecked = (goodsId, isChecked) => {
return http.get(`/cart/checkCart/${goodsId}/${isChecked}`)
}
/**
* @description 全选和全不选
* @param {*} isChecked 商品的选中状态
* @returns Promise
*/
export const reqCheckAllCart = (isChecked) => {
return http.get(`/cart/checkAllCart/${isChecked}`)
}
/**
* @description 删除购物车商品
* @param {*} goodsId 商品 id
* @returns Promise
*/
export const reqDelCart = (goodsId) => {
return http.get(`/cart/delete/${goodsId}`)
}
业务介绍:
点击加入购物车和立即购买的时候,展示购物弹框,在弹框中需要用户选择购买数量和祝福语
点击加入购物车和立即购买,触发的是同一个弹框。
因此点击弹框中的确定按钮时,我们需要区分当前是加入购物车操作还是立即购买操作。
这时候定义一个状态 buyNow
做区分,buyNow
等于 1 代表是立即购买,否则是加入购物车
产品需求
如果点击的是加入购物车,需要将当前商品加入到购物车
如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品
如果是立即购买,不支持购买多个商品
结构分析:
点击立即购买和加入购物车的时候,通过 show 属性,控制弹框的隐藏和展示
<van-goods-action>
+ <van-goods-action-button text="加入购物车" type="warning" bindtap="handleAddcart" />
+ <van-goods-action-button text="立即购买" bindtap="handeGotoBuy" />
van-goods-action>
<van-action-sheet show="{{ show }}" bind:close="onClose">
<view class="sheet-wrapper">
+ <view class="buy-btn" wx:if="{{ buyNow === 0 }}">
<van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
view>
view>
van-action-sheet>
点击立即购买和加入购物车的时候,通过 buyNow 属性,来区分是进行的某种操作
Page({
// 页面的初始数据
data: {
goodsInfo: {}, // 商品详情
show: false, // 加入购物车和立即购买时显示的弹框
count: 1, // 商品购买数量,默认是 1
blessing: '', // 祝福语
+ buyNow: '' // 是否立即购买
},
// 加入购物车
handleAddcart() {
this.setData({
show: true,
+ buyNow: 0
})
},
// 立即购买
handeGotoBuy() {
this.setData({
show: true,
+ buyNow: 1
})
},
// 代码略...
})
思路分析:
当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。
我们需要使用 Token
进行判断,因此需要让页面和 Store
对象建立关联。
这时候可以使用 BehaviorWithStore
让页面 和 Store
对象建立关联。
落地代码:
➡️ /behaviors/userBehavior.js
// 导入 BehaviorWithStore 让页面和 Store 对象建立关联
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入用户 Store
import { userStore } from '@/stores/userstore'
export const userBehavior = BehaviorWithStore({
storeBindings: {
store: userStore,
fields: ['token']
}
})
➡️ /behaviors/userBehavior.js
import { reqGoodsInfo } from '@/api/goods'
import { reqAddCart } from '@/api/cart'
+ import { userBehavior } from '@/behaviors/userBehavior'
Page({
+ behaviors: [userBehavior],
// 代码略...
})
思路分析:
点击加入购物车以及立即购买以后,需要先判断是否进行了登录,如果用户没有登录过,需要先跳转到登录页面进行登录。
如果点击的是 加入购物车,我们只需要调用 加入购物车 接口即可 (需要获取商品的 ID 、购买数量、祝福语)
如果点击的是 立即购买,我们需要携带参数跳转到商品结算页面 (获取商品的 ID 以及 祝福语跳转到结算页面)
购买数量的限制有 4 个限制,这 4 个限制直接使用 Vant
组件提供的属性进行限制即可:
1
,最大是200
1
,则重置为1
200
,则重置为200
1
实现步骤:
Stepper
步进器组件,通过value
设置输入值,同时绑定change
事件,并将值同步到 data
中落地代码:
➡️ /modules/goodsModule/pages/detail/detail.html
<van-stepper
value="{{ count }}"
+ integer
+ min="1"
+ max="200"
bind:change="onChangeGoodsCount"
/>
➡️ /modules/goodsModule/pages/detail/detail.js
// 监听是否更改了购买数量
onChangeGoodsCount(event) {
// 将最新的购买数量同步到 data
this.setData({
count: Number(event.detail)
})
},
// 弹框的确定按钮
async handleSubmit() {
// 解构获取数据
const { token, count, blessing, buyNow } = this.data
const goodsId = this.goodsId
// 如果没有 token ,让用户新登录
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 将用户输入的值转成 Number 类型
const count = Number(event.detail)
// 验证购买数量的正则
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
// 使用正则验证
const res = reg.test(count)
// 如果验证没有通过,直接返回,不执行后续的逻辑
if (!res) return
// 加入购物车
if (buyNow === 0) {
// 加入购物车
const res = await reqAddGood({ goodsId, count, blessing })
if (res.code === 200) {
wx.showToast({
title: '加入购物车成功'
})
this.setData({
show: false
})
}
} else {
// 立即购买
wx.navigateTo({
url: `/pages/order/detail/index?goodsId=${goodsId}&blessing=${blessing}`
})
}
}
思路分析:
判断用户是否进行了登录。
如果没有登录过,则不展示购物车商品的数量。
如果用户登录过,则需要展示购物车商品的数量,则获取购物车列表数据,通过累加计算得出商品购买数量
实现步骤:
token
是否存在落地代码:
➡️ /modules/goodsModule/pages/detail/detail.js
Page({
data: {
// coding...
+ allCount: '' // 购物车商品总数量
},
// 弹框的确定按钮
async handleSubmit() {
// 如果没有 token ,让用户新登录
if (!this.data.token) {
wx.navigateTo({
url: '/pages/login/login'
})
return
}
// 解构获取数据
const { count, blessing, allCount } = this.data
const goodsId = this.goodsId
// 加入购物车
if (this.data.buyNow === 0) {
// 加入购物车
const res = await reqAddCart({ goodsId, count, blessing })
if (res.code === 200) {
wx.toast({
title: '加入购物车成功',
icon: 'success',
mask: false
})
+ // 购物车购买数量合计
+ this.getCartCount()
this.setData({
show: false
})
}
} else {
// 立即购买
wx.navigateTo({
url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}`
})
}
},
+ // 计算购买数量
+ async getCartCount() {
+ // 如果没有 token ,说明用户是第一次访问小程序,没有进行登录过
+ if (!this.data.token) return
+
+ // 获取购物的商品
+ const res = await reqCartList()
+
+ if (res.data.length !== 0) {
+ // 购物车商品累加
+ let allCount = 0
+
+ // 获取购物车商品数量
+ res.data.forEach((item) => {
+ allCount += item.count
+ })
+
+ // 将购物车购买数量赋值
+ this.setData({
+ // 展示的数据要求是字符串
+ allCount: (allCount > 99 ? '99+' : allCount) + ''
+ })
+ }
+ },
onLoad(options) {
// 接收传递的商品 ID,并且将 商品 ID 挂载到 this 上面
this.goodsId = options.goodsId
// 调用获取商品详情数据的方法
this.getGoodsInfo()
+ // 计算购买数量
+ this.getCartCount()
}
// coding...
})
思路分析:
当用户进入购物车页面时时,需要判断用户是否进行了登录来控制页面的展示效果
这时候我们就需要使用 Token
进行判断,因此需要让页面和 Store
对象建立关联。
因为购物车页面采用的 Component
方法进行构建
这时候可以使用 ComponentWithStore
让页面 和 Store
对象建立关联。
落地代码:
➡️/pages/cart/components/cart.js
+ import { ComponentWithStore } from 'mobx-miniprogram-bindings'
+ import { userStore } from '@/stores/userstore'
+ import { reqCartList } from '@/api/cart'
+ ComponentWithStore({
+ storeBindings: {
+ store: userStore,
+ fields: ['token']
+ },
// 组件的初始数据
data: {
cartList: [],
emptyDes: '还没有添加商品,快去添加吧~'
},
+ // 组件的方法列表
+ methods: {
+ // 处理页面的展示
+ async showTipList() {
+ // 将 token 进行解构
+ const { token } = this.data
+
+ console.log(token)
+ },
onShow() {
+ this.showTipList()
}
}
})
思路分析:
如果没有进行登录,购物车页面需要展示文案:您尚未登录,点击登录获取更多权益
如果用户进行登录,获取购物车列表数据
购物车没有商品,展示文案: 还没有添加商品,快去添加吧~
购物车列表有数据,需要使用数据对页面进行渲染
实现步骤:
API
函数onShow
钩子中,根据产品的需求,处理页面的提示落地代码:
➡️/pages/cart/cart.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList } from '@/api/cart'
ComponentWithStore({
storeBindings: {
store: userStore,
fields: ['token']
},
// 组件的初始数据
data: {
cartList: [],
emptyDes: '还没有添加商品,快去添加吧~'
},
// 组件的方法列表
methods: {
+ // 获取购物车列表数据 + 处理页面的展示
+ async showTipGetList() {
+ // 将 token 进行解构
+ const { token } = this.data
+
+ // 1. 如果没有登录,购物车列表,展示文案:您尚未登录,点击登录获取更多权益
+ if (!token) {
+ this.setData({
+ emptyDes: '您尚未登录,点击登录获取更多权益',
+ cartList: []
+ })
+
+ return
+ }
+
+ // 获取商品列表数据
+ const { data: cartList, code } = await reqCartList()
+
+ if (code === 200) {
+ // 2. 如果用户登录,购物车列表为空,展示文案: 还没有添加商品,快去添加吧~
+ this.setData({
+ cartList,
+ emptyDes: cartList === 0 && '还没有添加商品,快去添加吧~'
+ })
+ }
+ },
// 页面展示时触发
onShow() {
+ this.showTipGetList()
}
}
})
➡️/pages/cart/components/cart.wxml
<view>
<view
wx:if="{{ token && cartList.length }}"
class="container goods-wrap"
bindtap="onSwipeCellPageTap"
>
<view class="cart-wrap">
+ <view class="goods-item" wx:for="{{ cartList }}" wx:key="id">
<van-swipe-cell class="goods-swipe" right-width="{{ 65 }}">
<view class="goods-info">
<view class="left">
<van-checkbox
checked-color="#FA4126"
+ value="{{ item.checked }}"
>van-checkbox>
view>
<view class="mid">
+ <image class="img" src="{{ item.imageUrl }}" />
view>
<view class="right">
+ <view class="title"> {{ item.name }} view>
<view class="buy">
<view class="price">
<view class="symbol">¥view>
+ <view class="num">{{ item.price }}view>
view>
<view class="buy-btn">
+ <van-stepper value="{{ item.count }}" />
view>
view>
view>
view>
<view slot="right" class="van-swipe-cell__right">删除view>
van-swipe-cell>
view>
view>
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
<van-checkbox value="{{ true }}" checked-color="#FA4126"> 全选 van-checkbox>
van-submit-bar>
view>
<van-empty wx:else description="{{ emptyDes }}">
+ <navigator url="/pages/index/index" wx:if="{{ token }}">
+ <van-button round type="danger" class="bottom-button">去购物van-button>
+ navigator>
+
+ <navigator url="/pages/login/login" wx:else>
+ <van-button round type="danger" class="bottom-button">去登录van-button>
+ navigator>
van-empty>
view>
思路分析:
点击商品的复选框时,更新商品的购买状态。
实现步骤:
API
函数reqUpdateGoodStatus
,并传参落地代码:
➡️ /pages/cart/cart.wxml
<van-checkbox
checked-color="#FA4126"
+ value="{{ item.isChecked }}"
+ bind:change="updateChecked"
+ data-id="{{ item.goodsId }}"
+ data-index="{{ index }}"
>van-checkbox>
➡️ /pages/cart/cart.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
+ import { reqCartList, reqUpdateChecked } from '@/api/cart'
Component({
// coding...
// 组件的方法列表
methods: {
// 切换商品的选中状态
async updateChecked(event) {
// 获取最新的选中状态
const { detail } = event
// 获取商品的索引和 id
const { id, index } = event.target.dataset
// 将最新的状态格式化成后端所需要的数据格式
const isChecked = detail ? 1 : 0
// 调用接口,传入参数,更新商品的状态
const res = await reqUpdateChecked(id, isChecked)
// 如果数据更新成功,需要将本地的数据一同改变
if (res.code === 200) {
this.setData({
[`cartList[${index}].isChecked`]: isChecked
})
}
},
// 获取购物车列表数据
async getCartList() {
// coding...
}
}
})
思路分析:
购物车列表中每个商品的状态 isCheckd
都是 1,说明每个商品都需要进行购买。
这时候就需要控制底部工具栏全选按钮的选中效果。
基于购物车列表中已有的数据,产生一个新的数据,来控制全选按钮的选中效果,可以使用 计算属性 来实现。
安装 框架拓展 computed
# 安装并构建 框架拓展 computed
npm i miniprogram-computed
实现步骤:
cart
组件中引入 miniprogram-computed
,然后再 behaviors
中进行注册computed
配置项,新增 allStatus
函数用来判断是否是全选落地代码:
➡️ /pages/cart/cart.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList, reqUpdateChecked } from '@/api/cart'
+ const computedBehavior = require('miniprogram-computed').behavior
ComponentWithStore({
+ // 注册计算属性
+ behaviors: [computedBehavior],
+ computed: {
+ // 判断是否全选
+ // computed 函数中不能访问 this ,只有 data 对象可供访问
+ // 这个函数的返回值会被设置到 this.data.selectAllStatus 字段中
+ selectAllStatus(data) {
+ return (
+ data.cartList.length !== 0 && data.cartList.every((item) => item.isChecked === 1)
+ )
+ }
+ }
// 其他代码略...
})
➡️ /pages/cart/cart.wxml
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
+ <van-checkbox value="{{ selectAllStatus }}" checked-color="#FA4126">
全选
van-checkbox>
van-submit-bar>
思路分析:
点击全选,控制所有商品的选中与全不选效果
实现步骤:
API
函数reqCheckAll
,并传参落地代码:
➡️ /pages/cart/cart.wxml
<van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
<van-checkbox
value="{{ selectAllStatus }}"
checked-color="#FA4126"
bind:change="changeAllStatus"
>
全选
van-checkbox>
van-submit-bar>
➡️ /pages/cart/cart.js
ComponentWithStore({
// coding...
methods: {
// coding...
// 全选和全不选功能
async updateAllStatus(event) {
// 获取全选和全不选的状态
const isChecked = event.detail ? 1 : 0
// 调用接口,更新服务器中商品的状态
const res = await reqCheckAllStatus(isChecked)
// 如果更新成功,需要将本地的数据一同改变
if (res.code === 200) {
// 将数据进行拷贝
const newCart = JSON.parse(JSON.stringify(this.data.cartList))
// 将数据进行更改
newCart.forEach((item) => (item.isChecked = isChecked))
// 进行赋值
this.setData({
cartList: newCart
})
}
},
// coding...
}
})
思路分析:
在输入框中输入购买的数量,并**不是直接将输入的数量同步给服务器,而是需要计算差值
**,服务器端进行处理
差值的计算公式:
差值 = 新值 - 旧值
例如:
1. 原来是 1,用户输入 11, 差值是:11 - 1 = 10,传递给服务器的是:10,服务器接收到 10 + 1 = 11
2. 原来是 11,用户输入 5, 差值是:5 - 11 = -6,传递给服务器的是:-6,服务器接收到 -6 + 11 = 5
📌 注意事项:
更新购买数量 和 加入购物车,使用的是同一个接口,为什么加入购物车没有计算差值,
这是因为在加入购物车以后,服务器对商品购买数量直接进行了累加。
例如:之前购物车添加了某个商品,购买数量是 1 个,商品详情又加入 1 个, 直接累加,在购物车显示购买 2 个
思路分析:
1
,最大是200
200
,输入框购买数量需要重置为200
1
,还原为之前的购买数量const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
实现步骤:
id
和 商品的购买之前的购买数量 num
落地代码:
➡️ /pages/cart/cart.wxml
<van-stepper
+ integer
+ min="1"
+ max="200"
value="{{ item.count }}"
+ data-id="{{ item.goodsId }}"
+ data-oldbuynum="{{ item.count }}"
+ data-index="{{ index }}"
+ bindchange="changeBuyNum"
/>
➡️ /pages/cart/cart.js
// 更新购买的数量
async changeBuyNum(event) {
// 获取最新的购买数量,
// 如果用户输入的值大于 200,购买数量需要重置为 200
// 如果不大于 200,直接返回用户输入的值
let buynum = event.detail > 200 ? 200 : event.detail
// 获取商品的 ID 和 索引
const { id: goodsId, index, oldbuynum } = event.target.dataset
// 验证用户输入的值,是否是 1 ~ 200 直接的正整数
const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
// 对用户输入的值进行验证
const regRes = reg.test(buynum)
// 如果验证没有通过,需要重置为之前的购买数量
if (!regRes) {
this.setData({
[`cartList[${index}].count`]: oldbuynum
})
return
}
// 如果通过,需要计算差值,然后将差值发送给服务器,让服务器进行逻辑处理
const disCount = buynum - oldbuynum
// 如果购买数量没有发生改变,不发送请求
if (disCount === 0) return
// 发送请求:购买的数量 和 差值
const res = await reqAddCart({ goodsId, count: disCount })
// 服务器更新购买数量成功以后,更新本地的数据
if (res.code === 200) {
this.setData({
[`cartList[${index}].count`]: buynum
})
}
}
思路分析:
每次改变购物车购买数量的时候,都会触发 changeBuyNum
事件处理程序,这会频繁的向后端发送请求,给服务器造成压力
我们希望用户在输入最终的购买数量,或者停止频繁点击加、减的以后在发送请求,在将购买数量同步到服务器。
这时候就需要使用 防抖 来进行代码优化。
Licia
是实用 JavaScript
工具库,该库目前拥有超过 400 个模块,同时支持浏览器、node 及小程序运行环境。可以极大地提高开发效率。
落地代码:
➡️ /pages/cart/cart.js
// 从 miniprogram-licia 导入防抖函数
import { debounce } from 'miniprogram-licia'
// 更新购买的数量
+ changeBuyNum: debounce(async function (event) {
+ // 代码略...
+ }, 500)
思路分析:
在订单提交栏位置,展示要购买商品的总金额。
需要判断购物车中哪些商品被勾选,然后将勾选商品的价格进行累加。
当用户更新了商品的状态,或者更新了商品的购买数量,我们都需要重新计算订单总金额。
我们需要基于购物车列表的数据,产生订单总金额,在这里我们使用依然使用 computed 来实现商品合计的功能
实现步骤:
computed
配置项,新增 totalPrice
函数用来计算商品价格总和落地代码:
➡️ /pages/cart/cart.wxml
<van-submit-bar
wx:if="{{ cartList.length }}"
price="{{ totalPrice }}"
button-text="去结算"
tip="{{ true }}"
>
<van-checkbox
value="{{ selectAllStatus }}"
checked-color="#FA4126"
bindchange="selectAllStatus"
>
全选
van-checkbox>
van-submit-bar>
➡️ /pages/cart/cart.js
ComponentWithStore({
// coding...
// 定义计算属性
computed: {
// coding...
// 计算商品价格总和
totalPrice(data) {
let totalPrice = 0
data.cartList.forEach((item) => {
// 如果商品的 isChecked 属性等于,说明该商品被选中的
if (item.isChecked === 1) {
totalPrice += item.count * item.price
}
})
return totalPrice
}
},
// coding...
})
思路分析:
点击删除按钮的时候,需要将对应的购物车商品进行删除
实现步骤:
API
函数,同时导入处理删除自动关闭效果的 behaviors
并进行注册API
函数,在删除购物车商品成功以后,给用户提示落地代码:
➡️ /pages/cart/components/cart.wxml
+ <view bindtap="onSwipeCellPage">
<van-swipe-cell
class="goods-swipe"
right-width="{{ 65 }}"
+ id="swipe-cell-{{ item.goodsId }}"
+ bind:open="swipeCellOpen"
+ bind:click="onSwipeCellClick"
>
<van-cell-group border="{{ false }}">
<view class="goods-info">
<view class="left">
<van-checkbox
checked-color="#FA4126"
value="{{ item.isChecked }}"
bindchange="updateChecked"
data-id="{{ item.goodsId }}"
data-index="{{ index }}"
>van-checkbox>
view>
<view class="mid">
<image class="img" src="{{ item.imageUrl }}" />
view>
<view class="right">
<view class="title"> {{ item.name }} view>
<view class="buy">
<view class="price">
<view class="symbol">¥view>
<view class="num">{{ item.price }}view>
view>
<view class="buy-btn">
<van-stepper
min="1"
max="200"
integer
value="{{ item.count }}"
data-id="{{ item.goodsId }}"
data-index="{{ index }}"
data-oldbuynum="{{ item.count }}"
bindchange="changeBuyNum"
/>
view>
view>
view>
view>
van-cell-group>
<view
slot="right"
class="van-swipe-cell__right"
+ bindtap="delCartGoods"
+ data-id="{{ item.goodsId }}"
>
删除
view>
van-swipe-cell>
view>
➡️ /pages/cart/components/cart.wxml
// 导入接口 API 函数
import {
reqCartList,
reqUpdateChecked,
reqCheckAllStatus,
reqAddCart,
+ reqDelCartGoods
} from '@/api/cart'
+ // 导入让删除滑块自动弹回的 behavior
+ import { swipeCellBehavior } from '@/behaviors/swipeCell'
ComponentWithStore({
// 注册 behavior
+ behaviors: [swipeCellBehavior, computedBehavior],
// 组件的方法列表
methods: {
// coding...
+ // 删除购物车中的商品
+ async delCartGoods(event) {
+ // 获取需要删除商品的 id
+ const { id } = event.currentTarget.dataset
+
+ // 询问用户是否删除该商品
+ const modalRes = await wx.modal({
+ content: '您确认删除该商品吗 ?'
+ })
+
+ if (modalRes) {
+ await reqDelCartGoods(id)
+
+ this.showTipGetList()
+ }
+ },
+ onHide() {
+ // 在页面隐藏的时候,需要让删除滑块自动弹回
+ this.onSwipeCellCommonClick()
+ }
}
})