五、实现购物车页面功能
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
指令,这样就可以使用懒加载的图片了