本文已参与「新人创作礼」活动,一起开启掘金创作之路。
之前章节传送:
【Vue项目实战】vue.js2.5 饿了么APP(1)概述+项目准备 juejin.cn/post/708900…
【Vue项目实战】vue.js2.5饿了么APP(2)主要组件实现 - 头部相关组件 - 【速看】面试教你有话可说 juejin.cn/post/708932…
【Vue项目实战】vue.js2.5饿了么APP(3)主要组件实现 - 购物车相关组件(上)juejin.cn/post/708900…
【Vue项目实战】vue.js2.5饿了么APP(4)主要组件实现 - 购物车相关组件(下)juejin.cn/post/709075…
速看
在商品页面中点击某一个商品,可以跳到商品详情页查看详情,包括:商品图片、名称价格、商品评价、加入购物车。该组件为全屏弹层组件,如果单纯作为goods的子组件调用会出现不能全屏显示的问题。 因此将其注册为create-API模块,让good组件动态挂载到body下。(这样就可以使用fixed全屏布局以及层级的z-index来控制是否盖住下面的header部分。)(原因:food是fixed布局,外部有一个transform,fixed布局会受到transform影响,因此对于全屏类弹层组件,组件的样式很容易受到父元素影响,(fixed布局是全屏类的))同样的,该页面也需要控制shop-cart-sticky购物车组件显隐。
评论部分有两个维度实现过滤:首先可以查看评价类型(全部、推荐、吐槽)还有“只看有内容的评价”,将其该部分设置为rating-select组件,接收props(ratings是个数组(通过ratings可以计算各部分的评价个数);selectType是选择类型(有三种类型,定义三种常量POSITIVE、NEGATIVE、ALL)使用常量增加可读性;onlycontent是否有内容;desc描述(对象默认描述:all:"全部" positive:“满意” negative:“不满意”) )外部可以传入标签,从而实现复用。在food中添加计算属性computedRatings,定义数组并遍历将符合的ratings放入(当选中所有时push(rating)或者当前选择类型和这条评价类型相同,也push(rating))从而按情况显示ratings
小掘友的话:本文充斥着很口语化的字样,在于友友希望做完一个项目之后可以很好的表达出来,面试被提问到的时候也有话可说、能够表达清除。
一、food组件-商品部分
1. 概述
在商品页面中点击某一个商品,可以跳到商品详情页查看详情,包括:商品图片、名称价格、商品评价、加入购物车。该组件为全屏弹层组件,如果单纯作为goods的子组件调用会出现不能全屏显示的问题。 因此将其注册为create-API模块,让good组件动态挂载到body下。(这样就可以使用fixed全屏布局以及层级的z-index来控制是否盖住下面的header部分。)(原因:food是fixed布局,外部有一个transform, fixed布局会受到transform影响,因此对于全屏类弹层组件,组件的样式很容易受到父元素影响,(fixed布局是全屏类的))同样的,该页面也需要控制shop-cart-sticky购物车组件显隐。
评论部分有两个维度实现过滤:首先可以查看评价类型(全部、推荐、吐槽)还有“只看有内容的评价”,将其该部分设置为rating-select组件,接收props(ratings是个数组(通过ratings可以计算各部分的评价个数);selectType是选择类型(有三种类型,定义三种常量POSITIVE、NEGATIVE、ALL)使用常量增加可读性;onlycontent是否有内容;desc描述(对象默认描述:all:"全部" positive:“满意” negative:“不满意”) )外部可以传入标签,从而实现复用。在food中添加计算属性computedRatings,定义数组并遍历将符合的ratings放入(当选中所有时push(rating)或者当前选择类型和这条评价类型相同,也push(rating))从而按情况显示ratings.
(在rating-select中的每个选项添加点击事件select,toggle 。由于selectType是传递过来的props不能直接修改,因此把这个props的值派发到父组件food,让后让父组件做修改,响应式更新数据,通过props再影响子组件。)
2. 布居
当点击某一商品时点击可以查看详情,主要分为:头部图片+商品购物+商品信息+评价
商品评价可以有选项分类,有全部、推荐、吐槽,并且有一个按钮可以只看有内容的评价
二期:goods组件需要基于API实现
<div class="food" v-show="visible">
<cube-scroll :data="computedRatings" ref="scroll">
<div class="food-content">
<div class="image-header"></div>
<div class="content">
<div class="cart-control-wrapper">
<cart-control @add="addFood" :food="food"></cart-control>
</div>
<transition name="fade"><div class="buy">加入购物车</div> </transition>
</div>
<split v-show="food.info"></split>
<div class="info" v-show="food.info"></div>
<split></split>
<div class="rating">
<rating-select></rating-select>
<div class="rating-wrapper"></div>
</div>
</div>
</cube-scroll>
</div>
</transition>
需要接收food这个props,需要根据不同的food渲染不同内容,开始是隐藏的,通过通过使用visible控制显隐,其中的对应左滑的过渡动画,内部使用cube-scroll组件实现滚动.
3. 实现
(1)添加头图
使用height:0 padding-top:100%的方式,让图片可以达到等比效果
(2)使用声明式调用food(未采用)
即在goods组件中引入food组件。使用food组加需要传入:food="selectedFood",在goods组件中,当选中列表的一项时,添加点击事件selectFood(food),并实现方法(将food赋值给selectedFood)。在data中声明一个当前选中的商品selectedFood.并且给添加一个ref="food"
this.selectedFood = food
this.$refs.food.show()
}
发现效果是在下面区域显示整个food组件而没有遮盖头部部分。
原因: food是fixed布局,外部有一个transform,fixed布局会受到transform影响,因此对于全屏类弹层组件,组件的样式很容易受到父元素影响,(fixed布局是全屏类的)
解决方法: 使用create-API模块,让good组件动态挂载到body下。这样就可以使用fixed全屏布局以及层级的z-index来控制是否盖住下面的header部分。
(3)注册api组件
定义私有方法_showFood(),当选择的food变化了,selectedFood响应式变化
this.selectedFood = food
this._showFood()
this._showShopCartSticky()
},
_showFood() {
this.foodComp = this.foodComp || this.$createFood({
$props: {
food: 'selectedFood'
},
$events: {
leave: () => {
this._hideShopCartList()
},
add: (el) => {
this.shopCartStickyComp.drop(el)
}
}
})
this.foodComp.show()
},
(4)实现food 组件内部滚动
scroll每次需要初始化,在food组件中添加created
created() {
this.$on(EVENT_SHOW, () => {
this.$nextTick(() => {
this.$refs.scroll.refresh()
})
})
},
(5)使用sticky组件
在goods组件中,打开food组件时,同时要将shop-cart-sticky组件显示出来。同时定义showSopCartSticky()方法
this.selectedFood = food
this._showFood()
this._showShopCartSticky()
},
_showShopCartSticky() {
this.shopCartStickyComp = this.shopCartStickyComp || this.$createShopCartSticky({
$props: {
selectFoods: 'selectFoods',
deliveryPrice: this.seller.deliveryPrice,
minPrice: this.seller.minPrice,
fold: true
}
})
this.shopCartStickyComp.show()
},
_hideShopCartSticky() {
this.shopCartStickyComp.hide()
}
当动画结束时隐藏sticky组件,在food组件添加过渡钩子函数after-leave=afterLeave
this.$emit(EVENT_LEAVE) //leave
},
并且在goods组件中的——showFood中添加event(hideShopCartSticky)
this.foodComp = this.foodComp || this.$createFood({
$props: {
food: 'selectedFood'
},
$events: {
leave: () => {
this._hideShopCartSticky()
},
add: (el) => {
this.shopCartStickyComp.drop(el)
}
}
})
this.foodComp.show()
},
(6)在商品详情页进行购物
比之前的cart-control多一步,在没有购买时会显示“加入购物车”,
实现:使用两层,一层是使用cat-control组件,另一层是一个按钮,并且使用组件包裹,驱动动画的变化,通过food.count控制显隐,当count是0或者undefined,就是显示的,否则相反。都是用绝对定位,让“加入购物车”部分盖住cart-control。
<cart-control @add="addFood" :food="food"></cart-control>
</div>
<transition name="fade">
<div @click="addFirst" class="buy" v-show="!food.count">加入购物车</div>
</transition>
并且在“加入购物车”添加addFirst方法,(没有count时设置为1)点击按钮驱动外层的shop-cart-sticky组件,因此需要派发EVENT_ADD;在cart-control添加addFood方法(事件向父组件传递,food组件的父组件是goods,里面含有shopCartStickyComp,它的drop()方法才是真正调用小球动画的,因此事件需要一层层派发)
this.$set(this.food, 'count', 1)
this.$emit(EVENT_ADD, event.target)
},
addFood(target) {
this.$emit(EVENT_ADD, target)
},
这是在goods中监听add方法完成动画
this.foodComp = this.foodComp || this.$createFood({
$props: {
food: 'selectedFood'
},
$events: {
leave: () => {
this._hideShopCartSticky()
},
add: (el) => {
this.shopCartStickyComp.drop(el)
}
}
})
this.foodComp.show()
},
二、food组件-评价部分
1. 布局
除了有对评价的显示,还有对评价的过滤,可以对评价列表进行切换。
<div class="rating-type border-bottom-1px">
<span @click="select(2)" class="block positive" :class="{'active': selectType===2}">{{desc.all}}
<span class="count">{{ratings.length}}</span>
</span>
<span @click="select(0)" class="block positive" :class="{'active': selectType===0}">{{desc.positive}}
<span class="count">{{positives.length}}</span>
</span>
<span @click="select(1)" class="block positive" :class="{'active': selectType===1}">{{desc.negative}}
<span class="count">{{negatives.length}}</span>
</span>
</div>
<div @click="toggle" class="switch" :class="{'on':onlyContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有内容的评价</span>
</div>
</div>
2. 实现
(1)rating-select组件接收数据
设置props,外部可以传入的变量。
接收ratings是个数组(通过ratings可以计算各部分的评价个数);selectType是选择类型(有三种类型,定义三种常量POSITIVE、NEGATIVE、ALL)使用常量增加可读性;onlycontent是否有内容;desc描述(对象默认描述:all:"全部" positive:“满意” negative:“不满意”)
const NEGATIVE = 1
const ALL = 2
props: {
ratings: {
type: Array,
default() {
return []
}
},
selectType: {
type: Number,
default: ALL
},
onlyContent: {
type: Boolean,
default: false
},
desc: {
type: Object,
default() {
return {
all: '全部',
positive: '满意',
negative: '不满意'
}
}
}
},
添加计算属性positive negative,根据服务端约定数据得到,计算每个部分的数量
positives() {
return this.ratings.filter((rating) => {
return rating.rateType === POSITIVE
})
},
negatives() {
return this.ratings.filter((rating) => {
return rating.rateType === NEGATIVE
})
}
},
(2)rating-select组件使用
在food组件写入date,包括onlyContent,selectType,desc(“全部、推荐、吐槽”)
return {
onlyContent: true,
selectType: ALL,
desc: {
all: '全部',
positive: '推荐',
negative: '吐槽'
}
}
},
然后在rating-select中绑定数据
:ratings="ratings"
:onlyContent="onlyContent"
:selectType="selectType"
:desc="desc"
@select="onSelect"
@toggle="onToggle">
</rating-select>
(3)添加点击事件
在rating-select中的每个选项添加点击事件(如上代码),并写方法select,toggle
const EVENT_TOGGLE = 'toggle'
methods: {
select(type) {
this.$emit(EVENT_SELECT, type)
},
toggle() {
this.$emit(EVENT_TOGGLE)
}
}
思路:这时不能直接修改selectType,不能修改传递过来的props,因此把这个props的值派发到父组件food,让后让父组件做修改,响应式更新数据,这个数据通过props再影响子组件。 因此在food组件中可以监听这两个方法onselect() ontoggle()控制改变。
onSelect(type) {
this.selectType = type
},
onToggle() {
this.onlyContent = !this.onlyContent
}
}
(在这个地方使用v-model不是很合适)
至此只是完成了交互效果,所有交互的效果应该影响列表,根据onlycontent 和 select计算而来
(4)控制列表显示变化
在food中添加计算属性computedRatings,定义数组遍历,
解释:当选中所有时push(rating)或者当前选择类型和这条评价类型相同,也push(rating)最后返回数组
computedRatings() {
const ret = []
this.ratings.forEach((rating) => {
if (this.onlyContent && !rating.text) {
return
}
if (this.selectType === ALL || this.selectType === rating.rateType) {
ret.push(rating)
}
})
return ret
}
},
使用这个计算属性, 在每一个li中遍历computedRatings,并且当没有评价时显示为“暂无评价”
<li
v-for="(rating, index) in computedRatings"
class="rating-item border-bottom-1px"
:key="index">
</li>
</ul>