Fork me on GitHub
余鸢

饿了么45个页面重构(八):appear的用法和购物车按钮组件开发

buyCart.vue

增加和减少按钮

顾名思义就是在选择食品数量的时相关动作,增加或减少。

思路:点击增加按钮,当前食品数量+1,也就是说当食品数量>=1时,同时显示减少按钮。当食品数量<1时,减少按钮隐藏。

由于buyCart作为基础组件,需要接收来自父组件的数据,这就要通过props定义相关数据,

1
props:['foods', 'shopId']

当商品数量有值时显示减少图标使用v-if="foodNum" 做判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<section class="cart_module">
<section class="cart_button">
<transition name="showReduce">
<svg @click="removeOutCart(foods.category_id, foods.item_id, foods.specfoods[0].food_id, foods.specfoods[0].name, foods.specfoods[0].price, '', foods.specfoods[0].packing_fee, foods.specfoods[0].sku_id, foods.specfoods[0].stock)" v-if="foodNum">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#cart-minus"></use>
</svg>
</transition>
<transition name="fade">
<span class="cart_num" v-if="foodNum">{{foodNum}}</span>
</transition>
<svg @click="addToCart(foods.category_id, foods.item_id, foods.specfoods[0].food_id, foods.specfoods[0].name, foods.specfoods[0].price, '', foods.specfoods[0].packing_fee, foods.specfoods[0].sku_id, foods.specfoods[0].stock, $event)">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#cart-add"></use>
</svg>
</section>
</section>

当购物车有变化的时候重新计算当前商品的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
computed: {
foodNum (){
let category_id = this.foods.category_id
let item_id = this.foods.item_id
if (this.shopCart&&this.shopCart[category_id]&&this.shopCart[category_id][item_id]) {
let num = 0
Object.values(this.shopCart[category_id][item_id]).forEach((item,index) => {
num += item.num
})
return num
}else {
return 0
}
},
}

监听购物车的变化,更新当前餐厅的购物车信息shopCart,这就需要得到购物车列表cartList的数据。使用vuex获取cartList

1
2
3
4
5
6
7
8
computed: {
shopCart () {
return Object.assign({}, this.cartList[this.shopId])
},
...mapState([
'cartList'
])
}

通过vuex mutation对购物车数据做增减处理。

添加购物车

通过state.cartList获取购物车的数据cart,然后通过cartshopId获取到餐厅shop,接着通过shop的category_id获取餐厅菜单分类category,最后通过categoryitem_id得到商品item。点击一次增加图标商品数量num就会+1,然后设置存入本地存储 setStore('buyCart', state.cartList)

mutations.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[types.ADD_CART] (state, {shopId, category_id, item_id, food_id, name, price, specs, packing_fee, sku_id, stock}) {
let cart = state.cartList
let shop = cart[shopId] = (cart[shopId] || {})
let category = shop[category_id] = (shop[category_id] || {})
let item = category[item_id] = (category[item_id] || {})
if (item[food_id]) {
item[food_id]['num']++
} else {
item[food_id] = {"num": 1, "id": food_id, "name": name, "price": price, "specs": specs, "packing_fee": packing_fee, "sku_id": sku_id, "stock": stock}
}
state.cartList = {...cart}
//存入localStorage
setStore('buyCart', state.cartList)
}

减少购物车

同理和增加购物车思路是一样的,先依次得到各个数据,点击一次减少图标商品数量num就会-1,然后设置存入本地存储 setStore('buyCart', state.cartList),否则商品数量为0,则清空当前商品的信息。

mutations.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[types.REDUCE_CART] (state, {shopId, category_id, item_id, food_id, name, price, specs}) {
let cart = state.cartList
let shop = (cart[shopId] || {})
let category = (shop[category_id] || {})
let item = (category[item_id] || {})
if (item && item[food_id]) {
if (item[food_id]['num'] > 0) {
item[food_id]['num']--
state.cartList = {...cart}
setStore('buyCart', state.cartList)
} else {
item[food_id] = null
}
}
},

