五、实现购物车页面功能
5.1 完成购物车顶部导航的功能
- 在购物页面引入顶部导航组件
Navbar.vue注册和使用 - 由于顶部导航只有中间部分显示信息,因此只需要使用中间插槽部分
- 在顶部导航显示购物商品数量
- 使用 vuex 中的
getters方法将cartList数据进行计算长度后返回 - 将返回的长度存储到
cartListLength中 - 再在购物车组件
Cart.vue中的computed中拿到getter中的cartListLength返回给cartLength - 再在页面上渲染商品数量的数据
{{cartLength}}
- 使用 vuex 中的
- 完成顶部导航的样式
5.2 使用 mapGetters 函数改造5.1中的内容
- 抽离
getters中的内容到getters.js中- 获取存储到
state中cartList的数组长度cartLength - 获取存储到
state中的商品数据cartList
- 获取存储到
- 在
Cart组件中引入mapGetters
import { mapGetters } from 'vuex' - 使用对象展开运算符将 getter 混入 computed 对象中
computed: { ...mapGetters(["cartLength"]) }
5.3 完成购物车列表商品卡片
- 使用 Vant 组件中的
Card,Checkbox,CheckboxGroup来实现完整商品卡片 - 创建商品列表组件
CartList.vue并将其引入Cart父组件中,注册并使用 - 在
CartList组件中使用mapGetters来获取 vuex 中的商品数据 - 用
CheckboxGroup组件将Checkbox和Card包裹在一起,使用v-for进行遍历来显示每个商品卡片,实现可选择的商品卡片 - 修改并调试样式,使其满足页面布局
- 实现商品列表的滚动区域,引入
Scroll组件,来让替换原生滚动-
CartList组件中将scroll的父标签上设置高度 - 再在
scroll标签上设置滚动范围,即外层高度减去头部和底部的高度 - 由于添加了购物车数据后可滚动区域的高度发生了变化,因此需要调用已
scroll的刷新activated() { this.$refs.scroll.refresh(); }, - 由于使用了
keep-alive保持状态的功能,需要在activated生命周期函数中去调用该刷新方法,这样在每次进入购物车页面时,由于滚动区域高度有变化重新刷新计算一下
-
5.4 实现添加购物车商品时,已经存在的商品自动加一
- 先查找之前的购物车列表中是否有该商品
- 使用
find函数查找cartList中与商品iid相符的数据,并返回该商品信息let oldProduct = state.cartList.find(item => item.iid === payload.iid)
- 使用
- 然后判断
oldProduct是否为空,即oldProduct是否为true- 使用
if else来判断,当oldProduct为true时,oldProduct.count +=1 - 否则
payload.count = 1,并往cartList中插入一条新的商品数据,并且该商品中带有count属性if (oldProduct){ oldProduct.count += 1 } else { payload.count = 1 state.cartList.push(payload) }
- 使用
- 对 vuex 中的 store 进行重构
-
mutations中的方法尽可能完成单一的事件 -
actions中来完成判断逻辑复杂和异步等操作- 在添加购物车时,采用
dispatch()方法来发送操作 - 将原本
mutations中的addToCartList方法放到actions中 - 而且接受一个与
store实例具有相同方法和属性的context对象 - 因此将 if 判断逻辑中的加1操作和push操作通过
commit提交
mutations: { addCounter(state, payload){ payload.count++ }, addToCart(state, payload){ state.cartList.push(payload) } } actions: { addToCartList(context, payload){ let oldProduct = constext.state.cartList.find(item => item.iid === payload.iid) if (oldProduct){ context.commit("addCounter", oldProduct) } else { payload.count = 1 context.commit("addToCart", payload) } } } - 在添加购物车时,采用
- 将
mutations中的内容进行抽离放到mutations.js文件中 - 将
actions中的内容进行抽离放到actions.js文件中
-
5.5 完成底部提交商品内容
- 创建组件
CartBottomBar组件,并在父组件购物车CartList中引入、注册和使用 - 使用UI Vant 组件中的
SubmitBar提交订单栏组件- 先在
main.js中引入SubmitBar和使用 - 再在
CartBottomBar组件中使用<van-submit-bar/>组件,并且其中包裹<van-checkbox/>用来作为全选按钮
[SubmitBar 提交订单栏]: https://youzan.github.io/vant/#/zh-CN/submit-bar#gao-ji-yong-fa - 调整其组件样式
- 先在
- 将添加到购物车的商品价格计算总数显示在
CartBottomBar组件上的:price中- 在父组件
CartList中的计算属性computed中计算并存储总价格totalPrice - 此处通过 reduce 计算累加,返回一个累加函数的结果
- 注意:由于
reduce对空数组不执行回调,当result数组为空时,会报错 - 因此给
result数组一个初始值0data() { return { result: [0] }; }, computed: { totalPrice() { // 此处通过 reduce 计算累加,返回一个累加函数的结果 // 注意:由于 reduce 对空数组不执行回调,当result数组为空时,会报错 return this.result.reduce((preValue, item) => { return preValue + item.price * item.count; }); } }, - 再将
totalPrice通过父子组件传值的方式传给CartBottomBar的props中的totalPrice - 最后需要将
totalPrice100给到price中:price="totalPrice100"
- 在父组件
- 实现全选反选各种场景功能
-
全选按钮场景分析:
- 全选按钮为选中时,所有商品全部选中
- 当商品全部选中时,全选按钮自动选中
- 全选中后,再次点击全选按钮,所有商品取消选中
在子组件的全选按钮上绑定
checkAll方法,将其发送给父组件CartList-
再在父组件的
<cart-bottom-bar/>上绑定发送过来的事件checkAllChange,通过该事件方法触发全选和反选效果(实现了场景1、3)checkAllChange() { // 通过判断 result 数组的长度与 cartList 数组的长度是否一致来进行取反 if (this.result.length < this.cartList.length) { this.$refs.checkboxGroup.toggleAll(true); } else { this.$refs.checkboxGroup.toggleAll(); } } -
在计算属性
isTotalchecked中判断,当商品全部选中时,将isTotalchecked传递给子组件<cart-bottom-bar/>的props中的totalChecked,并在复选框的v-model指令上使用(实现了场景2)// 判断当商品一一勾选后,全选按钮自动勾选 getTotalChecked() { return this.result.length === this.cartList.length && this.result.length > 0 ? true : false; }, -
注意: 1)如果将子组件中
props的totalChecked直接在v-model指令上使用会出现(第一个vue的告警),虽然不影响功能
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "totalChecked"2) 因此需要采用计算属性
computed将totalChecked的值返回给一个新属性值_totalChecked在在v-model中使用_totalChecked() { return this.totalChecked; }3)但是这又会产生新(第二个 vue 的告警)
[Vue warn]: Computed property "_totalChecked" was assigned to but it has no setter.
4)因此需要给_totalChecked设置set,之后再在v-model中引用就不会保存_totalChecked: { get: function() { return this.totalChecked; }, set: function() {} }
-
- 改造第3步中的计算已勾选商品的价格总数
getTotalPrice() { let arr = this.result; let total = 0; if (arr.length === 0) return 0; for (let j = 0; j < arr.length; j++) { total += arr[j].price * arr[j].count; } return total; },- 并将计算的结果返回给
totalPrice计算属性 - 通过属性绑定父子组件传值的方式,传递到子组件的
totalPrice中,并在界面渲染
- 并将计算的结果返回给
- 上面的第 4 步使用另外一种方式避免出现第一个vue告警的情况:
- 将
CartBottomBar组件内的代码直接写在父组件CartList中,就会避免采用父子组件的传值方式,也就不会出现直接使用props中的totalChecked而导致的第一个vue告警。 - 但第二个告警任然会出现,不过只需要像注意事项的第 4) 条中设置 Set 就可以了。
- 将
5.6 优化添加购物车方法,并引入提示
- 添加购物车成功后要有
toast提示,因此需要进行异步回调,来提示不同的内容 - 在
addToCartList方法中使用Promise函数进行回调- 当添加商品后,若是新增商品则回调
resolve('添加新的商品成功') - 若是只是商品 +1 则回调
resolve('当前的商品数量+1')addToCartList(context, payload) { return new Promise((resolve, reject) => { // 2. 先查找之前的购物车列表中是否有该商品 let oldProduct = context.state.cartList.find(item => item.iid === payload.iid) // 3. 然后判断 oldProduct 是否为空,即 oldProduct 是否为 true, // 不为空就将原本商品的数量加1,为空就往 cartList 插入一条带有 count = 1 属性的新的数据, if (oldProduct) { context.commit("addCounter", oldProduct) resolve('当前的商品数量+1') } else { payload.count = 1 context.commit("addToCart", payload) resolve('添加新的商品成功') } }) }
- 当添加商品后,若是新增商品则回调
- 在
Detail组件中的addToCart方法中对dispatch进行回调的内容用弹窗Toast提示
注意: 引入this.$store.dispatch("addToCartList", products).then(res => { Toast.success(res); });Toast组件时,若已经在main.js中已经引入,但直接使用任然会报错。因此需要在当前组件中再引入一次
5.7 优化图片懒加载的功能
- 需要使用
vue-lazyload组件 - 引入组件
VueLazyload并使用import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload, { loading: require('./assets/img/common/placeholder.png') }) - 在组件
GoodsItem组件中的图片标签中使用v-lazy指令,这样就可以使用懒加载的图片了