之前我们已经完成了图片,产品价格和购物车部分,split组件,之后就是评论选择让ratingselect的实现,先完成部分Dom结构
ratingselect组件的实现
1)创建ratingselect文件和ratingselect.vue
2)设置ratingselect组件中需要的props接收的数据,数据应从food.vue组件传入<ratingselect></ratingselect>,并由ratingselect.vue的props接收
我们要props的值如下: 首先是有一个变量是否显示只看内容,还有一个控制选择的类型,还有要维护一个所有评价的数据,因为这里有一个评价数量;还要去维护一个描述,是(全部,推荐,吐槽)还是(全部,满意,不满意),按照以上标准设置外部组件传入ratingselect的props值;
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
//需要一些评价数据才能完成评价组件
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: '不满意'
};
}
}
},
所以在food.vue(商品详情页)中引入ratingSelect组件的时候,将desc改成全部,推荐和吐槽,接下来去开发DOM
<template>
<div class="ratingselect">
<div class="rating-type" border-1px>
<span>{{desc.all}}</span>
<span>{{desc.positive}}</span>
<span>{{desc.negative}}</span>
</div>
<div @click="toggleContent($event)" class="switch" :class="{'on':oContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有内容的评价</span>
</div>
</div>
</template>
之后在food.vue组件注册并引入这个组件,
<div class="rating">
<h1 class="title"> 商品评价</h1>
<!-- ratings对应被点击的food的ratings-->
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
3)在data中挂载对上述对象的跟踪,并对其进行初始化
food.vue中,在foods组件中引入并注册ratingselect.vue组件,并data属性和(:)属性将2)中需要的数据从foods组件中传到ratingselect.vue组件的props中
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
data () {
return {
showFlag: false,
selectType: ALL,
onlyContent: false, //先设置组件一开始显示有内容的评价
desc: { //desc做了改变
all: '全部',
positive: '推荐',
negative: '吐槽'
}
};
},
我们对变量做了上述的初始化设置,但是我们希望在切换不同商品的时候能有相同的初始化状态,所以show函数作为goods组件中调用food组件的函数,即点开商品详情的显示函数,将初始化设置传入到show函数中
show() { //可以被父组件调用到,方法前加下划线一般是私有方法
this.showFlag = true;
//初始化部分,ratingselect组件是被被不同的商品使用的,所以我们希望在点开不同的商品时,能有一样的初始化状态
this.selectType = ALL;
this.onlyContent = false;
//展示界面时用到BScroll
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true // 可以被点击
});
} else {
this.scroll.refresh();
}
});
}
然后在模板中绑定data属性中的值,将其传入到ratingselect组件中,不要忘了food.ratings,food是由goods组件中传递过来的
<!-- ratings对应被点击的food的ratings-->
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
此时,desc已经从默认的满意和不满意变成了推荐和吐槽,接下来在父组件foods.vue中为title和ratingselect部分编写样式
<div class="rating">
<h1 class="title"> 商品评价</h1>
<!-- ratings对应被点击的food的ratings-->
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
<div class="rating-wrapper"> <!-- 评价列表-->
</div>
</div>
因为rating下有一条border,所以在rating下不可以设置四周的padding值,如果设置了border就撑不开整个屏幕了
.rating //因为要在rating title下方画一条横线,所以不能用padding-left,改用title的margin代替
padding-top: 18px
.title
line-height 14px
margin-left 18px
font-size 14px
color rgb(7,17,27)
之后是ratingselect自身样式的编写(ratingType和switch的普通样式和点击时的样式变化,若要改变初始状态就在food.vue中的show()函数中改变,show()函数是用来被goods.vue调用并展开详情界面的)
CSS样式:
.ratingselect
.rating-type
padding 18px 0
margin 0 18px //保证横线的长度
border-1px(rgba(7,17,27,0.1))
font-size 0
.block //没有写文字的时候是没有被撑开的
display inline-block
padding 8px 12px
margin-right 8px
border-radius 1px
line-height 16px
font-size 12px
color rgb(77,85,93)
&.active // block的active要设置一下
color #ffffff
.count
margin-left 2px
font-size 8px
&.positive
background rgba(0,160,220,.2)
&.active
background rgb(0,160,220)
&.negative
background rgba(77,85,93,0.2)
&.active
background rgb(77,85,93)
.switch
padding 12px 18px
line-height 24px
border-bottom 1px solid rgba(7,17,27,0.1)
color rgb(147,153,159)
font-size 0
&.on
.icon-check_circle
color #00c850
.icon-check_circle
display inline-block
vertical-align top
margin-right 4px
font-size 24px
.text
display inline-block
vertical-align top
font-size 12px
我们这里只是定义了样式,将其应用到DOM中,我们为其添加了active类和点击按钮,这里的sType对应的是
this.selectType = ALL;
this.onlyContent = false;
默认为全部选中并且看所有评价
<template>
<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">{{ratings.length}}</span> </span>
<span class="block positive" @click="select(0,$event)" :class="{'active':sType === 0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
<span class="block negative" @click="select(1,$event)" :class="{'active':sType === 1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
</div>
<div @click="toggleContent($event)" class="switch" :class="{'on':oContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有内容的评价</span>
</div>
</div>
</template>
4)为ratingType(是否满意)和switch(是否查看全部评估)编写点击事件,ratingselect.vue在select()中改变了selectType的值,要将改变的值通知到父组件food.vue的show()方法,可以通过派发事件完成
父组件food.vue 将selectType和onlyContent传入子组件,与之前的方式不变
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
在props中接收到父组件传过来的selectType和onlyContent的值之后,在data中重新定义变量接收,以便观测值的变化(因为子组件将改变data中的值,子组件要将这些变化的值传递个父组件)
data() {
return {
sType: this.selectType,
oContent: this.onlyContent
};
}
之后,sType就替代了this.selectType,所以DOM就变成了
<template>
<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">{{ratings.length}}</span> </span>
<span class="block positive" @click="select(0,$event)" :class="{'active':sType === 0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
<span class="block negative" @click="select(1,$event)" :class="{'active':sType === 1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
</div>
<div @click="toggleContent($event)" class="switch" :class="{'on':oContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有内容的评价</span>
</div>
</div>
</template>
但是之前定义的rating-type和switch的active的条件是不变的。
之后编写rating-type和swicth切换有内容评价部分的绑定函数:
select函数中,在点击的时候就把类型123传进去,传入event是因为外层是一个betterScroll,要进行点击事件的判断,将sType的值更新之后通过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);
},
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);
}
},
回到父组件food.vue中编写监听函数,并关联组件
<ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
5)统计不同评价的数量(过滤评价类型),添加positives和negitives数组,长度即为评价数量
<div class="rating-type" border-1px>
<span class="block positive" @click="select(2,$event)" :class="{'active':sType === 2}">{{desc.all}}<span class="count">{{ratings.length}}</span> </span>
<span class="block positive" @click="select(0,$event)" :class="{'active':sType === 0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
<span class="block negative" @click="select(1,$event)" :class="{'active':sType === 1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
</div>
computed: {
positives() { //对应所有正向评价的数组
return this.ratings.filter((rating) => {
return rating.rateType === POSITIVE;
});
},
negatives() {
return this.ratings.filter((rating) => {
return rating.rateType === NEGATIVE;
});
}
},
至此,ratingselect组件开发完成,我们将其与下方的评价列表连接起来,实现联动
回到food组件在ratingselect组件的下方添加评价列表的DOM结构
<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>
添加css样式:
.rating-wrapper
padding 0 18px
.rating-item
position relative
padding 16px 0
border-1px(rgba(7, 17, 27, 0.1))
.user
position absolute
right 0
top 16px
line-height 12px
font-size 0
.name
display inline-block
vertical-align top
font-size 10px
color rgb(147,153,159)
margin-right 6px
.avatar
border-radius 50%
.time
margin-bottom 6px
line-height 12px
font-size 10px
color rgb(147,153,159)
.text
line-height 16px
font-size 12px
color rgb(7,17,27)
.icon-thumb_up, .icon-thumb_down
margin-right 4px
line-height 16px
font-size 12px
.icon-thumb_up
color rgb(0,160,220)
.icon-thumb_down
color rgb(147,153,159)
.no-ratings
padding 16px 0
font-size 12px
color rgb(147,153,159)
添加好样式之后,为列表的显示添加选择:
<li v-show="needShow(rating.rateType, rating.text)" v-for="rating in food.ratings" :key="rating.id" class="rating-item border-1px">
添加needshow函数,因为我们是在子组件ratingselect中进行rating.rateType的切换,变量更改后的结果要传递到父组件中,这时用到了incrementTotal方法,对子组件更改的数值进行监听(在子组件ratingselect中使用select和toggleContent中进行emit派发),所以在切换了子组件的按钮之后,父组件就可以根据子组件的选择进行内容的切换
select (type, event) { //点击的时候外层是有一个BScroll的,所以要传递event阻止默认点击
if (!event._constructed) { //浏览器直接return掉,去掉自带click事件的点击
return;
}
//将this.selectType(this.sType)设置成传入的参数,而不是food传过来的初始化的值,之后样式就可以随着点击改变了
this.sType = type;
//派发事件通知父组件food.vue selectType的改变,将type值传出去
console.log('ratingselect.vue ' + type);
//this.$emit('se-type', type);
this.$emit('increment', 'selectType', this.sType);
},
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);
}
<ratingselect @increment="incrementTotal" :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
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;
}
},
incrementTotal(type, data) {
this[type] = data;
this.$nextTick(() => { // 当我们改变数据的时候,DOM的更新是异步的
this.scroll.refresh();
});
}
6)为其中时间的显示添加过滤器,将时间戳转化为时间字符串
<div class="time">{{rating.rateTime | formatDate}}</div>
import {formatDate} from 'common/js/date.js';
export function formatDate(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
}
}
return fmt;
}
function padLeftZero(str) {
return ('00' + str).substr(str.length);
}
filters: {
formatDate(time) {
let date = new Date(time);
return formatDate(date, 'yyyy-MM-dd hh:mm');
}
}
8)点击是否满意和只看有评价内容时,评价列表的显示,不是ratingselect组件的内容,写在food.vue中
<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>
利用needshow进行切换
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;
}
},