开发food.vue组件实现商品的详情页展示,首先写布局,组件正常情况下是隐藏的,要添加一个v-show的条件,food组件要维护的是goods组件中循环的每一个food的详情页,所以我们用props接收父组件goods传过来的food数据
<transition name="fade"> <!-- 为food界面的展开添加动画-->
<div v-show="showFlag" class="food" ref="food">
</div>
</transition>
props: { //接收传入的food,此时的food是被选中点开详情页的那个food
food: {
type: Object
}
}
food首先是根据屏幕做定位的,他的position被设置为fixed
.food
position fixed
left: 0
top: 0
bottom 48px // 底部留出购物车的空间
z-index 30 // 小于底部的购物车,也要小于购物车弹出的mask图层
width 100%
background #ffffff
写好food组件的基本样式,便可以回到food组件的父组件goods组件中,在循环foods的时候添加一个点击事件,维护一个selectedFood值,点击了其中某一个food就将food的值传入selectedFood中,在通过selectedFood传到food组件中,selectedFood在data中进行维护
首先在模板中添加food组件
<shopcart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
<food :food="selectedFood" ref="food"></food> <!-- data中的selectedFood传递进来-->
但是我没现在还不知道选中的selectedFood是哪一个,首先在data中定义这个被选中的food为空
data() {
return {
selectedFood: {} //一开始是空对象,点击food的时候再存入,在li(food)中添加点击事件
};
}
在循环food的时候添加点击事件,将选中的food传递到selectedFood中
<li v-for="food in item.foods" :key="food.id" class="food-item" border-1px @click="selectFood(food,$event)">
编写selectFood事件,将food传过去,不要忘了将$event传递进去
selectFood(food, event) { //@click函数
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
this.selectedFood = food;
//调用子组件的show()方法展开food组件
this.$refs.food.show();
}
}
这样我们就取到了被选中的selectedFood,将其传达组件food中, :food = selectedFood;
这样,food组件就拿到了被点选的这个food的相关数据,之后,我们回到子组件food中为其添加show方法,其中showFlag是表示商品详情页图层是否会出现的布尔值,它是一直在变化的量,不要忘记将其放入到data中进行观测
data () {
return {
showFlag: false, // 默认商品详情页不展开
}
show() { //可以被父组件调用到,方法前加下划线一般是私有方法
this.showFlag = true;
}
在子组件food中写好了show方法,那么我们在父组件goods中food的点击函数中就可以调用子组件food的show方法将其展开,俺么怎么在父组件中调用子组件的方法呢,在子组件food中添加一个ref即可
selectFood(food, event) { //@click函数
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
this.selectedFood = food;
//调用子组件的show()方法展开food组件
this.$refs.food.show();
}
}
这样点击food就可出现food的详情页了,之后,我们在为详情页的展开设置一个从右向左飞入的动画
&.fade-enter-active, &.fade-leave-active
transition all .3s linear //为动画过程添加缓动和最终状态
transform translate3d(0,0,0)
&.fade-enter, &.fade-leave-active
opacity 0
transform translate3d(100%, 0, 0) //动画x轴方向移动
到这里我们实现了点击,动画并且拿到了数据,现在我们为food组件填充内容,DOM结构如下
<div class="food-content">
<div class="image-header">
<img :src="food.image" alt="">
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
...
</div>
图片的高度是要跟屏幕的宽度一样的,屏幕的宽度不定,但是如果不设定图片的高度,页面的加载会出现抖动的过程,因为页面在一开始加载的时候是没有高度的,怎样优化这个过程(padding-top 100%)
.image-header
position relative
width 100%
height 0
padding-top 100% // 百分比是相对于盒子的宽度来计算的,看起来就像是一个正方形
img
position absolute
left 0
top 0
width 100%
height 100%
接下来添加返回按钮:
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
CSS代码如下:
.back
position absolute
top 10px
left 0
.icon-arrow_lift
display block
padding 10px //图片不要变大,但是点击区域会扩大
font-size 20px
color #ffffff
实现返回按钮,添加点击事件
hide() {
this.showFlag = false;
}
接下来实现
先写一下布局和样式
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好评率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
购物车样式
</div>
.content
padding 18px
position relative
.title
line-height 14px
margin-bottom 8px
font-size 14px
font-weight 700px
color rgb(7,17,27)
.detail
margin-bottom 18px
height 10px
line-height 10px
font-size 0 // 因为后面两个span是inline-block布局
.sell-count, .rating
font-size 10px
color rgb(147,153,159)
.sell-count
margin-right 12px
.price
line-height 24px
font-weight: 700
.now
margin-right 8px
font-size 14px
color rgb(240,20,20)
.old
text-decoration line-through
color rgb(147,153,159)
font-size 10px
.cartcontrol-wrapper
position absolute
right 12px
bottom 12px
页面有可能超过手机高度,这里需要引入一个betterScroll,父层绑定一个betterScroll,有一个固定的高度,内层海底一个内容层,当内层的高度大于外层的固定高度时,就会出现滚动
<transition name="fade"> <!-- 为food界面的展开添加动画-->
<div v-show="showFlag" class="food" ref="food">
</div>
</transiton>
所以在界面展示的时候,对betterScroll进行初始化,即在show函数中进行初始化
show() { //可以被父组件调用到,方法前加下划线一般是私有方法
this.showFlag = true;
//展示界面时用到BScroll
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true // 可以被点击
});
} else {
this.scroll.refresh();
}
});
}
接下来实现右侧的购物车按钮:原来是一个加入购物车的按钮,点击之后,会出现加减号组件,别忘了引入 food
<div class="cartcontrol-wrapper"> <!-- 加减号组件cartcontrol -->
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="food.count === 0 || !food.count">
加入购物车
</div> <!-- 这两种情况下加入购物车都会显示,否则就会隐藏-->
</transition>
添加css样式
.cartcontrol-wrapper
position absolute
right 12px
bottom 12px
.buy
position absolute
right 18px
bottom 18px
z-index 10 // 要盖住加减号组件
height 24px
line-height 24px
padding 0 12px
box-sizing border-box
font-size 10px
border-radius 10px
color #ffffff
background rgb(0,160,220)
&.fade-enter-active, &.fade-leave-active
transition all 0.2s
opacity 1
&.fade-enter, &fade-leave-active
opacity 0
设计好样式之后,为添加购物车按钮
<div @click.stop.prevent="addFirst" class="buy" v-show="food.count === 0 || !food.count">
加入购物车
</div> <!-- 这两种情况下加入购物车都会显示,否则就会隐藏-->
addFirst(event) { // 默认的参数是event,点击按钮的时候添加的是第一个商品
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
//第一次的时候el消失,找不到小球发出的其实位置,所以第一次小球动画消失
//解决办法就是将购物车的消失做成一个动画,而不是立马display:none
//添加的时候添加小球动画,派发事件,之后goods.vue就可以监听到cart-add,并传递给shopcart.vue
this.$emit('cart-add', event.target);
Vue.set(this.food, 'count', 1); //初始化this.food.count = 1;
}
点击购物车按钮立马消失的话会使getBoundingReact函数找不到位置,所以为其添加一个动画transition
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="food.count === 0 || !food.count">
加入购物车
</div> <!-- 这两种情况下加入购物车都会显示,否则就会隐藏-->
</transition>
点击主页面的加减号组件时,加减号所在的good的详情页也会展开,所以要对cartcontrol组件进行阻止冒泡的限定,所以回到cartcontrol组件
<div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
<div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div> <!-- 加号图标 -->
将split分割写成一个组件
有上下的一个boder,有高度和背景色:
<template>
<div class="split"></div>
</template>
<script type="text/ecmascript-6">
export default {};
</script>
<style lang="stylus" rel="stylesheet/stylus">
.split
width 100%
height 16px
border-top: 1px solid rgba(1, 17, 27, 0.1);
border-bottom: 1px solid rgba(1, 17, 27, 0.1);
background: #f3f5f7
</style>
将split引入到food组件,并注册即可
<split v-show="food.info"></split>
之后添加商品详情,但是有的food是没有商品详情的,所以要在这里做一个v-show的判断,包括split组件
<split v-show="food.info"></split>
<div class="info" v-show="food.info"> <!-- 并不是所有的商品都有info的 -->
<h1 class="title">商品信息</h1>
<p class="text">{{food.info}}</p>
</div>
<split></split>
接下来写商品介绍,即info部分的样式
.info
padding 18px
.title
line-height 14px
margin-bottom 16px
font-size 14px
color rgb(7,17,27)
.text
line-height 24px
padding 0 8px
font-size 12px
color rgb(77,85,93)
之后就是商品的评价部分了,ratings
Vue1.0升级至2.0之后 prop的.sync被去除 因此直接在子组件修改父组件的值是会报错的 目的是为了阻止子组件影响父组件的数据
1)首先将父组件的数据传递到子组件中,data :绑定
food.vue(父组件中)
data () {
return {
showFlag: false,
selectType: ALL,
onlyContent: true, //先设置组件一开始显示有内容的评价
desc: { //desc做了改变
all: '全部',
positive: '推荐',
negative: '吐槽'
}
};
},
<ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
2)子组件接收数据,修改数据,然后设置派发事件将修改后的数据派发的父组件
ratingSelect.vue(子组件中),在子组件中除了用props接收父子间的数据之外,还要在data中定义两个变量,将props中接收到的值赋值给这两个新变量,之后在子组件中改变的都是这两个新的变量
props: {
ratings: {
type: Array,
default() {
return [];
}
},
selectType: { //全部,满意,不满意
type: Number,
default: ALL //默认情况时ALL,值等于2
},
onlyContent: { //只看有内容的评价还是所有的评价
type: Boolean,
default: false //设置为可以看到所有的评价
},
desc: { //描述
type: Object,
default() { //默认desc是这三种,在商品详情页的时候传入推荐或者吐槽
return {
all: '全部',
positive: '满意',
negative: '不满意'
};
}
}
},
data() {
return {
sType: this.selectType,
oContent: this.onlyContent
}
},
<div class="ratingselect">
<div class="rating-type" border-1px>
<span class="block positive" @click="select(2,$event)" :class="{'active':sType === 2}">{{desc.all}}<span class="count">47</span> </span>
<span class="block positive" @click="select(0,$event)" :class="{'active':sType === 0}">{{desc.positive}}<span class="count">40</span></span>
<span class="block negative" @click="select(1,$event)" :class="{'active':sType === 1}">{{desc.negative}}<span class="count">47</span></span>
</div>
<div @click="toggleContent($event)" class="switch" :class="{'on':oContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有内容的评价</span>
</div>
</div>
在click事件中改变自定义变量的值,并利用$emit派发事件将新值传入
methods: {
select (type, event) { //点击的时候外层是有一个BScroll的,所以要传递event阻止默认点击
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
//将this.selectType设置成传入的参数,而不是food传过来的初始化的值,之后样式就可以随着点击改变了
this.sType = type;
//派发事件通知父组件food.vue selectType的改变,将type值传出去
console.log('ratingselect.vue ' + type);
//this.$emit('se-type', type);
this.$emit('increment', 'selectType', this.sType); //selectType要跟food.vue中data定义的同名
},
toggleContent (event) {
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
this.oContent = !this.oContent;
console.log('ratingselect.vue ' + this.oContent);
// this.$emit('toggle-content', this.oContent);
this.$emit('increment', 'onlyContent', this.oContent);//onlyContent要跟food.vue中data定义的同名
}
},
3)父组件food.vue监听函数,要写在methods中
incrementTotal(type, data) {
this[type] = data;
this.$nextTick(() => {
this.scroll.refresh();
});
在子组件中绑定函数 @increment="incrementTotal"
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
4)实现点击不同选项时评价列表的刷新,在父组件food.vue中添加
needShow(type, text) {
// console.log('this.selectType: ' + this.selectType + ' type: ' + type + ' out ' + text);
if (this.onlyContent && !text) {
return false;
}
if (this.selectType === ALL) {
return true;
} else {
//console.log('this.selectType: ' + this.selectType + 'type: ' + type + ' in ' + text);
return type === this.selectType;
}
},
并将needShow()函数添加在DOM中li中的v-show条件下
<div class="rating-wrapper"> <!-- 评价列表-->
<ul v-show="food.ratings && food.ratings.length">
<li v-show="needShow(rating.rateType, rating.text)" v-for="rating in food.ratings" :key="rating.id" class="rating-item border-1px">
<div class="user">
<span class="name">{{rating.username}}</span>
<img class="avatar" height="12" width="12" :src="rating.avatar" alt="">
</div>
<div class="time">{{rating.rateTime | formatDate}}</div>
<p class="text">
<span :class="{'icon-thumb_up':rating.rateType === 0, 'icon-thumb_down':rating.rateType === 1}"></span>{{rating.text}}
</p>
</li>
</ul>
<div class="no-ratings" v-show="!food.ratings || !food.ratings.length">暂无评价</div>
</div>