本节重点:
- 父组件到子组件的数据传递
- 子组件到父组件的数据传递
- js的模块化,按需引入
- 组件的模块化,重复使用
- 数组过滤与过滤器的使用
一、goods商品界面点击到food详情界面的实现
- 父组件good.vue界面:
- 与子组件的关联
- 点击商品时,更改传入的商品数据this.selectedfood = food
- 根据子组件的DOM节点this.$refs.thisfood,调用子组件的showthisfood方法,显示子组件
- 代码部分如下:
<template>
<food @cartAddEvent='_drop' ref='thisfood' :selectedfood = 'selectedfood'></food>
</template>
点击商品时触发事件调用该方法:
//点击展示商品详情界面
showfood(food,event){
//better scroll中阻止浏览器默认的点击事件
if(!event._constructed) return false;
this.selectedfood = food;
this.$refs.thisfood.showthisfood()
}
二、food详情界面的实现
- 界面实现结构图
- 详情界面切入、切出使用过渡
- 详情界面内容过长时,使用better scroll滑动组件
- 加入购物车cartcontrol组件的复用
- 详情界面中第一次添加商品到购物车使用 加入购物车按钮,并添加小球抛入到购物车的效果
- 当该商品已经被添加过,引入加入购物车cartcontrol组件,实现商品累加与累减功能
- 这个功能实现时,遇到一个的问题,点击加入购物车按钮,该按钮消失,加入购物车cartcontrol组件展示。此时实现小球抛入效果,无法取得该DOM节点的坐标,此DOM节点已经display:none .解决方法:给加入购物车按钮的消失一个过渡效果,巧妙的在小球抛入过渡时取到改DOM节点
- 商品评价中,筛选条件在另一个模块可以被复用,抽象为ratingSelecte组件,详细组件代码见本章尾部
<!--
-- ratings: 该商品的评论数据 Array ;
-- suggestion:商品评价的中文条件 Object
-- selectType:商品评价的中文条件选中的值,默认为全部 Number
-- onlyContent:是否只看内容的值 ,默认为false ,Boolean
-- 事件 onlyshow,selectCon: 点击筛选条件时派发的事件,更新父组件的数据
-->
<ratingSelecte :ratings = 'selectedfood.ratings' :suggestion ='suggestion' :selectType = 'selectType' :onlyContent='onlyContent' @onlyshow='onlyshowcon' @selectCon = 'selectContent'></ratingSelecte>
- 根据ratingSelecte组件的筛选条件,过滤 评论内容
- 每一行评论是否显示的控制v-show=’showRating(rating)’,showRating方法根据筛选条件来进行过滤
Template模板部分:
<div class='select-content'>
<ul v-if='selectedfood.ratings'>
<li v-show='showRating(rating)' class='ratingGroup' v-for='rating in selectedfood.ratings'>
<div class='ratingH clearfix'>
<div class='time'>{{rating.rateTime | dateString}}</div>
<div class='user'><span class='username'>{{rating.username}}</span><img class='userImg' :src='rating.avatar'></div>
</div>
<div class='ratingC'><span class='iconThumb' :class='{"icon-thumb_up":rating.rateType===0,"icon-thumb_down":rating.rateType===1}'></span><span class='text'>{{rating.text}}</span></div>
</li>
</ul>
</div>
JS代码部分
//是否显示评价信息
showRating(rating){
//只看评价时,不显示的
if(this.onlyContent && rating.text==''){
return false;
}
//点击三个评价筛选
if(this.selectType===All){
return true;
}else{
return this.selectType ===rating.rateType;
}
}
三、js模块化
将需要重复使用的js 方法抽象成一个公共的js,在需要使用到的组件中,import引入方法
import {dateToString} from '../../common/js/common.js';
需要引入多个方法时,例如需要引入a,b,c方法
import {a,b,c} from '../../common/js/common.js';
common.js:
// 将时间对象转换成字符串 “yyyy-mm-dd hh:mm”
export function dateToString(date) {
if (date == null || date == '') {
date = '';
return date;
}
var year = date.getFullYear();
var month =parseInt(date.getMonth())+1;
var day = parseInt(date.getDate());
var hours = date.getHours();
var Minutes = date.getMinutes();
if (Minutes < 10) {
Minutes = Minutes + "0";
}
date =year+'-'+month+'-'+day+' '+ hours + ':' + Minutes;
return date;
}
涉及的完整代码
food组件
<template>
<transition name='food'>
<div class="food" v-show='showthis' ref='food'>
<!-- 为bettter scroll 添加div层 -->
<div>
<!-- 商品图片 -->
<div class='foodImg'>
<img :src="selectedfood.image">
</div>
<!-- 返回到商品foods.vue页面 -->
<div class='backGoods' @click='hidethisfood'><span class='icon-arrow_lift'></span></div>
<!-- 商品信息 -->
<div class='foodInfo'>
<div class='goodsName'>{{selectedfood.name}}</div>
<div class='goodsxs'><span>月售{{selectedfood.sellCount}}份</span><span>好评率{{selectedfood.rating}}%</span></div>
<div class='foodsPrice'><span>¥{{selectedfood.price}}</span><span class='old' v-show='selectedfood.oldPrice!=""'>¥{{selectedfood.oldPrice}}</span>
</div>
<div class='cartcontrol-wrapper'>
<cartcontrol @cartAdd ='cartAddcontrol' :food ='selectedfood'></cartcontrol>
</div>
<transition name='firstAdd'>
<div class='addtocart' v-show='selectedfood.count===0||!selectedfood.count' @click='addCount($event)'>加入购物车</div>
</transition>
</div>
<!-- 商品介绍 -->
<div class='foodDetail'>
<h1 class='food-header'>商品介绍</h1>
<div class='food-content'>{{selectedfood.info}}</div>
</div>
<div class='foodAsses'>
<h1 class='Asses-header'>商品评价</h1>
<ratingSelecte :ratings = 'selectedfood.ratings' :suggestion ='suggestion' :selectType = 'selectType' :onlyContent='onlyContent' @onlyshow='onlyshowcon' @selectCon = 'selectContent'></ratingSelecte>
<div class='select-content'>
<ul v-if='selectedfood.ratings'>
<li v-show='showRating(rating)' class='ratingGroup' v-for='rating in selectedfood.ratings'>
<div class='ratingH clearfix'>
<div class='time'>{{rating.rateTime | dateString}}</div>
<div class='user'><span class='username'>{{rating.username}}</span><img class='userImg' :src='rating.avatar'></div>
</div>
<div class='ratingC'><span class='iconThumb' :class='{"icon-thumb_up":rating.rateType===0,"icon-thumb_down":rating.rateType===1}'></span><span class='text'>{{rating.text}}</span></div>
</li>
</ul>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import cartcontrol from '../cartcontrol/cartcontrol.vue';
import ratingSelecte from '../ratingSelecte/ratingSelecte.vue';
import {dateToString} from '../../common/js/common.js';
//商品评价的筛选状态
const SATISFY=0; //推荐
const UNSATISFY=1; //吐槽
const All=2; //全部
export default{
name:'food',
props:{
selectedfood:{
type:Object
}
},
data(){
return{
showthis:false,//显示或隐藏改组件
suggestion:{
all:'全部',
satisfy:'推荐',
unsatisfy:'吐槽'
},
selectType:All,
onlyContent:false//只看内容
}
},
methods:{
showthisfood(){
this.showthis = true;
//初始化筛选内容
this.selectType=All,
this.onlyContent=false//只看内容
this.$nextTick(()=>{
//better scroll需要更新DOM对象
if(!this.scroll){
this.scroll = new BScroll(this.$refs.food, {
click:true
});
}else{
this.scroll.refresh();
}
})
},
hidethisfood(){
this.showthis = false;
},
cartAddcontrol(el){
this.$emit('cartAddEvent',el);
},
addCount(el){
//为food数量置1
this.$set(this.selectedfood,'count',1)
//派发小球点击事件,做抛物动画
this.$emit('cartAddEvent',el.target);
},
//切换评价筛选条件
selectContent(type){
this.selectType = type
//切换筛选条件时,更新了DOM节点,要更新better scroll
this.$nextTick(()=>{
this.scroll.refresh()
})
},
onlyshowcon(){
this.onlyContent = !this.onlyContent
this.$nextTick(()=>{
this.scroll.refresh()
})
},
//是否显示评价信息
showRating(rating){
//只看评价时,不显示的
if(this.onlyContent && rating.text==''){
return false;
}
//点击三个评价筛选
if(this.selectType===All){
return true;
}else{
return this.selectType ===rating.rateType;
}
}
},
components:{cartcontrol,ratingSelecte}
,
filters:{
//将字符串时间转换成时间格式
dateString(datenumber) {
let date = new Date(datenumber);
return dateToString(date);
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus" scope>
@import '../../common/stylus/mixin.styl'
.food
position:fixed
top:0
left:0
bottom:48px
width:100%
background-color:#f3f5f7
z-index:30
&.food-enter-active,&.food-leave-active
transition:all 0.4s
&.food-enter,&.food-leave-to
transform:translate3d(100%,0,0)
.foodImg
position:relative
width:100%
height:0
padding-top:100%
img
position:absolute
left:0
top:0
width:100%
height:100%
.backGoods
position:absolute
left:10px
top:10px
.icon-arrow_lift
color:#fff
.foodInfo
position:relative
padding:18px;
background-color:#fff
border-1px(rgba(7,17,27,0.1))
.goodsName
font-size:14px
font-weight:700
color:rgb(7,17,27)
line-heigth:14px
.goodsxs
margin:8px 0 18px 0
line-height:10px
font-size:10px
color:rgb(147,153,159)
span:first-child
margin-right:12px
.foodsPrice
font-size:10px
color:rgb(240,20,20)
font-weight: normal
line-height: 14px
margin-top:8px
.old
text-decoration:line-through
margin-left:8px
color:rgb(147,153,159)
.cartcontrol-wrapper,
.addtocart
position:absolute
bottom:18px
right:18px
.addtocart
width:74px
height:24px
color:#fff
font-size:10px
line-height:24px
text-align:center
border-radius:12px
background-color:rgb(0,160,220)
&.firstAdd-enter-active,&.firstAdd-leave-active
transition:opacity 0.6s
&.firstAdd-enter,&.firstAdd-leave-to
opacity:0
.foodDetail
margin:16px 0
padding:18px
background-color:#fff
border-1px(rgba(7,17,27,0.1))
borderTop-1px(rgba(7,17,27,0.1))
.food-header
font-size:14px
font-weight:500
.food-content
margin-top:6px
padding:0 8px
font-size:12px
font-weight:200
line-height:24px
color:rgb(77,85,93)
.foodAsses
background-color:#fff
border-1px(rgba(7,17,27,0.1))
borderTop-1px(rgba(7,17,27,0.1))
.Asses-header
padding:18px 18px 6px 18px
font-size:14px
font-weight:500
.select-content
padding:0 18px
.ratingGroup:last-child
border-none()
.ratingGroup
padding:16px 0
border-1px(rgba(7,17,27,0.1))
.ratingH
margin-bottom:6px
.time
float:left
line-height:12px
color:rgb(147,153,159)
font-size:10px
.user
float:right
height:12px
.username
display:inline-block
vertical-align:top
line-height:12px
color:rgb(147,153,159)
font-size:10px
margin-right:6px
.userImg
display:inline-block
vertical-align:top
width:12px
height:12px
border-radius:50%
img
width:100%
height:100%
.ratingC
line-height: 24px;
height: 24px;
.iconThumb
font-size:12px
display:inline-block
vertical-align:middle
margin-right:4px
.icon-thumb_down
color:rgb(147,153,159)
.icon-thumb_up
color:rgb(0,160,222)
.text
font-size:12px
</style>
ratingSelect组件
<template>
<div class='ratingSelect'>
<div class='select-header'>
<div v-if='ratings' class='suggestion'>
<span class='su' :class="{on:selectType == 2}" @click='_selectContent(2)'>{{suggestion.all}} {{ratings.length}}</span>
<span class='su' :class="{on:selectType == 0}" @click='_selectContent(0)'>{{suggestion.satisfy}} {{satisfyNumber.length}}</span>
<span class='no' :class="{on:selectType == 1}" @click='_selectContent(1)'>{{suggestion.unsatisfy}} {{unsatisfyNumber.length}}</span>
</div>
<div class='onlyContent' @click='_onlyshowcon'>
<span class='icon-check_circle' :class='{oncheck_circle:onlyContent===true}'></span><span class='text'>只看有内容的评价</span>
</div>
</div>
</div>
</template>
<script>
const SATISFY=0; //推荐
const UNSATISFY=1; //吐槽
const All=2; //全部
export default{
name:'ratingSelect',
props:{
ratings:{
type:Array
},
suggestion:{
type:Object
},
//筛选条件之一
selectType:{
type:Number,
default:All
},
//是否只看内容
onlyContent:{
type:Boolean,
default:false
}
},
computed:{
satisfyNumber(){
return this.ratings.filter((rating)=>{
return rating.rateType === SATISFY
})
},
unsatisfyNumber(){
return this.ratings.filter((rating)=>{
return rating.rateType === UNSATISFY
})
}
},
methods:{
_selectContent(type){
if (!event._constructed) {
return;
}
// this.selectType = type
this.$emit('selectCon',type)
},
_onlyshowcon(){
if (!event._constructed) {
return;
}
// this.onlyContent = !this.onlyContent
this.$emit('onlyshow')
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus" scope>
@import '../../common/stylus/mixin.styl'
.ratingSelect
.select-header
.suggestion
padding:12px 0 18px 0
margin:0 18px
border-1px(rgba(7,17,27,0.1))
font-size:0
.su,.no
font-size:12px
padding:8px 12px
border-radius:2px
.su
margin-right:8px
background-color:rgba(0,160,220,0.2)
.no
background-color:rgba(77,85,93,0.2)
.on
background-color:rgb(0,160,220)
color:#fff
.onlyContent
padding:12px 18px
border-1px(rgba(7,17,27,0.1))
.icon-check_circle
display:inline-block
vertical-align:middle
font-size:24px
color:rgb(147,153,159)
margin-right:4px
.oncheck_circle
color: #00c850
.text
display:inline-block
color:rgb(147,153,159)
font-size:12px
line-height:24px
</style>