一、学习目标
-
实现Shopping Cart 增删改功能。
image.png
二、封装购物车的商品列表展示组件
1、在项目components目录下创建my-goods.vue组件。
<template>
<view>
<!--商品-->
<view class = "goods-item">
<!--商品左侧box-->
<view class = "goods-item-left">
<!-- 存储在购物车中的商品,包含 goods_state 属性,表示商品的勾选状态 -->
<!-- 在 radioChangeHandler 事件处理函数中,通过事件对象 e,得到商品的 goods_id 和 goods_state -->
<!-- radio的默认颜色,在不同平台不一样。微信小程序是绿色的,抖音小程序为红色,其他平台是蓝色的。更改颜色使用color属性。
如需调节radio大小,可通过css的scale方法调节,如缩小到70%style="transform:scale(0.7)"
radio不是checkbox,点击一个已经选中的radio,不会将其取消选中 -->
<radio :checked="goods.goods_state" color = "#c00000" style="transform:scale(0.7)" v-if = "showRadio" ></radio>
<image :src="goods.imgUrl" class = "goods-item-left-image"></image>
</view>
<!--商品右侧区域-->
<view class = "goods-item-right">
<!--商品标题-->
<view class = "goods-name">{{goods.goodsDesc}}</view>
<!--商品价格-->
<view class = "goods-info-box">
<view class = "goods-price">¥{{goods.price}}</view>
<!--商品数字输入框-->
<uni-number-box :min="1" v-if="showNum" :value="goods.goods_count" ></uni-number-box>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
// 定义 props 属性,用来接收外界传递到当前组件的数据
props:{
// 商品的信息对象
goods:{
type:Object,
default:{}
},
//是否展示radio
showRadio:{
type:Boolean,
default: false
},
//是否展示数字框
showNum:{
type:Boolean,
default: false
}
},
data() {
return {
}
},
methods: {
}
</script>
<style lang = "scss">
.goods-item{
display: flex;
padding: 5px 10px;
border-bottom: 1px solid #f0f0f0;
background-color: #FFFFFF;
.goods-item-left{
display: flex;
align-items: center;
margin-right: 5px;
.goods-item-left-image{
width: 100px;
height: 100px;
}
}
.goods-item-right{
display: flex;
flex-direction: column;
justify-content: space-between;
.goods-name{
font-size: 13px;
}
.goods-info-box{
display: flex;
justify-content: space-between;
.goods-price{
font-size: 16px;
color:#c00000
}
}
}
}
</style>
三、渲染购物车商品列表的标题区域
1、 定义如下的 UI 结构。
<!-- 购物车商品列表的标题区域 -->
<view class="cart-title">
<!-- 左侧的图标 -->
<uni-icons type="shop" size="18"></uni-icons>
<!-- 描述文本 -->
<text class="cart-title-text">购物车</text>
</view>
2、美化样式
.cart-title {
height: 40px;
display: flex;
align-items: center;
font-size: 14px;
padding-left: 5px;
border-bottom: 1px solid #efefef;
.cart-title-text {
margin-left: 10px;
}
}
3、 渲染效果
四、渲染商品列表区域的基本结构
1、通过 mapState 辅助函数,将 Store 中的 cart 数组映射到当前页面中使用。
import badgeMix from '@/mixins/tabbar-badge.js'
// 按需导入 mapState 这个辅助函数
import { mapState } from 'vuex'
export default {
mixins: [badgeMix],
computed: {
// 将 m_cart 模块中的 cart 数组映射到当前页面中使用
...mapState('m_cart', ['cart']),
},
data() {
return {}
},
}
3、在 UI 结构中,通过 v-for 指令循环渲染自定义的 my-goods 组件,其中 :showRadio="true" 是展示商品的勾选框,:showNum="true"是展示商品数量的数字输入框。
<!-- 商品列表区域 -->
<block v-for="(goods, i) in cart" :key="i">
<my-goods :goods = "goods" :showRadio="true" :showNum="true" ></my-goods>
</block>
4、渲染效果
五、修改购物车中商品的勾选状态
1、为 my-goods 组件封装 radio-change 事件。
当点击 radio 组件,希望修改当前商品的勾选状态,此时可以为 my-goods 组件绑定自定义事件radio-change ,从而获取当前商品的 id 和 goods_state。
<my-goods :goods = "goods"
:showRadio="true"
:showNum="true"
@radio-change="radioChangeHandler" >
</my-goods>
2、定义 radioChangeHandler 事件处理函数如下。
methods: {
// 商品的勾选状态发生了变化
radioChangeHandler(e) {
console.log(e) // 参数e为当前被点击是否勾选的商品对象信息
}
}
3、在 my-goods.vue 组件中,为 radio 组件绑定 @click 事件处理函数,并用this.$emit('radio-change')方法调用radio-change自定义事件处理函数。
methods: {
// radio 组件的点击事件处理函数
radioChangeHandler(){
// 通过 this.$emit() 触发外界通过 @ 绑定的 radio-change 事件,
// 同时把商品的 Id 和 勾选状态 作为参数传递给 radio-change 事件处理函数
this.$emit('radio-change',{
//商品ID
id:this.goods.id,
//商品状态
goods_state: !this.goods.goods_state
})
},
}
4、在 store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的勾选状态。
/**
* 根据商品ID修改购物车中的商品状态
* @param {Object} state 当前storeage中已缓存的购物车中的商品信息
* @param {Object} goods 需要更改状态的商品信息
*/
updateGoodsState(state,goods){
// 根据 商品id 查询购物车中对应商品的信息对象
const findResult = state.cart.find(x=>x.id===goods.id)
// 如果有对应的商品信息对象
if(findResult){
// 更新对应商品的勾选状态
findResult.goods_state = goods.goods_state
// 持久化存储到本地缓存
this.commit('m_cart/saveToStorage')
}
},
2、在购物车cart.vue中,导入 mapMutations 这个辅助函数,从而将需要的 mutations 方法映射到当前页面中使用。
import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'
export default {
mixins: [badgeMix],
computed: {
...mapState('m_cart', ['cart']),
},
data() {
return {}
},
methods: {
//将updateGoodsState方法映射到当前页面
...mapMutations('m_cart', ['updateGoodsState']),
// 商品的勾选状态发生了变化
radioChangeHandler(e) {
this.updateGoodsState(e)
},
},
}
六、修改购物车中商品的数量
1、为 my-goods 组件封装num-change 事件。
当修改了 NumberBox 的值以后,希望将最新的商品数量更新到购物车中,此时可以为 my-goods 组件绑定 @num-change 事件,从而获取当前商品的id 和goods_count。
<my-goods
:goods = "goods"
:showRadio="true"
:showNum="true"
@radio-change="radioChangeHandler"
@num-change = "numChangeHandler">
</my-goods>
2、定义 numberChangeHandler 事件处理函数。
// 商品的数量发生了变化
numberChangeHandler(e) {
console.log(e)
}
3、在 my-goods.vue 组件中,为 uni-number-box 组件绑定 @change 事件处理函数。并用this.$emit('num-change')方法调用radio-change自定义事件处理函数。
//number组件的改变事件
changeHandler(val){
this.$emit('num-change',{
// 商品的 Id
id:this.goods.id,
//商品的最新数量
goods_count: +val //无法保证val是数字
})
}
4、在 store/cart.js 模块中,声明如下的 mutations 方法,用来修改对应商品的数量。
/**
* 根据商品ID修改购物车中的商品数量
* @param {Object} state 当前storeage中已缓存的购物车的商品信息
* @param {Object} goods 需要更改数量的商品信息
*/
updateGoodsCount(state,goods){
// 根据 goods_id 查询购物车中对应商品的信息对象
const findResult = state.cart.find(x=>x.id === goods.id)
//如果有对应的商品
if(findResult){
//更新对应商品的数量
findResult.goods_count = goods.goods_count
//将更改的数据持久化到本地缓存
this.commit('m_cart/saveToStorage')
}
},
5、在 购物车页面cart.vue 中,通过 mapMutations 这个辅助函数,将需要的 mutations 方法映射到当前页面中使用。
import badgeMix from '@/mixins/tabbar-badge.js'
import { mapState, mapMutations } from 'vuex'
export default {
mixins: [badgeMix],
computed: {
...mapState('m_cart', ['cart']),
},
data() {
return {}
},
methods: {
//将updateGoodsState方法映射到当前页面
//将updateGoodsCount方法映射到当前页面
...mapMutations('m_cart', ['updateGoodsState', 'updateGoodsCount']),
// 商品的勾选状态发生了变化
radioChangeHandler(e) {
this.updateGoodsState(e)
},
// 商品的数量发生了变化
numberChangeHandler(e) {
this.updateGoodsCount(e)
},
},
}
七、 实现滑动删除的功能
1、改造购物车cart.vue 页面的 UI 结构,将商品列表区域的结构修改如下(可以使用 uniSwipeAction 代码块快速生成基本的 UI 结构)。
<!-- 商品列表区域 -->
<!-- uni-swipe-action 是最外层包裹性质的容器 -->
<uni-swipe-action>
<block v-for="(goods, i) in cart" :key="i">
<!-- uni-swipe-action-item 可以为其子节点提供滑动操作的效果。需要通过 options 属性来指定操作按钮的配置信息 -->
<uni-swipe-action-item :options="options" @click="swipeActionClickHandler(goods)">
<my-goods :goods="goods" :show-radio="true" :show-num="true" @radio-change="radioChangeHandler" @num-change="numberChangeHandler"></my-goods>
</uni-swipe-action-item>
</block>
</uni-swipe-action>
2、在 data 节点中声明 options 数组,用来定义操作按钮的配置信息。
data() {
return {
options: [{
text: '删除', // 显示的文本内容
style: {
backgroundColor: '#C00000' ,// 按钮的背景颜色
fontSize:'12px' //显示的文本内容的字体大小
}
}]
};
},
3、渲染效果
4、在 methods 中声明 uni-swipe-action-item 组件的 @click 事件处理函数。
// 点击了滑动操作按钮
swipeActionClickHandler(goods) {
console.log(goods)
}
3、在 store/cart.js 模块的 mutations 节点中声明如下的方法,从而根据商品的 Id 从购物车中移除对应的商品。
/**
* 根据商品ID删除商品
* @param {Object} state 当前storeage中已缓存的购物车的数据
* @param {Object} goods 需要删除的商品数据
*/
removeGoods(state,goods){
// 调用数组的 filter 方法进行过滤
state.cart = state.cart.filter(x=> x.id !== goods.id)
//将更改的数据持久化到本地缓存
this.commit('m_cart/saveToStorage')
},
},
4、在 购物车页面cart.vue 中,使用 mapMutations 辅助函数,把需要的方法映射到当前页面中使用。
methods:{
//将updateGoodsState方法映射到当前页面
//将updateGoodsCount方法映射到当前页面
//将removeGoods方法映射到当前页面
...mapMutations ('m_cart', ['updateGoodsState','updateGoodsCount','removeGoods']),
// 商品的勾选状态发生了变化
radioChangeHandler(e){
//修改购物车里的商品是否选中的状态
this.updateGoodsState(e)
},
//商品的数量发生了变化
numChangeHandler(e){
//修改购物车里的商品的数量
this.updateGoodsCount(e)
},
// 点击了滑动操作按钮
swipeActionClickHandler(goods) {
//删除购物车中的商品
this.removeGoods(goods)
}
}
八、部分代码
1、my-goods.vue组件完整代码
<template>
<view>
<!--商品-->
<view class = "goods-item">
<!--商品左侧box-->
<view class = "goods-item-left">
<!-- 存储在购物车中的商品,包含 goods_state 属性,表示商品的勾选状态 -->
<!-- 在 radioChangeHandler 事件处理函数中,通过事件对象 e,得到商品的 goods_id 和 goods_state -->
<!-- radio的默认颜色,在不同平台不一样。微信小程序是绿色的,抖音小程序为红色,其他平台是蓝色的。更改颜色使用color属性。
如需调节radio大小,可通过css的scale方法调节,如缩小到70%style="transform:scale(0.7)"
radio不是checkbox,点击一个已经选中的radio,不会将其取消选中 -->
<radio :checked="goods.goods_state" color = "#c00000" style="transform:scale(0.7)" v-if = "showRadio" @click="radioChangeHandler"></radio>
<image :src="goods.imgUrl" class = "goods-item-left-image"></image>
</view>
<!--商品右侧区域-->
<view class = "goods-item-right">
<!--商品标题-->
<view class = "goods-name">{{goods.goodsDesc}}</view>
<!--商品价格-->
<view class = "goods-info-box">
<view class = "goods-price">¥{{goods.price}}</view>
<!--商品数字输入框-->
<uni-number-box :min="1" v-if="showNum" :value="goods.goods_count" @change="changeHandler"></uni-number-box>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
// 定义 props 属性,用来接收外界传递到当前组件的数据
props:{
// 商品的信息对象
goods:{
type:Object,
default:{}
},
//是否展示radio
showRadio:{
type:Boolean,
default: false
},
//是否展示数字框
showNum:{
type:Boolean,
default: false
}
},
data() {
return {
}
},
methods: {
// radio 组件的点击事件处理函数
radioChangeHandler(){
// 通过 this.$emit() 触发外界通过 @ 绑定的 radio-change 事件,
// 同时把商品的 Id 和 勾选状态 作为参数传递给 radio-change 事件处理函数
this.$emit('radio-change',{
//商品ID
id:this.goods.id,
//商品状态
goods_state: !this.goods.goods_state
})
},
//number组件的改变事件
changeHandler(val){
this.$emit('num-change',{
id:this.goods.id,
goods_count: +val //val-0 无法保证val是数字-0
})
}
}
}
</script>
<style lang = "scss">
.goods-item{
display: flex;
padding: 5px 10px;
border-bottom: 1px solid #f0f0f0;
background-color: #FFFFFF;
.goods-item-left{
display: flex;
align-items: center;
margin-right: 5px;
.goods-item-left-image{
width: 100px;
height: 100px;
}
}
.goods-item-right{
display: flex;
flex-direction: column;
justify-content: space-between;
.goods-name{
font-size: 13px;
}
.goods-info-box{
display: flex;
justify-content: space-between;
.goods-price{
font-size: 16px;
color:#c00000
}
}
}
}
</style>
2、store/cart.js完整代码
// cart.js
export default {
// 为当前模块开启命名空间
namespaced: true,
// 模块的 state 数据
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
cart: JSON.parse(uni.getStorageSync('cart')||'[]'),
}),
// 模块的 mutations 方法
mutations: {
addToCart(state, goods) {
// 根据提交的商品的Id,查询购物车中是否存在这件商品
// 如果不存在,则 findResult 为 undefined;否则,为查找到的商品信息对象
const findResult = state.cart.find((x) => x.id === goods.id)
if (!findResult) {
// 如果购物车中没有这件商品,则直接 push
state.cart.push(goods)
} else {
// 如果购物车中有这件商品,则只更新数量即可
findResult.goods_count++
}
// 通过 commit 方法,调用 m_cart 命名空间下的 saveToStorage 方法
this.commit('m_cart/saveToStorage')
},
/**
* 根据商品ID修改购物车中的商品状态
* @param {Object} state 当前storeage中已缓存的购物车的数据
* @param {Object} goods 需要更改状态的商品数据
*/
updateGoodsState(state,goods){
// 根据 goods_id 查询购物车中对应商品的信息对象
const findResult = state.cart.find(x=>x.id===goods.id)
// 如果有对应的商品信息对象
if(findResult){
// 更新对应商品的勾选状态
findResult.goods_state = goods.goods_state
// 持久化存储到本地
this.commit('m_cart/saveToStorage')
}
},
/**
* 根据商品ID修改购物车中的商品数量
* @param {Object} state 当前storeage中已缓存的购物车的数据
* @param {Object} goods 需要更改数量的商品数据
*/
updateGoodsCount(state,goods){
// 根据 goods_id 查询购物车中对应商品的信息对象
const findResult = state.cart.find(x=>x.id === goods.id)
//如果有对应的商品
if(findResult){
//更新对应商品的数量
findResult.goods_count = goods.goods_count
//将更改的数据持久化到本地缓存
this.commit('m_cart/saveToStorage')
}
},
/**
* 根据商品ID删除商品
* @param {Object} state 当前storeage中已缓存的购物车的数据
* @param {Object} goods 需要删除的商品数据
*/
removeGoods(state,goods){
// 调用数组的 filter 方法进行过滤
state.cart = state.cart.filter(x=> x.id !== goods.id)
//将更改的数据持久化到本地缓存
this.commit('m_cart/saveToStorage')
},
// 将购物车中的数据持久化存储到本地
saveToStorage(state){
uni.setStorageSync('cart',JSON.stringify(state.cart))
}
},
// 模块的 getters 属性
getters: {
// 统计购物车中商品的总数量
total(state){
let c = 0
// 循环统计商品的数量,累加到变量 c 中
state.cart.forEach(goods=>c+=goods.goods_count)
return c
}
},
}
3、购物车商品展示页面cart.vue完整代码
<template>
<view>
<!--购物车标题区域开始-->
<view class = "cart-title">
<uni-icons type="shop" size="18"></uni-icons>
<text class = "cart-title-text">购物车</text>
</view>
<!--购物车标题区域结束-->
<!--购物车列表区域开始-->
<uni-swipe-action>
<block v-for="(goods,i) in cart" :key = "i">
<!--将购物车store中的数据传递给my-goods组件-->
<!--此处需要向下兼容 option需要替换成right-option 详情请阅读uniapp官方文档中uni-swipe-action-item组件使用说明-->
<uni-swipe-action-item
:right-options="options"
@click="swipeActionClickHandler(goods)">
<my-goods :goods = "goods" :showRadio="true" :showNum="true" @radio-change="radioChangeHandler" @num-change = "numChangeHandler"></my-goods>
</uni-swipe-action-item>
</block>
</uni-swipe-action>
<!--购物车列表区域结束-->
</view>
</template>
<script>
//导入封装mixin
import badgeMixin from '@/mixins/tabbar_badge.js'
// 按需导入 mapState 这个辅助函数
import {mapState,mapMutations } from 'vuex'
export default {
//导入封装mixin 渲染购物车徽标
mixins:[badgeMixin],
computed:{
...mapState('m_cart',['cart'])
},
data() {
return {
options: [{
text: '删除', // 显示的文本内容
style: {
backgroundColor: '#C00000' ,// 按钮的背景颜色
fontSize:'12px'
}
}]
};
},
methods:{
...mapMutations ('m_cart', ['updateGoodsState','updateGoodsCount','removeGoods']),
// 商品的勾选状态发生了变化
radioChangeHandler(e){
//修改购物车里的商品是否选中的状态
this.updateGoodsState(e)
},
//商品的数量发生了变化
numChangeHandler(e){
//修改购物车里的商品的数量
this.updateGoodsCount(e)
},
// 点击了滑动操作按钮
swipeActionClickHandler(goods) {
console.log(goods)
this.removeGoods(goods)
}
}
}
</script>
<style lang="scss">
.cart-title{
height: 40px;
border-bottom: 1px solid #efefef;
display: flex;
align-items: center;
font-size: 14px;
padding-left: 5px;
background-color: #FFFFFF;
.cart-title-text{
margin-left: 10px;
}
.goods-item {
// 让 goods-item 项占满整个屏幕的宽度
width: 750rpx;
// 设置盒模型为 border-box
box-sizing: border-box;
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #f0f0f0;
}
.uni-swipe_button-text{
font-size: 13px;
}
}
</style>