当我们在goods页面中,将商品添加到购物车的时候,购物车logo上方会显示添加的商品数,如下图所示
当购物车的商品数大于0的时候,点击底部的<shop-cart>部位,会触发显示添加到购物车的商品详情,如下图所示
当点击“清空”按钮的时候,购物车中的商品会删除掉,当点击“-”或者“+”按钮的时候,购物车的右上角的数字会减少或者增加,当点击“去结算”按钮的时候,会跳转到结算的界面。
下面讲讲如何实现上述功能的具体步骤
1、首先在shopCart.vue中书写购物车详情shopCartList的html
<!--shopCartList的出现与隐藏有个动画的过程-->
<transition name="fold">
<!--listShow()在computed中定义-->
<!--当this.totalCount即购物车中的数量大于0的时候,初始化better-scroll实例-->
<!--从而当shopCartList中的内容高度大于规定高度时可以滚动-->
<div class="shopCartList" v-show="listShow">
<div class="listHeader">
<h1 class="title">购物车</h1>
<!--empty()在methods中定义,清空购物车中的商品-->
<span class="empty" @click="empty">清空</span>
</div>
<div class="listContent" ref="listContent">
<ul>
<li class="food" v-for="food in selectFoods">
<span class="name">{{food.name}}</span>
<div class="price">
<span>¥{{food.price * food.count}}</span>
</div>
<div class="cartControlWrapper">
<!--add是在组件cart-control中通过this.$emit()传递过来的事件名-->
<cart-control @add="addFood" :food="food"></cart-control>
</div>
</li>
</ul>
</div>
</div>
</transition>
2、然后,在shopCart.vue中书写购物车详情shopCartList的css
.shopCartList
position:absolute
left:0
top:0
z-index: -1
width:100%
transform: translate3d(0, -100%, 0)
&.fold-enter-active,&.fold-leave-active
transiton:all 0.5s
&.fold-enter,&.fold-leave-active
transform:translate3d(0,0,0)
.listHeader
height:40px
line-height:40px
padding:0 18px
background: #f3f5f7
border-bottom:1px solid rgba(7,17,27,0.1)
.title
float:left
font-size:14px
color:rgb(7,17,27)
.empty
float:right
font-size:12px
color:rgb(0,160,220)
.listContent
padding: 0 18px
max-height:217px
over-flow:hidden
background:#fff
.food
position:relative
paddign:12px 0
box-sizing:border-box
border-1px(rgba(7,17,27,0.1))
.name
line-height:24px
font-size:14px
color:rgb(7,17,27)
.price
position:absolute
right:90px
bottom:1px
line-height:24px
font-size:14px
font-weight:700
color:rgb(240,20,20)
.cartControlWrapper
position: absolute
right:0
bottom:-5px
3、接着,在shopCart.vue中书写购物车详情的js
1)当购物车中有商品的时候,点击底部的shopCart.vue中的任意一个地方的时候,会弹出一个shopCartList,如下图所示
那么就需要在这个底部的容器里面定义一个点击事件,即toggleList(),其主要负责将在data()里面定义的变量fold的状态取反,即每次点击.content部分的区域,fold的值是true和false交替变化
fold在data()中的定义
fold:true //默认下该列表是折叠的
<template>中的定义如下
<div class="content" @click="toggleList">
在<script>中定义的toggleList()事件如下:
//实现.content的展开与折叠,也就是设置fold的值false和true的交替变化
toggleList(){
if(!this.totalCount){
//购物车还没有任何food
return;
}
//更改fold的状态
this.fold = !this.fold;
}
然后,在.shopCartList中定义v-show='listShow'
<!--listShow()在computed中定义-->
<!--当this.totalCount即购物车中的数量大于0的时候,初始化better-scroll实例-->
<!--从而当shopCartList中的内容高度大于规定高度时可以滚动-->
<div class="shopCartList" v-show="listShow">
函数listShow()是在computed中定义的,当this.fold的值发生变化的时候,就会触发函数listShow()的执行
//是否展开.shopCartList的内容
//主要有两个功能:设置该区域v-show的值
//以及初始化better-scroll实例(.listContent),从而当内容高度大于规定高度的时候,该区域内容可以滚动
listShow(){
if(!this.totalCount>0){
//购物车还没有添加任何商品
this.fold = true;
return false;
}
let show = !this.fold;
if(show){
//show为true的时候,才去初始化content-list的better-scroll
//异步执行,需要等到DOM更新完在初始化better-scroll
this.$nextTick(()=>{
//由于.listContent的高度是不断变化的,所以需要进行判断处理
// this.scroll不存在,则需要创建
//this.scroll存在了,但是.listContent的高度发生变化
//需要重新计算better-scroll,当DOM结构发生变化的时候务必要调用refresh(),确保滚动效果
//正常
if(!this.scroll){
this.scroll = new BScroll(this.$refs.listContent,{
click:true
});
}else{
//存在,则直接调用better-scroll中的接口函数refresh()
this.scroll.refresh();
}
});
}
return show;
}
},
2)点击“清空”按钮的时候,.shopCartList会消失
在“清空”按钮的DOM上定义点击事件,如下面所示:
<!--empty()在methods中定义,清空购物车中的商品-->
<span class="empty" @click="empty">清空</span>
点击事件empty()在methods中定义:
//清空.foodList里面选中的food,就是把food.count设为0
empty(){
//对selectFoods中的每一个food进行遍历
this.selectFoods.forEach((food)=>{
food.count = 0;
});
},
3)当点击<cart-control>中的“+”或者“-”的时候,商品数量会自加或者会自减
<!--add是在组件cart-control中通过this.$emit()传递过来的事件名-->
<!--addFood()在methods中定义,主要是将子组件传过来的对象-->
<!--传给drop(target)-->
<cart-control @add="addFood" :food="food"></cart-control>
在methods中addFood(target)定义如下
addFood(target) {
//将子组件传过来的对象target,传给在shopCart.vue中定义的函数drop(target)
this.drop(target);
}
4)点击“去结算”按钮会跳转到结算界面,这里这是写了一个弹窗
在<template>中的定义 如下,注意智力定义的点击事件pay(),要防止冒泡
<!--修饰符.stop/.prevent阻止冒泡事件,防止触发toggleList()-->
<!--.stop调用event.stopPropagation()-->
<!--.prevent调用event.preventDefault()-->
<div class="content-right" @click.stop.prevent="pay">
<!--payClass在computed里面定义,当价格总数>最小价格的时候,最右边的背景呈高亮状态-->
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
pay()在methods中的定义 如下:
//去结算
pay(){
if(this.totalPrice < this.minPrice){
return;
}
window.alert(`支付${this.totalPrice}元`);
}
5)然后,在弹出的.shopCartList中,有一个半透明的背景层,点击他的时候,会隐藏 掉.shopCartList和.listMask,当购物车有商品的时候,这个半透明层才会出现,所以用了v-show='listShow',同上面的定义
在<template>中的定义如下:
<transition name="fade">
<div class="listMask" v-show="listShow" @click="hideList"></div>
</transition>
在<style>中的定义如下:
.listMask
position:fixed
left:0
top:0
height:100%
width:100%
z-index:40
opacity:1
background: rgba(7, 17, 27, 0.6)
&.fade-enter-active,&.fade-leave-active
transition:all 0.4s
&.fade-enter,&.fade-leave-active
opacity:0
background:rgba(7,17,27,0)
事件hideList()在methods中定义 如下:
//当.shopCartList展开的时候,点击半透明的背景层的时候,可以将.shopCartList隐藏掉
hideList(){
this.fold = true;
}
以上大概就是实现购物车详情页面的显示的操作
完整代码如下:
<template>
<template>
<div>
<div class="shopCart">
<div class="content" @click="toggleList">
<div class="content-left">
<div class="logo-wrapper">
<!--totalCount在computed中定义,购物车里面的数量totalCount大于0,则-->
<!--购物车背景以及logo呈高亮状态-->
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<!--logo右上角的数字,当totalCount<=0,则不显示-->
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<!--totalPrice在computed里面计算,totalPrice>0则价格总数高亮-->
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送费¥{{deliveryPrice}}元</div>
</div>
<!--修饰符.stop/.prevent阻止冒泡事件,防止触发toggleList()-->
<!--.stop调用event.stopPropagation()-->
<!--.prevent调用event.preventDefault()-->
<div class="content-right" @click.stop.prevent="pay">
<!--payClass在computed里面定义,当价格总数>最小价格的时候,最右边的背景呈高亮状态-->
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
<!--定义小小球-->
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop">
<div class="ball" v-show="ball.show">
<!--.inner是一个小球-->
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
<!--shopCartList的出现与隐藏有个动画的过程-->
<transition name="fold">
<!--listShow()在computed中定义-->
<!--当this.totalCount即购物车中的数量大于0的时候,初始化better-scroll实例-->
<!--从而当shopCartList中的内容高度大于规定高度时可以滚动-->
<div class="shopCartList" v-show="listShow">
<div class="listHeader">
<h1 class="title">购物车</h1>
<!--empty()在methods中定义,清空购物车中的商品-->
<span class="empty" @click="empty">清空</span>
</div>
<div class="listContent" ref="listContent">
<ul>
<li class="food" v-for="food in selectFoods">
<span class="name">{{food.name}}</span>
<div class="price">
<span>¥{{food.price * food.count}}</span>
</div>
<div class="cartControlWrapper">
<!--add是在组件cart-control中通过this.$emit()传递过来的事件名-->
<!--addFood()在methods中定义,主要是将子组件传过来的对象-->
<!--传给drop(target)-->
<cart-control @add="addFood" :food="food"></cart-control>
</div>
</li>
</ul>
</div>
</div>
</transition>
</div>
<transition name="fade">
<div class="listMask" v-show="listShow" @click="hideList"></div>
</transition>
</div>
</template>
<script>
<script>
import cartControl from '../cartControl/cartControl';
import BScroll from 'better-scroll';
export default {
components:{
'cart-control':cartControl
},
props:{
//从父组件中传递过来的数据是一个数组,每个元素是一个对象,对象属性有price,count
//分别对应每种食品的单价以及这种商品加进购物车的数量
selectFoods:{//矩阵里面存放着每一个被选中的food
type:Array,
default(){ //矩阵类型的默认值是返回一个空的数组
return [
];
}
},
deliveryPrice:{
type:Number,
default:0
},
minPrice:{
type:Number,
default:0
}
},
data(){
return {
//维护每个小球的状态,因为transition只对v-if v-show v-for有过渡效果
//所以这里定义了show,在<template>中采用v-show指令
balls:[
{
show: false
},
{
show:false
},
{
show:false
},
{
show:false
},
{
show:false
}
],
dropBalls:[], //存放下落的小球
fold:true //表示选中的food的列表是折叠还是打开
};
},
computed:{
//计算加入购物车所有商品的价格
totalPrice(){
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
// 计算所有选择商品的数量,用于在logo上面显示所选择商品数量,并且数量>0的时候购物车高亮
// 并且购物车logo高亮
totalCount(){
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
//最右边的20元起送的一个描述,要有一个逻辑的思维
payDesc(){
if( this.totalPrice === 0 ){
return `¥${this.minPrice}元起送`;
}else if( this.totalPrice < this.minPrice ){
let diff = this.minPrice - this.totalPrice;
return `还差¥${diff}元起送`;
}else{
return '去结算';
}
},
//右边文字的样式类
payClass(){
if( this.totalPrice < this.minPrice ){
return 'not-enough';
}else{
return 'enough';
}
},
//是否展开选中food的list
//主要有两个功能:设置该区域v-show的值
//以及初始化better-scroll实例,从而当内容高度大于规定高度的时候,该区域内容可以滚动
listShow(){
if(!this.totalCount>0){
//购物车还没有添加任何商品
this.fold = true;
return false;
}
let show = !this.fold;
if(show){
/ow为true的时候,才去初始化content-list的better-scroll
//异步执行,需要等到DOM更新完在初始化better-scroll
this.$nextTick(()=>{
//由于.listContent的高度是不断变化的,所以需要进行判断处理
// this.scroll不存在,则需要创建
//this.scroll存在了,但是.listContent的高度发生变化
//需要重新计算better-scroll,当DOM结构发生变化的时候务必要调用refresh(),确保滚动效果
//正常
if(!this.scroll){
this.scroll = new BScroll(this.$refs.listContent,{
click:true
});
}else{
//存在,则直接调用better-scroll中的接口函数refresh()
this.scroll.refresh();
}
});
}
return show;
}
},
methods:{
drop(el){
//对球进行遍历
let len = this.balls.length;
for( let i=0;i<len;i++ ){
let ball = this.balls[i];
if( !ball.show ){
ball.show = true;
ball.el = el;
this.dropBalls.push(ball); //将下落的小球放进来
return;
}
}
},
addFood(target) {
//将子组件传过来的对象target,传给在shopCart.vue中定义的函数drop(target)
this.drop(target);
},
beforeDrop(el){
//将所有设为true的小球找到
// 遍历所有的小球
let count = this.balls.length;
while(count--){
let ball = this.balls[count];
if(ball.show){
//返回元素的大小及其相对于视口的位置的对象
let rect = ball.el.getBoundingClientRect();
//水平和竖直方向的偏移量
let x = rect.left - 32; //左下角购物车和右侧点击的“+”的水平距离
let y = -(window.innerHeight - rect.top -22);//竖直方向的距离差
//外层做一个纵向的变化
el.style.display = '';
el.style.webkitTransform = `translate3d(0,${y}px,0)`;
el.style.transform = `translate3d(0,${y}px,0)`;
//内层做一个横向的变化
let inner = el.getElementsByClassName('inner-hook')[0];//取到的是一个数组,所以要取第一个元素
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
dropping(el,done){
let rf = el.offsetHeight;
this.$nextTick(()=>{
//当下降的时候,重写外部和内部的translate3d()
el.style.webkitTransform = 'translate3d(0,0,0)';
el.style.transform = 'translate3d(0,0,0)';
let inner = el.getElementsByClassName('inner-hook')[0];//取到的是一个数组,所以要取第一个元素
inner.style.webkitTransform = 'translate3d(0,0,0)';
inner.style.transform = 'translate3d(0,0,0)';
el.addEventListener('transitionend',done)
});
},
afterDrop(el){
//当降落完一个ball,就将该ball从balls数组中取出来
let ball = this.dropBalls.shift();
if(ball){
//又可以利用该ball
ball.show = false;
el.style.display = 'none';
}
},
//实现.content的展开与折叠
toggleList(){
if(!this.totalCount){
//购物车还没有任何food
return;
}
//更改fold的状态
this.fold = !this.fold;
},
//清空.foodList里面选中的food,就是把food.count设为0
empty(){
//对selectFoods中的每一个food进行遍历
this.selectFoods.forEach((food)=>{
food.count = 0;
});
},
//当.shopCartList展开的时候,点击半透明的背景层的时候,可以将.shopCartList隐藏掉
hideList(){
this.fold = true;
},
//去结算
pay(){
if(this.totalPrice < this.minPrice){
return;
}
window.alert(`支付${this.totalPrice}元`);
}
}
}
</script>
<style>
<style lang="stylus">
@import '../../common/stylus/mixin.styl'
.shopCart
position: fixed
left:0
bottom:0
height:48px
z-index:50
width: 100%
.content
display:flex
background: #141d27
font-size:0
color:rgba(255,255,255,0.4)
.content-left
flex:1
.logo-wrapper,.price,.desc
display: inline-block
vertical-align: top
.logo-wrapper
position: relative
top: -10px
margin:0 12px
padding:6px
width:56px
height:56px
box-sizing:border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
background: #2b343c
text-align:center
&.highlight
background:rgb(0,160,220)
.icon-shopping_cart
font-size:24px
line-height:44px
color:#80858a
&.highlight
color:#fff
.num
position: absolute
top:0
right:0
width:24px
height:16px
line-height:16px
text-align:center
border-radius: 16px
font-size:9px
font-weight:700
color: #ffffff
background:rgb(240,20,20)
box-shadow:0 4px 8px 0 rgba(0,0,0,0.4)
.price,.desc
line-height:24px
.price
margin-top:12px
padding-right:12px
box-sizing:border-box
border-right:1px solid rgba(255,255,255,0.1)
font-size:16px
font-weight:700
&.highlight
color:#fff
.desc
margin:12px 0 0 12px
font-size:10px
.content-right
flex:0 0 105px
width:105px
.pay
height:48px
line-height:48px
text-align:center
font-size:12px
font-weight:700
background:#2b333b
&.not-enough
background:#2b333b
&.enough
background: #00b43c
color:#fff
.ball-container
.ball
position:fixed
left:32px
bottom:22px
z-index:200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width:16px
height:16px
border-radius:50%
background:rgb(0, 160, 220)
transition:all 0.4s linear
.shopCartList
position:absolute
left:0
top:0
z-index: -1
width:100%
transform: translate3d(0, -100%, 0) /*有一个向上变化的动作*/
&.fold-enter-active,&.fold-leave-active
transiton:all 0.5s
&.fold-enter,&.fold-leave-active
transform:translate3d(0,0,0)
.listHeader
height:40px
line-height:40px
padding:0 18px
background: #f3f5f7
border-bottom:1px solid rgba(7,17,27,0.1)
.title
float:left
font-size:14px
color:rgb(7,17,27)
.empty
float:right
font-size:12px
color:rgb(0,160,220)
.listContent
padding: 0 18px
max-height:217px
over-flow:hidden
background:#fff
.food
position:relative
paddign:12px 0
box-sizing:border-box
border-1px(rgba(7,17,27,0.1))
.name
line-height:24px
font-size:14px
color:rgb(7,17,27)
.price
position:absolute
right:90px
bottom:1px
line-height:24px
font-size:14px
font-weight:700
color:rgb(240,20,20)
.cartControlWrapper
position: absolute
right:0
bottom:-5px
.listMask
position:fixed
left:0
top:0
height:100%
width:100%
z-index:40
opacity:1
background: rgba(7, 17, 27, 0.6)
&.fade-enter-active,&.fade-leave-active
transition:all 0.4s
&.fade-enter,&.fade-leave-active
opacity:0
background:rgba(7,17,27,0)
</style>