回到buyCart.vue组件中,分别对减少和增加图标创建点击事件removeOutCart()addToCart()。引入mapMutation直接对方法做处理就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {mapState, mapMutations} from 'vuex'
methods: {
removeOutCart(category_id, item_id, food_id, name, price, specs, packing_fee, sku_id, stock){
if (this.foodNum > 0) {
this.REDUCE_CART({shopId: this.shopId, category_id, item_id, food_id, name, price, specs, packing_fee, sku_id, stock})
}
},
addToCart(category_id, item_id, food_id, name, price, specs, packing_fee, sku_id, stock, event){
this.ADD_CART({shopId: this.shopId, category_id, item_id, food_id, name, price, specs, packing_fee, sku_id, stock})
},
...mapMutations([
'ADD_CART', 'REDUCE_CART'
])
}

多规格商品购物车

这里首先要v-if和v-else做个判断,如果是多规格商品则显示多规格商品购物车,如果不是则显示增加购物车。

1
2
3
4
5
6
7
8
<section class="cart_module">
<section v-if="!foods.specifications.length" class="cart_button">
....
</section>
<section v-else class="choose_specification">
....
</section>
</section>

和增加减少按钮思路一样,点击增加按钮,当前食品数量+1,也就是说当食品数量>=1时,同时显示减少按钮。当食品数量<1时,减少按钮隐藏。同时对购物车商品数量进行实时监听,操作和前面的一样,这里就不多说了,直接贴代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<section v-else class="choose_specification">
<section class="choose_icon_container">
<transition name="showReduce">
<svg class="specs_reduce_icon" v-if="foodNum" @click="showReduceTip">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#cart-minus"></use>
</svg>
</transition>
<transition name="fade">
<span class="cart_num" v-if="foodNum">{{foodNum}}</span>
</transition>
<transition name="fade">
<p class="show_delete_tip" v-if="showDeleteTip">多规格商品只能去购物车删除哦</p>
</transition>
<span class="show_chooselist" @click="showChooseList">选规格</span>
</section>
</section>

点击选规格按钮触发showChooseList点击事件。控制显示规格列表。data中定义showSpecs默认为false,表示控制显示食品规格。定义specsIndex默认为0,表示当前选中的规格索引值。

1
2
3
4
5
6
7
8
9
10
11
data () {
return {
showSpecs: false
}
}
methods: {
showChooseList(){
this.showSpecs = !this.showSpecs
this.specsIndex = 0
},
}

当点击减少按钮时会弹出提示框,提示用户多规格商品只能去购物车删除,创建showReduceTip()点击事件。data中定义showDeleteTip默认为false,表示点击多规格商品减少按钮时弹出提示框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<p class="show_delete_tip" v-if="showDeleteTip">多规格商品只能去购物车删除哦</p>
data () {
return {
showDeleteTip: false
}
},
methods: {
showReduceTip(){
this.showDeleteTip = true
clearTimeout(this.timer)
this.timer = setTimeout(() => {
clearTimeout(this.timer)
this.showDeleteTip = false
}, 3000)
},
}

显示多规格列表

点击多规格按钮后弹出多规格列表框,对框内dom进行填充数据渲染。多规格列表分为商品名称、商品明细及价格显示和加入购物车按钮。

商品名称

1
2
3
4
5
6
7
8
9
<div class="specs_list" v-if="showSpecs">
<header class="specs_list_header">
<h4 class="ellipsis">{{foods.name}}</h4>
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" version="1.1"class="specs_cancel" @click="showChooseList">
<line x1="0" y1="0" x2="16" y2="16" stroke="#666" stroke-width="1.2"/>
<line x1="0" y1="16" x2="16" y2="0" stroke="#666" stroke-width="1.2"/>
</svg>
</header>
</div>

设置商品名称,对关闭框按钮绑定showChooseList事件。

商品明细

1
2
3
4
5
6
7
8
9
10
11
<div class="specs_list" v-if="showSpecs">
...
<section class="specs_details" >
<h5 class="specs_details_title">{{foods.specifications[0].name}}</h5>
<ul>
<li v-for="(item, itemIndex) in foods.specifications[0].values" :class="{specs_activity: itemIndex == specsIndex}" @click="chooseSpecs(itemIndex)">
{{item}}
</li>
</ul>
</section>
</div>

设置规格名称,遍历规格中的值v-for="(item, itemIndex) in foods.specifications[0].values",同时绑定默认显示哪个index :class="{specs_activity: itemIndex == specsIndex}",点击哪个item后高亮显示同时记录当前所选规格的索引值,触发chooseSpecs(itemIndex)点击事件。

1
2
3
chooseSpecs(index){
this.specsIndex = index
},

价格显示和加入购物车按钮

1
2
3
4
5
6
7
8
9
10
<div class="specs_list" v-if="showSpecs">
...
<footer class="specs_footer">
<div class="specs_price">
<span>¥ </span>
<span>{{foods.specfoods[specsIndex].price}}</span>
</div>
<div class="specs_addto_cart" @click="addSpecs(foods.category_id, foods.item_id, foods.specfoods[specsIndex].food_id, foods.specfoods[specsIndex].name, foods.specfoods[specsIndex].price, foods.specifications[0].values[specsIndex])">加入购物车</div>
</footer>
</div>

设置规格商品价格,选择商品完成后点击加入购物车按钮触发addSpecs()点击事件。

1
2
3
4
addSpecs(category_id, item_id, food_id, name, price, specs){
this.ADD_CART({shopId: this.shopId, category_id, item_id, food_id, name, price, specs})
this.showChooseList()
},

点击加入购物车后记录选择后的商品id,同时关闭多规格列表提示框。

css动画渲染

当点击增加购物车按钮时会生成一个小球有做弧形下落的动画运动操作,同时当购物车数量num有值时显示num,减少购物车按钮做滚动动画显示。这些需要设置transition来实现一系列的动画操作。

初始渲染的过渡

点击添加购物车生成的小球做弧形下落的动画,因为小球动画只有一个方向(只执行单方向从上到下滚落),所以只用了before-enter和after-enter,这里我使用transition的appear特性设置节点在初始渲染的过渡,实现列表的渐进过渡。

appear表示是否在初始渲染使用过渡,也就是当前transition元素第一次渲染时的过渡动画。它的用法和enter的用法相似,它只是在第一次渲染的时候才会起作用。使用appear-class 之前先使用 prop appear,<transition appear appear-class="...." appear-active-class="....">,也可以自定义JavaScript 钩子:<transition appear before-appear="...." after-appear="....">

data中定义showMoveDot默认是[],表示控制下落的小球。遍历下落的小球v-for="(item,index) in showMoveDot"

1
2
3
4
5
6
7
8
9
10
11
12
13
<transition
appear
@before-appear="beforeEnter"
@after-appear = "afterEnter"
v-for="(item,index) in showMoveDot"
:key="index"
>
<span class="move_dot" v-if="item">
<svg class="move_liner">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#cart-add"></use>
</svg>
</span>
</transition>

这里自定义JavaScript 钩子@after-appear = "afterEnter"@before-appear="beforeEnter"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
data () {
return {
showMoveDot: [],
elLeft: 0, //当前点击加按钮在网页中的绝对top值
elBottom: 0, //当前点击加按钮在网页中的绝对left值
windowHeight: null, //屏幕的高度
}
},
mounted(){
this.windowHeight = window.innerHeight
},
methods: {
beforeEnter(el){
el.style.transform = `translate3d(0,${39 + this.elBottom - this.windowHeight}px,0)`
el.children[0].style.transform = `translate3d(${this.elLeft - 40}px,0,0)`
},
afterEnter(el){
el.style.transform = `translate3d(0,0,0)`
el.children[0].style.transform = `translate3d(0,0,0)`
el.style.transition = 'all .55s cubic-bezier(0.3, -0.19, 0.65, -0.15)'
el.children[0].style.transition = 'all .55s linear'
//圆点到达目标点后移出
this.showMoveDot = this.showMoveDot.map(item => false)
//监听运动结束,通知父级进行后续操作
el.children[0].addEventListener('transitionend', () => {
this.$emit('moveInCart')
})
},
}

点击添加购物车按钮小球的运动弧度,如图:

v23

设置减少购物车按钮的滚动动画效果

1
2
3
4
5
6
7
8
9
<transition name="showReduce">...</transition>
.showReduce-enter-active, .showReduce-leave-active {
transition: all .3s ease-out;
}
.showReduce-enter, .showReduce-leave-active {
opacity: 0;
transform: translateX(1rem);
}

设置商品数量的动画

1
2
3
4
5
6
7
8
9
10
<transition name="fade">
<span class="cart_num" v-if="foodNum">{{foodNum}}</span>
</transition>
.fade-enter-active, .fade-leave-active {
transition: all .3s;
}
.fade-enter, .fade-leave-active {
opacity: 0;
}

运动效果如图:

减少按钮动画

到此,购物车按钮及多规格商品按钮组件的开发到此为止!

具体代码见:

https://github.com/kakajing/vue-elmm