goods组件

前言

本节分为四大块:

1.商品 goods 组件(左侧 menu 布局、右侧食品列表布局、第三方插件库better-scroll 的应用)

2.购物车 shopcart 组件

3.购物车小球 cartcontrol 组件(动画实现)

4.购物车详情页

  PS:本节所有代码在文章底部。

 

 

商品 goods 组件

 

1. 外壳 CSS 设置

1 .goods
2     display flex
3     position absolute
4     /*header:134px,tab:40px*/
5     top 174px
6     bottom 46px
7     width 100%
8     overflow hidden
外壳 CSS 代码设置

1)这一块的布局是左侧固定,右侧随屏幕宽度自适应,所以用 flex 布局。

2)左右两侧内容超过手机屏幕宽度时,并没有产生滚动,要定义视口高度,所以应该是绝对定位布局(要设置 top和 left)。并且超过视口高度的内容会被隐藏。

3).menu-wrapper 另外设置 width 80px,是为了解决兼容性问题。因为不设置的话在Android浏览器下会有问题。设置的话,在不兼容 flex 的浏览器也能有 80px 的宽。

 

2. 数据获取设置

 1     const ERR_OK = 0;
 2     ...
 3     props: {
 4       seller: {
 5         type: Object
 6       }
 7     },
 8     data() {
 9       return {
10         goods: []
11       };
12     },
13     created() {
14       this.$http.get('/api/goods').then((response) => {
15         response = response.body;
16         if (response.errno === ERR_OK) {
17           this.goods = response.data;
18           this.$nextTick(() => {
19             this._initScroll();
20             this.calculateHeight();
21           });
22         }
23       });
24     },
数据获取相关代码

先通过 vue-router 将 seller 传进来,使用 props 接收 seller 对象,后续内容会用到 seller 对象。

goods 组件内容要用到 goods 数据,这些数据后期会使用在 DOM 元素上。因为 data() 方法里的变量,会被自动增加 getter 和 setter 方法,其变化能直接映射到 DOM 上。所以这里先使用 data() 方法返回该空数组 goods,然后利用 vue-resource 这个技术去获取数据。

以下是在 data() 里面的变量和没在 data() 里面的变量的对照图:

  

 

3. 左侧 menu 布局

 1     <div class="menu-wrapper" ref="menuWrapper">
 2         <ul>
 3           <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
 4             <span class="text border-1px">
 5               <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>{{item.name}}
 6             </span>
 7           </li>
 8         </ul>
 9       </div>
10     ...
11 
12     created() {
13       this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
14     },
15     ...
16 
17     .menu-wrapper
18       /*三个参数:等分,占位情况,缩放空间*/
19       flex 0 0 80px
20       width 80px
21       background #f3f5f7
22       .menu-item
23         display table
24         height 54px
25         width 56px
26         padding 0 12px
27         line-height 14px
28         &.current
29           position relative
30           /*要盖住border,要在最顶层*/
31           z-index 10
32           /*盖住上边的border*/
33           margin-top -1px
34           background #fff
35           .text
36             border-none()
37             font-weight 700
38         .icon
39           display inline-block
40           vertical-align top
41           width 12px
42           height 12px
43           margin-right 2px
44           background-size 12px 12px
45           background-repeat no-repeat
46           &.decrease
47             bg-image('decrease_3')
48           &.discount
49             bg-image('discount_3')
50           &.guarantee
51             bg-image('guarantee_3')
52           &.invoice
53             bg-image('invoice_3')
54           &.special
55             bg-image('special_3')
56         .text
57           display table-cell
58           width 56px
59           vertical-align middle
60           border-1px (rgba(7, 17, 27, 0.1))
61           font-size 12px
左侧 menu 布局相关代码

1)侧边栏是列表,用 ul 元素

2)图片需要判断(type>0)才有图片,这里和上一节说到的 supports 一样,所以这里需要有 classMap 的数组。

3)技巧:用 class 不用 tag,因为查找速度上 class 会更快

4)dispay table:不定行的垂直居中,用 table 布局最有效。

 

4. 右侧食品列表布局

 1     <div class="foods-wrapper" ref="foodsWrapper">
 2         <ul>
 3           <li v-for="(item,index) in goods" class="food-list food-list-hook">
 4             <h1 class="title">{{item.name}}</h1>
 5             <ul>
 6               <li v-for="food in item.foods" class="food-item border-1px">
 7                 <div class="icon">
 8                   <img :src="food.icon" width="57" height="57">
 9                 </div>
10                 <div class="content">
11                   <h2 class="name">{{food.name}}</h2>
12                   <p class="desc">{{food.description}}</p>
13                   <div class="extra">
14                     <span class="count">月售{{food.sellCount}}份</span><span>好评率{{food.rating}}%</span>
15                   </div>
16                   <div class="price">
17                     <span class="now">¥{{food.price}}</span><span v-show="food.oldPrice"
18                                                                   class="old">¥{{food.oldPrice}}</span>
19                   </div>
20                 </div>
21               </li>
22             </ul>
23           </li>
24         </ul>
25       </div>
26     ...
27     
28     .foods-wrapper
29       flex 1
30       .title
31         padding-left 14px
32         height 26px
33         line-height 26px
34         border-left 1px solid #d9dde1
35         font-size 12px
36         color rgb(147, 153, 159)
37         background #f3f5f7
38       .food-item
39         display flex
40         margin 18px
41         padding-bottom 18px
42         border-1px(rgba(7, 17, 27, 0.1))
43         &:last-child
44           border-none()
45           margin-bottom 0
46         .icon
47           flex 0 0 57px
48           margin-right 10px
49         .content
50           flex 1
51           .name
52             margin 2px 0 8px
53             height 14px
54             line-height 14px
55             font-size 14px
56             color rgb(7, 17, 27)
57           .desc, .extra
58             font-size 10px
59             color rgb(147, 153, 159)
60           .desc
61             margin-bottom 8px
62             line-height 12px
63           .extra
64             line-height 10px
65             .count
66               margin-right 12px
67           .price
68             font-weight 700
69             line-height 24px
70             .now
71               margin-right 8px
72               font-size 14px
73               color rgb(240, 20, 20)
74             .old
75               text-decoration line-through
76               font-size 10px
77               color rgb(147, 153, 159)
右侧食品列表布局相关代码

1)先遍历 goods,获得食品分类,再遍历分类下的详细商品。使用列表嵌套。

2)不知道有没有 food.oldPrice ,要判断一下。

3)分为左右布局,左边图片固定,右边文字内容自适应,所以又用到了flex布局

4)有 1 像素的 border,但每个列表最后一个 li 是没有这个样式的,所以要去掉这条线。在 mixin.styl 中写 border-none 的代码,只需要设置 display 为 none 即可。

1 border-none()
2   &:after
3     display none
border-none()

5)li 虽然设了 margin 18px,但是因为上下两个 li 的 margin 会重合的关系,实际只有18px,而不是36px。为解决这个问题,需要额外加上 padding-bottom 18px。但这里又会带来新的问题,即最后一个 li 因为这两行代码设置,会有总共 36px 的底部边距,所以要给最后一个子元素 li 去除其中一个边距,所以给最后一个 li 元素(&:last-chuild)加上 margin-bottom 0。

6)在浏览器中发现不能滚动,是因为设置了视口高度,并且设置了 overflow 为 hidden。

 

5. 第三方插件库 better-scroll 的应用

接下来添加滚动功能:该功能依赖第三方插件库 better-scroll。 

 1     <div class="menu-wrapper" ref="menuWrapper">
 2     <div class="foods-wrapper" ref="foodsWrapper">
 3     ...
 4     
 5     import BScroll from 'better-scroll';
 6 
 7     methods: {
 8       _initScroll() {
 9         this.menuScroll = new BScroll(this.$refs.menuWrapper, {});
10         this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {});
11       }
前3点部分代码

1)安装并导入 better-scroll 。

2)实例化 better-scroll 。实例化时需要接收 DOM,要把 menu-wrapper 和 foods-wrappe r传送给 BScroll 实例。在 methods 中定义方法_initScroll(DOM,option) 。better-scroll 中,这个方法的参数设置和 iscroll 一样,需要接收两个参数:第一个参数是 DOM 对象;第二个参数是 option 对象,是一个json对象。

3)DOM 在 vue 中如何获得?通过 vue 2.0 的特殊特性 ref 来获得。ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

PS:
1.DOM 元素的获取,由 v-el 迁移为 ref 。
2.命名规则:HTML 中命名用中划线不用驼峰写法,是因为 HTML 大小写不敏感。JavaScript 中用驼峰写法。例如:

v-el:menu-wrapper    /*HTML中用中划线*/
this.menuScroll = new BScroll(this.$refs.menuWrapper);  // JavaScript中用驼峰法

这样通过 $refs 就获取到了 DOM,此处的 option 先不传。

 

4)在浏览器查看,发现已有相应样式,但页面却没办法滚动?

 1     created() {
 2       this.$http.get('/api/goods').then((response) => {
 3         response = response.body;
 4         if (response.errno === ERR_OK) {
 5           this.goods = response.data;
 6 //          console.log(this.goods);
 7           this.$nextTick(() => {
 8             this._initScroll();
 9             this.calculateHeight();
10           });
11         }
12       });
13     },
$nextTick

滚动原理:当页面内容的高度超过视口高度的时候,会出现滚动条。这里的页面没法滚动,是因为没有计算出正确高度。

问题:vue 中有一个 nextTick 方法,这个方法在数据修改之后会被立即调用,从而获取更新后的 DOM。因为是异步更新数据,所以这里虽然改变了数据,但此时 DOM 并没有变化,DOM 没有变化,则初始化 betterScroll,高度就会有问题。

解决办法:在 created() 中,调用 vue 的 nextTick 接口,传进去一个匿名函数 _initScroll(),就能正确计算高度。因为此时已经获取了内层 ul 的高度,当判断到内层高度大于外层 wrapper 的高度时,就可以发生滚动。

PS:关于 Vue.nextTick 和 vm.$nextTick的不同 。

 

5)实现左右联动。

可以实时计算右侧滚动时的坐标值,即 y 值落在哪个区间。通过判断右侧食品列表 y 值所在的区间,就可以确定左边 menu 就要显示到哪个区间。所以应该先计算整体区间高度(最高点即最低点),即没个商品分类区间高度,然后获取实时滚动的 y 值,判断y 值到底位于哪个区间,进而得到左侧 menu 高亮的索引值,最后用 vue 的 :class 绑定高亮设置即可。

第一步:计算右侧索引高度。

先定义 listHeight 数组,实现计算高度的方法 calculateHeight(),在 $nextTick() 接口中调用该方法。

 1 <li v-for="(item,index) in goods" class="food-list food-list-hook">
 2 ...
 3 
 4 data() {
 5       return {
 6         listHeight: [], // 存放大区间高度的数组
 7        };
 8     },
 9 ...
10 
11 created() {
12     ...
13           this.$nextTick(() => {
14             this.calculateHeight();
15           });
16         ...
17     },
18 methods: {
19       calculateHeight() {
20 //        获取每个大区间的li(标题和内容)
21         let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
22         let height = 0;
23         this.listHeight.push(height);
24         for (let i = 0; i < foodList.length; i++) {
25           let item = foodList[i];
26           height += item.clientHeight;
27           this.listHeight.push(height);
28         }
29       },
计算区间高度代码

1. 每个食品分类区间高度的计算是一个数值累加的过程,为了方便跟踪依赖,在 data() 中定义数组 listHeight。

2. 实现计算高度的方法 calculateHeight()。先通过 $refs 获得外层 foodsWrapper 的 DOM 元素,再通过原生的 DOM 方法getElementsByClassName(),获得每个食品分类区间的 li 元素,存放在变量 foodList 数组中。

3. 应该在拿到数据后才计算高度,即在 $nextTick() 中,此时 DOM 已更新,可以正确计算高度。

PS:小技巧:给每个食品分类区间的 li 元素定义一个带 hook 的类名,例如:food-list-hook,在实际编程中,带 hook 的类名一般是没有样式定义的,只是供 js 代码中选择 DOM 元素时使用。

第二步:获得右侧实时的 y 值,与左侧的索引值做一个映射。

 1 <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}"
 2 ...
 3 
 4     data() {
 5       return {
 6         listHeight: [], // 存放大区间高度的数组
 7         scrollY: 0,   // 右侧实时变化的y值
 8       };
 9     },
10     created() {
11       this.$http.get('/api/goods').then((response) => {
12         response = response.body;
13         if (response.errno === ERR_OK) {
14           this.goods = response.data;
15           this.$nextTick(() => {
16             this._initScroll();
17             this.calculateHeight();
18           });
19         }
20       });
21     },
22     methods: {
23       _initScroll() {
24         this.menuScroll = new BScroll(this.$refs.menuWrapper, {});
25         this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
26           probeType: 3
27         });
28         this.foodsScroll.on('scroll', (pos) => {
29           this.scrollY = Math.abs(Math.round(pos.y));
30         });
31       },
32       calculateHeight() {
33         let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
34         let height = 0;
35         this.listHeight.push(height);
36         for (let i = 0; i < foodList.length; i++) {
37           let item = foodList[i];
38           height += item.clientHeight;
39           this.listHeight.push(height);
40         }
41       },
42     },
43     computed: {
44       currentIndex() {
45         for (let i = 0; i < this.listHeight.length; i++) {
46           let height1 = this.listHeight[i];
47           let height2 = this.listHeight[i + 1];
48           if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
49             return i;
50           }
51         }
52         return 0;
53       }
54     }
55 ...
56       &.current
57           position relative
58           z-index 10
59           margin-top -1px
60           background #fff    
获得右侧实时 y 值并与左侧索引值做映射

1. 在 data() 中定义变量 scrollY,存放右侧实时变化的 y 值。

2. 在右侧食品列表的 better-scroll 实例中将 probeType 的值设置为 3,用于实时监听滚动位置,即可以在滚动时实时派发 scroll 事件,告诉我们滚动的位置,相当于探针的作用。

3. 利用 better-scroll 的一个事件:scroll,获取实时的 scrollY。

this.foodsScroll.on('scroll', (pos) => {
    this.scrollY = Math.abs(Math.round(pos.y));
});    

pos 是滚动的实时坐标,pos.y 是坐标的 y 值,是一个负值,使用 round 方法取整,再用 abs 方法将负数转为正数。

4. 计算 scrollY 位于哪个区间。

在计算属性 computed 中定义 currentIndex()。遍历 listHeight,获取当前索引值对应的高度及下一个索引值对应的高度,判断 scrollY 是不是位于这个范围中,是的话返回当前索引值。这里有一个问题,就是当遍历到最后一个 li 时,没有对应的下一个索引值的高度,也就是说此时 scrollY 是在最后一个食品分类区间,这样的话也要返回当前的索引值,其余情况下返回 0。

5. 与左侧索引做一个映射。

当滚动时,根据 vue 特性,currentIndex() 会重新计算。接下来给 .menu-item 绑定一个 :class,可以在其中加入简单的判断语句。

<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}"

currentIndex() 函数会返回 scrollY 所处的区间的索引值,遍历左侧 menu 列表,当找到与右侧函数返回的索引值相等的 index 值时,就会给当前的 li 设置一个 current 的类。接下来给 current 设置样式即可。

第三步:实现左侧 menu 点击功能,左侧点击,右侧就跳到相应位置。

 1 <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
 2 ...
 3     methods: {
 4       selectMenu(index, event) {
 5         if (!event._constructed) {
 6           // eslint-disable-next-line
 7           return;
 8         }
 9         let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
10         let el = foodList[index];
11         this.foodsScroll.scrollToElement(el, 300);
12       },
13       _initScroll() {
14         this.menuScroll = new BScroll(this.$refs.menuWrapper, {
15           click: true
16         });
17       },
18     }
点击功能代码

1. 给 .menu-item 添加点击事件selectMenu(),传进两个参数,一个是当前 li 对应的索引值index,另一个是事件 event。

2. 先通过 $refs 获得外层 foodsWrapper 的 DOM 元素,再通过原生的 DOM 方法getElementsByClassName(),获得每个食品分类区间的 li 元素,存放在变量 foodList 数组中。

3. 通过 index 值,找到右侧食品列表中对应的食品分类对应 index 的 DOM 元素,使用 better-scroll 的 scrollToElement 接口,直接滚动到相应元素位置。scrollToElement() 需要传入两个参数,第一个是 DOM 元素,第二个是滚动时间。这里设置滚动时间为300毫秒。

4. 在手机端调试时发现点击没有效果。这是因为 better-scroll 会监听 touchstar、touchmove、touchend 等事件,会使用 preventDefault 阻止掉这些默认事件,所以这里点击是没有效果的。需要在初始化时(_initScroll)设置 click:true,这样就实现了点击功能。

5. 当调试模式由手机到转到 PC 时,会发现点击效果被实现了两次。这是因为在PC端时,默认事件并没有被阻止,原生点击也能被监听到。又因为在初始化时设置 click:true,即派发了一个单击事件,合起来相当于派发了两个点击事件,所以点击的回调函数被执行了两次。

怎么解决:在 DOM 元素中添加点击事件时,传入事件 $event,selectMenu() 收到 event 变量,这个 event,就是点击时传递的 event 事件。better-scroll中,派发的事件与原生点击事件有一个属性区别:自定义派发的事件,会有一个私有属性 constructed,值 为 true,而浏览器原生点击事件是没有这个属性的。我们可以在函数中加上判断,如果监听到浏览器原生点击事件,可以将它 return 掉,即逻辑不执行。只有当自定义派发的事件,逻辑才会接着往下走,执行函数。如何理解呢?当我们点击时,程序没有监听到私有属性 constructed,表明现在是浏览器原生点击事件,此时 return,即不执行。这样浏览器原生点击事件就不会被执行,只是执行我们默认派发的事件,这样就可以达到手机端和 PC 端都是点击函数只执行一次的效果。


总结:
1. 在vue开发时,与原生库做交互时,可以通过 ref 定义变量,然后通过 $refs 访问它,这相当于拿到了原生 DOM 。

2. 要进行一些涉及到 DOM 的计算操作时,必须保证 DOM 元素已经被渲染。虽然在 vue 中有数据的自然映射(改变数据就改变了 DOM),但实际上 DOM 真正改变是在 nextTick 回调函数之后。所以要操作原生DOM时,一定要调用 $nextTick() 接口,在这个接口的回调函数中做修改,保证 DOM 已经渲染好,这样在计算 DOM 相关属性时就不会发生错误。

3. nextTick$nextTick(),以及两者区别

4. 当遇到报错是关于 undefined、no function,一般都是前边调用函数或者对象错了,可以检查它们是不是 undefined。

5. methods 不要写成 method!!!

 

 

购物车 shopcart 组件

1. 新建组件(components -> shopcart ->shopcart.vue),在 good.vue 引入注册并使用。

 1 <template>
 2  <div class="shopcart"></div>
 3 </template>
 4 
 5 <script type="text/ecmascript-6">
 6   export default {};
 7 </script>
 8 
 9 <style lang="stylus" rel="stylesheet/stylus">
10 
11 </style>
shopcart.vue
1 <shopcart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
2 
3 import shopcart from '../../components/shopcart/shopcart';
4 
5 export default {
6     components: {
7       shopcart
8     }  
9 };
goods.vue

测试: 

 

2. 布局及 CSS 设置

  1 <template>
  2   <div>
  3     <div class="shopcart">
  4       <div class="content" @click="toggleList">
  5         <div class="content-left">
  6           <div class="logo-wrapper">
  7             <div class="logo" :class="{'highlight':totalCount>0}">
  8               <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i>
  9             </div>
 10             <div class="num" v-show="totalCount>0">{{totalCount}}</div>
 11           </div>
 12           <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
 13           <div class="desc">另需配送费¥{{deliveryPrice}}元</div>
 14         </div>
 15         <div class="content-right" @click.stop.prevent="pay">
 16           <div class="pay" :class="payClass">{{payDesc}}</div>
 17         </div>
 18       </div>
 19   </div>
 20 </template>
 21 
 22 <style lang="stylus" rel="stylesheet/stylus">
 23   @import "../../common/stylus/mixin.styl"
 24 
 25   .shopcart
 26     position fixed
 27     left 0
 28     bottom 0
 29     /*多出的区块(购物车)要盖住上面的区块*/
 30     z-index 50
 31     width 100%
 32     height 48px
 33     .content
 34       display flex
 35       background #141d27
 36       /*消除空白间隙影响*/
 37       font-size 0
 38       .content-left
 39         flex 1
 40         color rgba(255, 255, 255, 0.4)
 41         .logo-wrapper
 42           display inline-block
 43           vertical-align top
 44           position relative
 45           top -10px
 46           margin 0 12px
 47           padding 6px
 48           /*此处IF盒模型,包括padding,border在内*/
 49           width 56px
 50           height 56px
 51           box-sizing border-box
 52           border-radius 50%
 53           background #141d27
 54           .logo
 55             width 100%
 56             height 100%
 57             border-radius 50%
 58             text-align center
 59             background #2b343c
 60             &.highlight
 61               background rgb(0, 160, 220)
 62             .icon-shopping_cart
 63               line-height 44px
 64               font-size 24px
 65               color #80858a
 66               &.highlight
 67                 color #fff
 68           .num
 69             position absolute
 70             top 0
 71             right 0
 72             /*绝对定位,要指定宽高,才能撑开*/
 73             width 24px
 74             heigt 16px
 75             line-height 16px
 76             text-align center
 77             border-radius 16px
 78             font-size 9px
 79             font-weight 700
 80             color #ffffff
 81             background rgb(240, 20, 20)
 82             box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.4)
 83         .price
 84           display inline-block
 85           vertical-align top
 86           margin-top 12px
 87           padding-right 12px
 88           line-height 24px
 89           box-sizing border-box
 90           border-right 1px solid rgba(255, 255, 255, 0.1)
 91           font-size 16px
 92           font-weight 700
 93           &.highlight
 94             color #fff
 95         .desc
 96           display inline-block
 97           vertical-align top
 98           margin 12px 0 0 12px
 99           line-height 24px
100           font-size 10px
101       .content-right
102         flex 0 0 105px
103         width 105px
104         color rgba(255, 255, 255, 0.4)
105         .pay
106           height 48px
107           text-align center
108           line-height 48px
109           font-size 12px
110           font-weight 700
111           &.not-enough
112             background #2b333b
113           &.enough
114             background #00b43c
115             color #fff
116 </style>
布局及 CSS 代码

1)购物车组件时定义在视口底部,所以应该是 fixed 布局。

2)右侧固定宽度,左侧自适应。右侧除了设置 flex 0 0 105px,还另外设置 width 105px,是为了解决兼容性问题。

3) 因为购物车 logo 是超出父元素,所以应该设置为相对定位,并设置 top 值。

4)将 logo-wrapper 设置为 IE 盒模型(border-box),即包括 padding 和 border 在内。

5)这里有两块需要垂直居中,但设置不一样。左侧的使用 line-height 与 margin-top 结合实现垂直居中,右侧的使用 height = line-height 实现垂直居中。为什么左侧要这么设置呢?这是因为左侧中间需要设置 border,如果使用 height = line-height,那么 border 的高度会等于区块高度,而不是像图中的设计效果。

 

 6)商品总数是绝对定位

 

3. 在 goods 组件中使用 shopcart 组件

<shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>

在 good.vue 中传递参数最低起送费和配送费,如果仅仅是这么写会遇到以下报错信息:

vue.esm.js?efeb:576 [Vue warn]: Error in render: "TypeError: Cannot read property 'deliveryPrice' of undefined"

如何解决这个问题?遇到这种 undefined 问题,找找上层问题。 

要先从 good.vue 找找是否有接收seller对象:

    props: {
      seller: {
        type: Object
      }
    },

有的话,再到外层的 vue 找找是否有传入 seller,在 App.vue 文件中发现 router-view 并没有传入seller,因此在这了传入 seller:

<router-view :seller="seller"></router-view>

这样 goods组件 才能获得 seller,goods 组件才能再传给 shopcart 组件。

到这里这两个参数还没办法使用,需要在 shopcart 组件中通过 props 接收这两个参数:

 1     props: {
 2       deliveryPrice: {
 3         type: Number,
 4         default: 0
 5       },
 6       minPrice: {
 7         type: Number,
 8         default: 0
 9       }
10     },

至此,shopcart 组件才算被 goods 组件正确使用。

 

4. 购物车是对选择商品的映射 

可以看到,购物车一共有三种变化。购物车是对选择商品的映射,商品的选择是在 goods 组件中进行的,应该通过 goods 组件告诉shopcart 组件一共选择了多少商品,然后根据商品的价格进行样式的调整。

 1     <div class="content">
 2         <div class="content-left">
 3           <div class="logo-wrapper">
 4             <div class="logo" :class="{'highlight':totalCount>0}">
 5               <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i>
 6             </div>
 7             <div class="num" v-show="totalCount>0">{{totalCount}}</div>
 8           </div>
 9           <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
10           <div class="desc">另需配送费¥{{deliveryPrice}}元</div>
11         </div>
12         <div class="content-right">
13           <div class="pay" :class="payClass">{{payDesc}}</div>
14         </div>
15       </div>
16     ...
17 
18 export default {
19     props: {
20       selectFoods: {
21         type: Array,
22         default() {
23           return [];
24         }
25       },
26       deliveryPrice: {
27         type: Number,
28         default: 0
29       },
30       minPrice: {
31         type: Number,
32         default: 0
33       }
34     },
35     computed: {
36       totalPrice() {
37         let total = 0;
38         this.selectFoods.forEach((food) => {
39           total += food.price * food.count;
40         });
41         return total;
42       },
43       totalCount() {
44         let count = 0;
45         this.selectFoods.forEach((food) => {
46           count += food.count;
47         });
48         return count;
49       },
50       payDesc() {
51         if (this.totalPrice === 0) {
52           return `¥${this.minPrice}元起送`;
53         } else if (this.totalPrice < this.minPrice) {
54           let diff = this.minPrice - this.totalPrice;
55           return `还差¥${diff}元起送`;
56         } else {
57           return '去结算';
58         }
59       },
60       payClass() {
61         if (this.totalPrice < this.minPrice) {
62           return 'not-enough';
63         } else {
64           return 'enough';
65         }
66       }
67     }
68 };
69 ...
70 
71     .content-left
72         ...
73           .logo
74             &.highlight
75               background rgb(0, 160, 220)
76             .icon-shopping_cart
77               &.highlight
78                 color #fff
79         .price
80           &.highlight
81             color #fff
82       .content-right
83         ...
84         .pay
85           &.not-enough
86             background #2b333b
87           &.enough
88             background #00b43c
89             color #fff
购物车是对选择商品的映射的代码

 

1)在 props 中接收 selectFoods(selectFoods 应该是由父组件 goods 传过来的),它是选择了的商品的数组,为它添加默认 default 值(一般,如果 type 是 Array 或者 Object,则 default 是一个函数)。

 

2)totalPrice() 

totalPrice(商品总价)是根据数组 selectFoods 进行计算的,商品总价是等于选择商品的个数乘以单价。在这里,希望 selectFoods 是存放每个 food 的内容,在 data.json 可以看到,每个 food 有很多描述信息,其中有单价。所以这里希望添加一个 count,用于存放选择商品的个数。

在计算属性 computed 中,添加 totalPrice() 方法。首先遍历数组 selectFoods,将数组中的每种商品的总价,最后再计算出 totalPrice。

如何测试?给 selectFoods 添加数值,在浏览器中看看有没有效果,如果 price 由 0 变成 10,证明设置成功。

<div class="price">¥{{totalPrice}}</div>
  ...  
    selectFoods: {
         type: Array,
         default() {
           return [
              price: 10,
               count: 1
           ];
         }
       }

 

3)totalCount()

购物总商品数目也是与 selectFoods 有关。在计算属性 computed 中,添加 totalCount() 方法。遍历数组 selectFoods,计算出 totalCount。

 

4)进行左侧高亮设置及 v-show 设置

有选择商品的时候,样式上会有一些变化。在 CSS 中进行高亮设置,在 HTML 中进行判断,如:购物车图标的变化,如果 totalCount>0 ,则显示高亮设置;而总价文字的变化,则需要通过 totalPrice>0 判断。购物车左上角关于商品总数的显示:当 totalCount>0 则显示。

 1 <div class="logo-wrapper">
 2     <div class="logo" :class="{'highlight':totalCount>0}">
 3         <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i>
 4     </div>
 5     <div class="num" v-show="totalCount>0">{{totalCount}}</div>
 6 </div>
 7 <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>    
 8 ...
 9 .logo
10     &.highlight
11         background rgb(0, 160, 220)
12 .icon-shopping_cart
13     &.highlight
14         color #fff    

 

5)进行右侧设置

文字变化(三种):¥X元起送,还差¥X元起送,去结算。

在计算属性 conputed 中定义变量 payDesc(),进行判断:当总价为 0 时,返回文字“¥X元起送”,当总价低于最低起送价时,返回文字“还差¥X元起送”,当总价高于最低起送价时,返回文字“去结算”。

样式变化:背景颜色改变。

先定义 not-enough 和 enough 德阳市,然后给 DOM 元素绑定 :class,给它定义一个变量 payClass。在计算属性 conputed 中定义变量 payClass(),进行判断:当总价低于最低起送价时,返回样式“not-enough”,当总价高于最低起送价时,返回样式“enough”。

PS:

1. 等号设置:要用三等号,二等号会进行隐式转换。

2. ES6的字符串扩展:使用反引号(键盘左上角波浪号),可以拼接变量和字符串,不需要写加号。可以用${}的方式传进变量。

没有变量时应该是用单引号,不能使用反引号,否则eslint会报错。

return `¥${this.minPrice}元起送`;
return '去结算';

 

 

接下来加上添加删除按钮,让购买和购物车组件联动起来。

 

购物车小球 cartcontrol 组件(动画实现)

按钮是多次出现的,被多次复用,所以抽象为组件 cartcontrol 。

1. 新建组件(components -> cartcontrol ->cartcontrol.vue),在 good.vue 引入注册并使用。

 

2. 布局及 CSS 样式

 1 <template>
 2   <div class="cartcontrol">
 3     <transition name="move">
 4       <div class="cart-decrease" v-show="food.count>0" @click="decreaseCart($event)">
 5         <!--inner:平移,inner1:滚动-->
 6         <span class="inner inner1 icon-remove_circle_outline"></span>
 7       </div>
 8     </transition>
 9     <div class="cart-count" v-show="food.count>0">{{food.count}}</div>
10     <div class="cart-add icon-add_circle" @click="addCart($event)"></div>
11   </div>
12 </template>
13 
14 <style lang="stylus" rel="stylesheet/stylus">
15   .cartcontrol
16     font-size 0
17     .cart-decrease
18       display inline-block
19       padding 6px
20       .inner
21         display inline-block
22         line-height 24px
23         font-size 24px
24         color rgb(0, 160, 220)
25         transition: all 0.4s linear
26         transform rotate(0)
27       &.move-enter-active, &.move-leave-active
28         transition: all 0.4s linear
29         transform: translate3d(0, 0, 0)
30       &.move-enter, &.move-leave-active //刚进入和离开后的状态
31         opacity: 0
32         transform: translate3d(24px, 0, 0)
33         .inner1
34           transform: rotate(180deg)
35     .cart-count
36       display inline-block
37       vertical-align top
38       width 12px
39       padding-top 6px
40       line-height 24px
41       text-align center
42       font-size 12px
43       color rgb(147, 153, 159)
44     .cart-add
45       display inline-block
46       padding 6px
47       line-height 24px
48       font-size 24px
49       color rgb(0, 160, 220)
50 </style>
布局及 CSS 样式

为扩大小球点击的范围,可以设置 padding 6px 

 

3. 给点击按钮添加逻辑

 1 export default {
 2     props: {
 3       food: {
 4         type: Object
 5       }
 6     },
 7     created() {
 8 //      console.log(this.food);
 9     },
10     methods: {
11       addCart(event) {
12         if (!event._constructed) {
13           // eslint-disable-next-line
14           return;
15         }
16 //        console.log('click');
17         if (!this.food.count) {
18           Vue.set(this.food, 'count', 1);
19         } else {
20           this.food.count++;
21         }
22       },
23       decreaseCart(event) {
24         if (!event._constructed) {
25           // eslint-disable-next-line
26           return;
27         }
28 //        console.log('click');
29         if (this.food.count) {
30           this.food.count--;
31         }
32       }
33     }
34   };
逻辑设置部分代码

1)cartcontrol 与商品有关联,且关联的应该是单个的 food 的 count,这个数据是从父级传过来的。使用 props 接收从父级传过来的 food 属性。

2)在 methods 中添加 addCart() 方法。判断有没有被选择商品数量,没有的话,先设为 1,;有的话,在原来的基础上加 1 。

addCart() {
    if (!this.food.count) {
        this.food.count = 1;
    } else {
        this.food.count++;
    }    
}

PS:以下具体知识点在上述已经提及过

1. 要记得将浏览器原生的点击事件 return 掉,这样在浏览器点击时才不会有两次

2. 要在 goods.vue 中的 _initScroll() 方法的 foodsScroll 的 Bscroll 实例中添加 click: true,这样移动端才能实现点击功能

 

3)将点击事件添加到相应 DOM 元素中。使用 v-show,给减号小球以及中间的文字加上判断:当商品数量不大于 0 时,这两部分都不显示。 

4)调试时发现点击没有生效,因为减号没有出现。

因为 vue.js 有一个特性,当给观测对象添加一个本不存在的字段,像上面一样企图直接赋值是不行的,props 无法检测到新增字段的变化。所以新增或删除某字段时,要想观察到该字段的变化,需要通过 vue 的一个接口。这里需要先把 vue 导入,然后通过 vue.set() 方法去添加一个属性时,属性的变化就可以被观测到,这样最终可以通知DOM发生变化。

 1     import Vue from 'vue';
 2 
 3     addCart(event) {
 4         if (!event._constructed) {
 5           // eslint-disable-next-line
 6           return;
 7         }
 8         // console.log('click');
 9         if (!this.food.count) {
10           Vue.set(this.food, 'count', 1);
11         } else {
12           this.food.count++;
13         }
14       }    
addCart()方法相关代码

5)给减号按钮添加点击事件 decreaseCart() 

 

4. 减号按钮动画

使用 vue 组件 transition 实现动画。要实现平移和滚动的动画:外层组件负责平移,内层组件负责滚动。

 1 .cart-decrease
 2       display inline-block
 3       padding 6px
 4       opacity: 1
 5       transform: translate3d(0, 0, 0) //最终状态
 6       .inner
 7         display inline-block
 8         line-height 24px
 9         font-size 24px
10         color rgb(0, 160, 220)
11         transition: all 0.4s linear
12         transform rotate(0) // 最终状态
13       &.move-enter-active, &.move-leave-active //过程
14         transition: all 0.4s linear
15       &.move-enter, &.move-leave-active //刚进入和离开后的状态,只有一帧
16         opacity: 0
17         transform: translate3d(24px, 0, 0)
18         .inner
19           transform: rotate(180deg)

1)因为减号按钮其实是一个图标字体,而字体需要滚动,所以应该放在内层。

<div class="cart-decrease" v-show="food.count>0" @click="decreaseCart($event)">
    <span class="inner icon-remove_circle_outline"></span>
</div>

2)用 translate3d 属性实现平移动画,是因为这里可以利用 css 属性开启硬件加速,使动画更流畅。

3)程序首先应用 move-enter,设置透明度和小球位置,这个过程很快,只有一帧。接着应用 move-enter-active(这个类可以被用来定义过渡的过程时间,延迟和曲线函数) 实现平移动画,持续时间是 0.4s,平移最终状态应该是参照代码 transform: translate3d(0, 0, 0)。离开时的动画也是一样,但是为什么是设置 move-leave-active 而不是 move-leave 呢?因为 move-leave 只有一帧,如果设置 move-leave,透明度会瞬间变成 0,这样就看不到小球动画了。

4)滚动需要给 inner 设置 display:inline-block,这样按钮高度才不会为 0,才有宽高,滚动动画才能实现。

 

5. 完成 goods 组件中 selectFoods 属性

 1 <div class="cartcontrol-wrapper">
 2     <cartcontrol @event="getEvent" :food="food"></cartcontrol>
 3 </div>
 4 
 5 <shopcart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
 6 
 7     data() {
 8       return {
 9         goods: []
10       };
11     },
12 
13     computed: {
14      selectFoods() {
15         let foods = [];
16 //        遍历商品分类
17         this.goods.forEach((good) => {
18 //          遍历分类下的商品
19           good.foods.forEach((food) => {
20 //            如果food.count存在,即大于0,证明商品被选中,将被选中商品放进数组里
21             if (food.count) {
22               foods.push(food);
23             }
24           });
25         });
26         return foods;
27       }
28     },
selectFoods() 相关代码

cartconctrol 组件是修改 food.count 属性,而 food 是由父组件(good组件)传过来的对象。当修改对象,即给对象增加一些属性,会影响对象本身,也会影响父组件,即 goods 组件。food 与 selectFoods 有关。这里需要完善 selectFoods 属性。

selectFoods需要计算,在 good.vue 文件的计算属性 computed 中完善该属性。selectFoods 需要遍历 goods,拿到食品分类。再遍历分类下面的商品,接下来判断 food.count 即可,如果该值大于 0,证明商品被选中,将被选中的商品放进数组并返回。 最后将 selectFoods 传递给 shopcart 组件,将两个组件联动起来。

原理:cartcontrol 操作单个 food 对象,food 发生变化,则 selectFoods 也会跟着变化,这是因为 selectFoods 是计算属性,它观测的是 goods 对象(goods 对象是在 data 中定义的,凡是在 data 中定义变量或数组等,都会被加上 getter 和 setter 方法进行跟踪依赖),而 goods 对象又是由 food 组成的。一旦 goods 发生变化,selectFoods 就会重新计算,即逻辑再执行一遍,如果 food.count 存在,food 就会被放入返回的数组,selectFoods 也就有对应的值,这时,food.price 和 food.count 都有了,shopcart 组件就能正常显示,这样就将组件联动起来了。

  

 

6. 加号小球动画

加号小球进行抛物线动画,最终落点是购物车。与减号小球不同的是,加号小球只有 enter 过程没有 leave 过程。抛物线动画涉及到横轴坐标及纵轴坐标的变化,所以这里依旧需要两层,外层负责纵轴变化,内层负责横轴变化。这里的难点如何确定是小球初始位置。在确定小球位置后,可以使用 vue 组件 transition 的 JavaScript 钩子 完成小球下落动画。

1)在定义 shopcart.vue 文件一个类名为 ball-contain 的 div 元素,里面通过 v-for 遍历数组 balls。balls 数组中一共有五个小球,数组每个元素维护当前小球状态。

2)data 会返回一个对象,对象里面有一个数组 balls,将每个小球的 show 属性设置为 false。这样每个小球初始时都是隐藏的,通过后续代码设置使其进行动画演示。

3)在 CSS代码中设置小球落点的最终位置。

4)通过代码动态获取被点击小球的对应的 DOM 元素(这相当于获得了小球的初始位置)

当点击小球时会产生动画,点击就触发动作,所以可以在点击事件中派发事件,传送一个 DOM 对象。

vue 1.0:

思路:在 cartcontrol 中通过 $dispatch() 方法派发事件,DOM 对象作为事件参数传入,在 goods 组件添加事件 events,用于接收 cart.add 事件,这个事件会接收一个参数 target,这里就由父组件接收到 cart.add 传来的一个事件,接下来处理这个事件。父组件接收到这个事件后,应该调用子组件(shopcart)的一个方法,子组件通过方法传入一个 target,然后处理一个下落函数,然后就可以处理target,通过drop方法传入target。

实现:在 cartcontrol 的 methods 中定义 drop 方法,这个方法就是要调用 shopcart 的 drop 方法。在 shopcart 的 methods 中定义 drop 方法,这个方法支持定义参数 el,这个 el 就是 cartcontrol。在 goods 组件中,私有方法 drop 里,调用 drop 方法的 target,target 的获得是通过事件监听,将当前 target 传进来,再去调用 drop 方法。

顺序:通过 cartcontrol 里面的 DOM 元素传递给父组件(goods),父组件再调用子组件(shopcart)的 drop方 法,在将 target 传递给子组件。

 

vue 2.0 中语法改变:组件通信变化 $dispatch 废除,事件监听变化,废除 events 属性。

$dispatch() 废除原因:事件不能直接通过 $dispatch 派发到当前父组件上,所以实际上还是在子组件中监听了 addFood 事件,只是因为这个方法可以在父组件中定义,父组件就相当于接收到这个事件。

$dispatch 与 $emit 的区别:$dispatch是子组件通过冒泡,将事件一层层冒泡到父组件上 。$emit 是在当前实例上派发的事件。即当前事件只能在子组件上通过 on 监听到,而不能在父组件上监听到。

 

vue 2.0:

使用 vue 实例方法 $emit。父组件是通过 @add="addFood" 的方式监听add事件,而不是通过添加 events 属性后通过 events 监听事件。也就是说,使用 @add="addFood" 这个方式,实际上依旧是在 goods 组件上监听到 add 事件,只是 add 事件监听的回调事件是 addFood,这个方法可以在 goods 组件中定义,这样在父组件中就可以通过 addFood 方法去执行回调。

methods: {
      addCart(event) {
//        将DOM对象作为事件的参数传递出去,用于计算小球的位置
        this.$emit('add', event.target);
      }
    }
cartcontrol.vue
 1     <cartcontrol :food="food" @add="addFood"></cartcontrol>
 2 
 3     methods: {
 4       addFood(target) {
 5         this.drop(target);
 6       },
 7       drop(el) {
 8         console.log(el);
 9       },
10     }
shopcart.vue
 1     <cartcontrol @add="addFood" :food="food"></cartcontrol>
 2 
 3     methods: {
 4       addFood(target) {
 5         this._drop(target);
 6       },
 7       // drop方法(父组件的私有方法)调用子组件(shopcart)的drop方法
 8       _drop(target) {
 9         // 体验优化,异步执行下落动画
10         this.$nextTick(() => {
11           this.$refs.shopcart.drop(target);
12         });
13       },
14     }
goods.vue

 

详细解析:

在 cartcontrol.vue 文件中的加号按钮点击事件 addCart 中通过 this.$emit('add', event.target); 将 DOM 元素作为参数,等待传出。在goods.vue 组件中通过 @add="addFood" 方式触发 add 事件,DOM 元素就传到了 goods.vue 中。goods.vue 通过 addFood 执行回调函数,调用 goods.vue 中的私有方法 drop,并将 DOM 元素传给 drop 方法。goods.vue 中的私有方法 drop 调用子组件 shopcart 中的 drop 方法。如何调用子组件呢?可以通过 ref 访问子组件,然后通过 this.$refs.shopcart 方法访问到子组件。接下来完善 shopcart 组件中的 drop 方法。

 

5)完善 shopcart 组件中的 drop 方法 

遍历 balls 数组,取出第一个 show 值为 false 的小球(相当于隐藏),将这个小球的 show 值为 true,并用一个 el 对象保存当前小球的 element,最后将这个球放进数组 dropBalls 中。数组 dropBalls 是存放下落小球,是需要跟踪依赖的,所以在 data 中定义该数组。

 

6)完成小球下落动画

使用 vue 组件 transition 的 JavaScript 钩子 完成动画。drop有三个钩子:beforeEnter,enter,afterEnter,每个钩子都接受一个参数,及当前执行动画的DOM对象。

 1 beforeDrop(el) {
 2         // 遍历所有show为true的小球,给这些小球加上下落动画
 3 //        console.log('beforeDrop' + el);
 4         let count = this.balls.length;
 5         while (count--) {
 6           let ball = this.balls[count];
 7           if (ball.show) {
 8             // getBoundingClientRect:获取DOM元素相对于视口的位置,返回值的left和top就是相对于视口的偏移
 9             let rect = ball.el.getBoundingClientRect();
10             let x = rect.left - 32;
11             // y值本身是负值,所以取反
12             let y = -(window.innerHeight - rect.top - 22);
13             // 因为v-show会将display设置为none,即不显示;这里设置为空,即显示
14             el.style.display = '';
15             // 兼容性设置
16             // 外层元素设置纵向动画
17             el.style.webkitTransform = `translate3d(0,${y}px,0)`;
18             el.style.transform = `translate3d(0,${y}px,0)`;
19             // 内层元素设置横向动画
20             let inner = el.getElementsByClassName('inner-hook')[0];
21 //            console.log('inner' + inner);
22             inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
23             inner.style.transform = `translate3d(${x}px,0,0)`;
24           }
25         }
26       },
27       // 下落动画完成,小球进入购物车的状态
28       dropping(el) {
29         // 需要先触发浏览器重绘,然后重置样式,单纯字符串,没有变量时,要用单引号而不是反引号
30         // re变量之后是不用的,所以要写一行eslint,让其跳过该检查
31         /* eslint-disable no-unused-vars */
32         let rf = el.offsetHeight;
33         this.$nextTick(() => {
34           el.style.webkitTransform = 'translate3d(0,0,0)';
35           el.style.transform = 'translate3d(0,0,0)';
36           let inner = el.getElementsByClassName('inner-hook')[0];
37           inner.style.webkitTransform = 'translate3d(0,0,0)';
38           inner.style.transform = 'translate3d(0,0,0)';
39         });
40       },
41       // 动画完成,将小球状态重置
42       afterDrop(el) {
43         // 将小球取出
44         let ball = this.dropBalls.shift();
45         if (ball) {
46           ball.show = false;
47           el.style.display = 'none';
48         }
49       },
下落动画

beforeDrop(el):遍历所有 show 为 true 的小球,给这些小球加上下落动画。

1. ball.show 值为 true 时触发动画

2. getBoundingClientRect:获取DOM元素相对于视口的位置,返回值的 left 和 top 就是相对于视口的偏移

3. x 值和 y 值是小球现在所处位置

4. 样式设置:因为 v-show 指令会将 display 设置为 none,即不显示;这里先将设置 display 设置为空,即显示。然后设置动画。

5. CSS设置:transition 的默认值是(all 0 ease 0)。要达到抛物线效果,需要加上 贝塞尔曲线。x 轴不需要滑动设置,所以要设置 linear。

&.drop-enter-active, &.drop-leave-active
    transition all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
  .inner
    transition all 0.4s linear  

6. 因为涉及变量还有字符串的拼接,可以使用 ES6 语法中的反引号,然后用 ${} 方式传入变量

dropping(el):下落动画完成,小球进入购物车的状态

1. 需要先触发浏览器重绘,然后重置样式。let rf = el.offsetHeight;

2. re变量之后是不用的,所以要写一行eslint,让其跳过该检查。 /* eslint-disable no-unused-vars */

3. 单纯字符串,没有变量时,要用单引号而不是反引号

4. 样式设置:x 和 y 坐标应该发生变化

afterDrop(el):动画完成,将小球状态重置

从数组 dropBalls 中取出小球,将其 show 值重置为 false,并将其 display 设置为 none 即可。

 

7)优化设计

当第一次选择商品时,即同时进行加号按钮和减号按钮动画时,动画会有点卡。这是因为两个动画开始时都需要计算,计算量比较大。如何优化?在 good.vue 文件的私有方法 drop 中设置,不要一开始就执行drop函数。

    _drop(target) {
        // 体验优化,异步执行下落动画
        this.$nextTick(() => {
          this.$refs.shopcart.drop(target);
        });
      },

 

 

购物车详情页

1. 布局设置及样式设置

  1 <template>
  2   <div>
  3     <div class="shopcart">
  4       <div class="content" @click="toggleList">
  5         <div class="content-left">
  6           <div class="logo-wrapper">
  7             <div class="logo" :class="{'highlight':totalCount>0}">
  8               <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i>
  9             </div>
 10             <div class="num" v-show="totalCount>0">{{totalCount}}</div>
 11           </div>
 12           <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
 13           <div class="desc">另需配送费¥{{deliveryPrice}}元</div>
 14         </div>
 15         <div class="content-right" @click.stop.prevent="pay">
 16           <div class="pay" :class="payClass">{{payDesc}}</div>
 17         </div>
 18       </div>
 19       <div class="ball-contain">
 20         <div v-for="ball in balls">
 21           <transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop">
 22             <!--inner-hook用于内层元素设置横向动画时选择-->
 23             <div class="ball" v-show="ball.show">
 24               <div class="inner inner-hook"></div>
 25             </div>
 26           </transition>
 27         </div>
 28       </div>
 29       <transition name="fold">
 30         <div class="shopcart-list" v-show="listShow">
 31           <div class="list-header">
 32             <h1 class="title">购物车</h1>
 33             <span class="empty" @click="empty">清空</span>
 34           </div>
 35           <div class="list-content" ref="listContent">
 36             <ul>
 37               <li class="food border-1px" v-for="food in selectFoods">
 38                 <span class="name">{{food.name}}</span>
 39                 <div class="price">
 40                   <span>¥{{food.price * food.count}}</span>
 41                 </div>
 42                 <div class="cartcontrol-wrapper">
 43                   <cartcontrol :food="food" @add="addFood"></cartcontrol>
 44                 </div>
 45               </li>
 46             </ul>
 47           </div>
 48         </div>
 49       </transition>
 50     </div>
 51     <transition name="fade">
 52       <div class="list-mask" @click="hideList" v-show="listShow"></div>
 53     </transition>
 54   </div>
 55 </template>
 56 
 57 <style lang="stylus" rel="stylesheet/stylus">
 58   @import "../../common/stylus/mixin.styl"
 59 
 60   .shopcart
 61     position fixed
 62     left 0
 63     bottom 0
 64     /*多出的区块(购物车)要盖住上面的区块*/
 65     z-index 50
 66     width 100%
 67     height 48px
 68     .content
 69       display flex
 70       background #141d27
 71       /*消除空白间隙影响*/
 72       font-size 0
 73       .content-left
 74         flex 1
 75         color rgba(255, 255, 255, 0.4)
 76         .logo-wrapper
 77           display inline-block
 78           vertical-align top
 79           position relative
 80           top -10px
 81           margin 0 12px
 82           padding 6px
 83           /*此处IE盒模型,包括padding,border在内*/
 84           width 56px
 85           height 56px
 86           box-sizing border-box
 87           border-radius 50%
 88           background #141d27
 89           .logo
 90             width 100%
 91             height 100%
 92             border-radius 50%
 93             text-align center
 94             background #2b343c
 95             &.highlight
 96               background rgb(0, 160, 220)
 97             .icon-shopping_cart
 98               line-height 44px
 99               font-size 24px
100               color #80858a
101               &.highlight
102                 color #fff
103           .num
104             position absolute
105             top 0
106             right 0
107             /*绝对定位,要指定宽高,才能撑开*/
108             width 24px
109             heigt 16px
110             line-height 16px
111             text-align center
112             border-radius 16px
113             font-size 9px
114             font-weight 700
115             color #ffffff
116             background rgb(240, 20, 20)
117             box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.4)
118         .price
119           display inline-block
120           vertical-align top
121           margin-top 12px
122           padding-right 12px
123           line-height 24px
124           box-sizing border-box
125           border-right 1px solid rgba(255, 255, 255, 0.1)
126           font-size 16px
127           font-weight 700
128           &.highlight
129             color #fff
130         .desc
131           display inline-block
132           vertical-align top
133           margin 12px 0 0 12px
134           line-height 24px
135           font-size 10px
136       .content-right
137         flex 0 0 105px
138         width 105px
139         color rgba(255, 255, 255, 0.4)
140         .pay
141           height 48px
142           text-align center
143           line-height 48px
144           font-size 12px
145           font-weight 700
146           &.not-enough
147             background #2b333b
148           &.enough
149             background #00b43c
150             color #fff
151     .ball-contain
152       .ball
153         /*相对于视口进行变化*/
154         position fixed
155         left 32px
156         bottom 22px
157         z-index 200
158         &.drop-enter-active, &.drop-leave-active
159           transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
160         .inner
161           width: 16px
162           height: 16px
163           border-radius: 50%
164           background: rgb(0, 160, 220)
165           transition: all 0.4s linear
166     .shopcart-list
167       position absolute
168       top 0
169       left 0
170       /*因为这个列表是从底部穿出来的,要设置z-index属性*/
171       z-index -1
172       width 100%
173       transform: translate3d(0, -100%, 0)
174       &.fold-enter-active, &.fold-leave-active
175         transition: all 0.5s
176       &.fold-enter, &.fold-leave-active
177         transform translate3d(0, 0, 0)
178       .list-header
179         height 40px
180         line-height 40px
181         padding 0 18px
182         background #f3f5f7
183         border-bottom 1px solid rgba(7, 17, 27, 0.1)
184         .title
185           float left
186           font-size 14px
187           color rgb(7, 17, 27)
188         .empty
189           float right
190           font-size 14px
191           color rgb(0, 160, 220)
192       .list-content
193         padding 0 18px
194         max-height 217px
195         overflow hidden
196         background #fff
197         .food
198           position relative
199           padding 12px 0
200           box-sizing border-box
201           border-1px(rgba(7, 17, 27, 0.1))
202           .name
203             line-height 24px
204             font-size 14px
205             color rgb(7, 17, 27)
206           .price
207             position absolute
208             right 90px
209             bottom 12px
210             line-height 24px
211             font-size 14px
212             font-weight 700
213             color rgb(240, 20, 20)
214           .cartcontrol-wrapper
215             position absolute
216             right 0
217             /*因为cartcontrol组件中设置了加号按钮有6像素边距*/
218             bottom 6px
219 
220   .list-mask
221     position fixed
222     left 0
223     top 0
224     width 100%
225     height 100%
226     /*z-index的值要小于shopcart层的z-index的值,因为是背景*/
227     z-index 40
228     backdrop-filter blur(10px)
229     opacity 1
230     background rgba(7, 17, 27, 0.6)
231     &.fade-enter-active, &.fade-leave-active
232       transition: all 0.5s
233     &.fade-enter, &.fade-leave-active
234       opacity 0
235       background: rgba(7, 17, 27, 0)
236 </style>
布局设置及 CSS 设置

1)购物详情层是从底部穿出的,设置 z-index -1。

2)需要添加 cartcontrol 组件。引入注册并使用。(import,components,<cartcontrol>)

3)购物车详情页是不定高的,动画设置是可以设置 y 轴变化可以设置为 -100%,即相对于当前自身高度进行偏移。

4)cartcontrol 组件时绝对定位,需要设置 bottom 属性,因为 cartcontrol 组件中设置了加号按钮有 6 像素边距,所以这里 bottom 的值是 6px


2. 购物详情层控制(点击展开或收起)

 1 <div class="content" @click="toggleList">
 2 <div class="shopcart-list" v-show="listShow">
 3 
 4 data() {
 5         fold: true       // 默认折叠状态
 6       };
 7     },
 8 
 9 computed: {
10       listShow() {
11         if (!this.totalCount) {
12           this.fold = true;
13           return false;
14         }
15         let show = !this.fold;
16         if (show) {
17           this.$nextTick(() => {
18             if (!this.scroll) {
19               // 初始化
20               this.scroll = new BScroll(this.$refs.listContent, {
21                 click: true
22               });
23             } else {
24               this.scroll.refresh();
25             }
26           });
27         }
28         return show;
29       }
30     },
31 
32 methods: {
33     toggleList() {
34         if (!this.totalCount) {
35           // eslint-disable-next-line
36           return;
37         }
38         // 取反
39         this.fold = !this.fold;
40       }
41     },
购物车详情层

思路:购物详情层是需要可以隐藏的,添加 v-show 指令,绑定 listShow 变量。listShow 变量需要根据 totalcount 和 fold 属性进行计算:只有当totalCount>0 时,并且 fold 属性为 false,购物详情层才能展开。判断完 listShow 后,需要再给详情层添加点击事件,该事件改变 fold 属性。

实现:

第一步:给购物车详情层添加 v-show 指令。

第二步:在 data 中添加 fold 变量,并设置其默认值为 false。

第三步:在计算属性 computed 中进行 listShow 的计算。先判断 totalCount 是否存在,即是否大于 0,如果不大于 0 的话,设置 fold 的值为 true,并返回 false 值(即 listShow 的值为 false)。如果大于 0 的话,设置 show 的值为 fold 相反的值,并返回 show 的值(fold 为 true 时,表示折叠,那么此时 listShow 的值应该为 true,才能控制购物详情层隐藏)。

第四步:给购物车组件内容层(content)添加点击事件 toggleList。

第五步:在 methods 中定义 toggleList 方法。先判断 totalCount 是否存在,即是否大于 0,如果不大于 0 的话,则 return,逻辑不执行。如果大于 0 的话,就把 fold 值取反。

怎么验证:浏览器中点击购物车组件内容层,可以看到 .shopcart-list 的 display 属性有变化。

 

3. 实现 cartcontrol 按钮的点击功能

 1 import BScroll from 'better-scroll';
 2 
 3 computed: {
 4       listShow() {
 5         if (!this.totalCount) {
 6           this.fold = true;
 7           return false;
 8         }
 9         let show = !this.fold;
10         if (show) {
11           this.$nextTick(() => {
12             if (!this.scroll) {
13               // 初始化
14               this.scroll = new BScroll(this.$refs.listContent, {
15                 click: true
16               });
17             } else {
18               this.scroll.refresh();
19             }
20           });
21         }
22         return show;
23       }
24     },
点击功能

1)点击加号和减号小球,没有反应,为什么在这一层没办法实现点击功能?在 cartcontrol 中,可以看到 event._constructed,这是需要 better-scroll 派发事件,然后才可以点击。在这里刚好也需要使用到 better-scroll,因为这个列表之后也可能需要滚动。所以先把依赖的库 better-scroll 添加进来。

2)什么时候给列表做初始化?在 listShow 中,因为只有列表展示时才需要。所以在 listShow 中添加代码,先判断 show 存不存在(即列表展不展示),存在的话,在 $nextTick 接口中进行初始化设置(关于这个知识点,上述已经讲过,这是因为数据变化了,DOM 却没有立刻生效,而 better-scroll 是依赖于 DOM,所以需要在这个接口执行之后再写代码)。初始化需要把 listContent 传进来(用 ref 给其添加变量,通过 $refs 访问DOM元素),最后添加属性 click:true(派发点击事件)。到这里,初始化已经完成。

3)初始化完成了,还有一个问题:就是当 listShow 变化时,scroll 就会重新 new 一遍。为解决这个问题,可以判断是否有 this.scroll 的值,如果有,只需要调用 scroll 的 refresh 的接口,不需要重新实例化。这个方法会重新计算视口和内容的高度差,然后决定是否滚动。

 

4. 清空效果

1 <span class="empty" @click="empty">清空</span>
2 
3 methods: {
4       empty() {
5         this.selectFoods.forEach((food) => {
6           food.count = 0;
7         });
8       }
9     },
empty()

给清空按钮添加点击事件 empty,在 methods 中定义该方法。遍历数组 selectFoods,并将 food.count 的值设置为 0 即可。

 

5. 添加背景层

 1     <transition name="fade">
 2       <div class="list-mask" @click="hideList" v-show="listShow"></div>
 3     </transition>
 4 
 5 .list-mask
 6     position fixed
 7     left 0
 8     top 0
 9     width 100%
10     height 100%
11     /*z-index的值要小于shopcart层的z-index的值,因为是背景*/
12     z-index 40
13     backdrop-filter blur(10px)
14     opacity 1
15     background rgba(7, 17, 27, 0.6)
16     &.fade-enter-active, &.fade-leave-active
17       transition: all 0.5s
18     &.fade-enter, &.fade-leave-active
19       opacity 0
20       background: rgba(7, 17, 27, 0)
背景层代码

1)背景层绝对定位(fixed布局),需要与 shopcart 平级。

2)z-index 的值要小于 shopcart 层的 z-index 的值,因为是背景层

 

6. 添加 hideList 方法

1 <div class="list-mask" @click="hideList" v-show="listShow"></div>
2 
3 methods: {
4     hideList() {
5         this.fold = true;
6       }
7     },
hideList()

在购物车详情层之外的空白地方点击时,购物车详情车会隐藏 。在 methods 中定义该方法,将 fold 的值设置为 true即可。

 

7. 给结算按钮添加点击事件 pay

 1 <div class="content-right" @click.stop.prevent="pay">
 2 
 3 methods: {
 4       pay() {
 5         if (this.totalPrice < this.minPrice) {
 6           // eslint-disable-next-line
 7           return;
 8         }
 9         window.alert(`支付${this.totalPrice}元`);
10       }
11     },
pay()

在 methods 中定义该方法,先进行判断,如果总价小于最低起送价,则不执行逻辑。反之,弹出提示总价提示页面。

 

8. 阻止冒泡事件

<div class="content-right" @click.stop.prevent="pay">

当点击结算按钮,发现购物车详情层弹了出来,这是因为冒泡的关系。如何解决,添加 阻止冒泡,可以在点击事件中加上串联修饰符( @click.stop.prevent)。

 

总价:

1)报错:Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

意思是 vue2.0 只允许有一个根元素。在外层套上一个 div。

2)本项目除了 better-scroll 用了原生 DOM,其余基本就没有 DOM 方法,这就是使用 vue 的好处。

3) 修饰符

1 <!-- 停止冒泡 -->
2 <button @click.stop="doThis"></button>
3 <!-- 阻止默认行为 -->
4 <button @click.prevent="doThis"></button>
5 <!-- 阻止默认行为,没有表达式 -->
6 <form @submit.prevent></form>
7 <!--  串联修饰符 -->
8 <button @click.stop.prevent="doThis"></button>

 

至此,goods 组件已全部完成。

附代码:

  1 <template>
  2   <div>
  3     <div class="goods">
  4       <div class="menu-wrapper" ref="menuWrapper">
  5         <ul>
  6           <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}"
  7               @click="selectMenu(index,$event)">
  8             <span class="text border-1px">
  9               <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>{{item.name}}
 10             </span>
 11           </li>
 12         </ul>
 13       </div>
 14       <div class="foods-wrapper" ref="foodsWrapper">
 15         <ul>
 16           <li v-for="(item,index) in goods" class="food-list food-list-hook">
 17             <h1 class="title">{{item.name}}</h1>
 18             <ul>
 19               <li @click="selectFood(food,$event)" v-for="food in item.foods" class="food-item border-1px">
 20                 <div class="icon">
 21                   <img :src="food.icon" width="57" height="57">
 22                 </div>
 23                 <div class="content">
 24                   <h2 class="name">{{food.name}}</h2>
 25                   <p class="desc">{{food.description}}</p>
 26                   <div class="extra">
 27                     <span class="count">月售{{food.sellCount}}份</span><span>好评率{{food.rating}}%</span>
 28                   </div>
 29                   <div class="price">
 30                     <span class="now">¥{{food.price}}</span><span v-show="food.oldPrice"
 31                                                                   class="old">¥{{food.oldPrice}}</span>
 32                   </div>
 33                   <div class="cartcontrol-wrapper">
 34                     <cartcontrol @add="addFood" :food="food"></cartcontrol>
 35                   </div>
 36                 </div>
 37               </li>
 38             </ul>
 39           </li>
 40         </ul>
 41       </div>
 42       <!--最低起送费(minPrice)和配送费(deliveryPrice)-->
 43       <shopcart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice"
 44                 :min-price="seller.minPrice"></shopcart>
 45     </div>
 46     <!--商品详情层-->
 47     <food :food="selectedFood" ref="food"></food>
 48   </div>
 49 </template>
 50 
 51 <script type="text/ecmascript-6">
 52   import BScroll from 'better-scroll';
 53   import shopcart from '../../components/shopcart/shopcart';
 54   import cartcontrol from '../../components/cartcontrol/cartcontrol';
 55   import food from '../../components/food/food';
 56 
 57   const ERR_OK = 0;
 58 
 59   export default {
 60     props: {
 61       seller: {
 62         type: Object
 63       }
 64     },
 65     data() {
 66       return {
 67         goods: [],
 68         listHeight: [], // 存放大区间高度的数组
 69         scrollY: 0,   // 右侧实时变化的y值
 70         selectedFood: {}
 71       };
 72     },
 73     created() {
 74       this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
 75       this.$http.get('/api/goods').then((response) => {
 76         response = response.body;
 77         if (response.errno === ERR_OK) {
 78           this.goods = response.data;
 79 //          console.log(this.goods);
 80           this.$nextTick(() => {
 81             this._initScroll();
 82             this.calculateHeight();
 83           });
 84         }
 85       });
 86     },
 87     methods: {
 88       selectFood(food, event) {
 89         if (!event._constructed) {
 90           // eslint-disable-next-line
 91           return;
 92         }
 93         this.selectedFood = food;
 94         this.$refs.food.show();
 95       },
 96       selectMenu(index, event) {
 97 //        console.log(index);
 98 //        console.log(event);
 99 //        如果是浏览器原生点击事件,返回,不执行接下来的代码。(自定义派发的事件,constructed为true,而浏览器原生点击事件是没有这个属性的)
100         if (!event._constructed) {
101           // eslint-disable-next-line
102           return;
103         }
104         let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
105 //        找到点击对应的DOM元素
106         let el = foodList[index];
107         this.foodsScroll.scrollToElement(el, 300);
108       },
109       addFood(target) {
110         this._drop(target);
111       },
112       // drop方法(父组件的私有方法)调用子组件(shopcart)的drop方法
113       _drop(target) {
114         // 体验优化,异步执行下落动画
115         this.$nextTick(() => {
116           this.$refs.shopcart.drop(target);
117         });
118       },
119       _initScroll() {
120         this.menuScroll = new BScroll(this.$refs.menuWrapper, {
121 //          better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。
122           click: true
123         });
124         this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
125 //          click: true的设置是因为cartcontrol组件中需要添加点击事件
126           click: true,
127           probeType: 3
128         });
129 //        当滚动时,实时监测滚动位置并返回
130         this.foodsScroll.on('scroll', (pos) => {
131 //          pos.y是一个负值,abs:将负数转为正数,round:取整
132           this.scrollY = Math.abs(Math.round(pos.y));
133         });
134       },
135       calculateHeight() {
136 //        获取每个大区间的li(标题和内容)
137         let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
138         let height = 0;
139         this.listHeight.push(height);
140         for (let i = 0; i < foodList.length; i++) {
141           let item = foodList[i];
142           height += item.clientHeight;
143           this.listHeight.push(height);
144         }
145       }
146     },
147     computed: {
148 //      左侧索引
149       currentIndex() {
150         for (let i = 0; i < this.listHeight.length; i++) {
151 //          获取区间上下范围
152           let height1 = this.listHeight[i];
153           let height2 = this.listHeight[i + 1];
154 //          如果索引值在区间内或者是是最后一个值,返回当前索引值,如果没有listHeight.length,返回0。
155           if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
156             return i;
157           }
158         }
159         return 0;
160       },
161 //      返回被选中的商品(供shopcart组件使用)
162       selectFoods() {
163         let foods = [];
164 //        遍历商品分类
165         this.goods.forEach((good) => {
166 //          遍历分类下的商品
167           good.foods.forEach((food) => {
168 //            如果food.count存在,即大于0,证明商品被选中,将被选中商品放进数组里
169             if (food.count) {
170               foods.push(food);
171             }
172           });
173         });
174         return foods;
175       }
176     },
177     components: {
178       shopcart,
179       cartcontrol,
180       food
181     }
182   };
183 </script>
184 
185 <style lang="stylus" rel="stylesheet/stylus">
186   @import "../../common/stylus/mixin.styl"
187   .goods
188     display flex
189     position absolute
190     /*header:134px,tab:40px*/
191     top 174px
192     bottom 46px
193     width 100%
194     overflow hidden
195     .menu-wrapper
196       /*三个参数:等分,占位情况,缩放空间*/
197       flex 0 0 80px
198       width 80px
199       background #f3f5f7
200       .menu-item
201         display table
202         height 54px
203         width 56px
204         padding 0 12px
205         line-height 14px
206         &.current
207           position relative
208           /*要盖住border,要在最顶层*/
209           z-index 10
210           /*盖住上边的border*/
211           margin-top -1px
212           background #fff
213           .text
214             border-none()
215             font-weight 700
216         .icon
217           display inline-block
218           vertical-align top
219           width 12px
220           height 12px
221           margin-right 2px
222           background-size 12px 12px
223           background-repeat no-repeat
224           &.decrease
225             bg-image('decrease_3')
226           &.discount
227             bg-image('discount_3')
228           &.guarantee
229             bg-image('guarantee_3')
230           &.invoice
231             bg-image('invoice_3')
232           &.special
233             bg-image('special_3')
234         .text
235           display table-cell
236           width 56px
237           vertical-align middle
238           border-1px (rgba(7, 17, 27, 0.1))
239           font-size 12px
240     .foods-wrapper
241       flex 1
242       .title
243         padding-left 14px
244         height 26px
245         line-height 26px
246         border-left 1px solid #d9dde1
247         font-size 12px
248         color rgb(147, 153, 159)
249         background #f3f5f7
250       .food-item
251         display flex
252         margin 18px
253         padding-bottom 18px
254         border-1px(rgba(7, 17, 27, 0.1))
255         &:last-child
256           border-none()
257           margin-bottom 0
258         .icon
259           flex 0 0 57px
260           margin-right 10px
261         .content
262           flex 1
263           .name
264             margin 2px 0 8px
265             height 14px
266             line-height 14px
267             font-size 14px
268             color rgb(7, 17, 27)
269           .desc, .extra
270             font-size 10px
271             color rgb(147, 153, 159)
272           .desc
273             margin-bottom 8px
274             line-height 12px
275           .extra
276             line-height 10px
277             .count
278               margin-right 12px
279           .price
280             font-weight 700
281             line-height 24px
282             .now
283               margin-right 8px
284               font-size 14px
285               color rgb(240, 20, 20)
286             .old
287               text-decoration line-through
288               font-size 10px
289               color rgb(147, 153, 159)
290           .cartcontrol-wrapper
291             position absolute
292             right 0
293             bottom 12px
294 </style>
good.vue
 1 <template>
 2   <div class="cartcontrol">
 3     <transition name="move">
 4       <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
 5         <span class="inner icon-remove_circle_outline"></span>
 6       </div>
 7     </transition>
 8     <div class="cart-count" v-show="food.count>0">{{food.count}}</div>
 9     <div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div>
10   </div>
11 </template>
12 
13 <script type="text/ecmascript-6">
14   import Vue from 'vue';
15 
16   export default {
17     props: {
18       food: {
19         type: Object
20       }
21     },
22     created() {
23 //      console.log(this.food);
24     },
25     methods: {
26       addCart(event) {
27         if (!event._constructed) {
28           // eslint-disable-next-line
29           return;
30         }
31 //        console.log('click');
32         if (!this.food.count) {
33           Vue.set(this.food, 'count', 1);
34         } else {
35           this.food.count++;
36         }
37 //        将DOM对象作为事件的参数传递出去,用于计算小球的位置
38 //        this.$bus.emit('add', event.target);
39         this.$emit('add', event.target);
40       },
41       decreaseCart(event) {
42         if (!event._constructed) {
43           // eslint-disable-next-line
44           return;
45         }
46 //        console.log('click');
47         if (this.food.count) {
48           this.food.count--;
49         }
50       }
51     }
52   };
53 </script>
54 
55 <style lang="stylus" rel="stylesheet/stylus">
56   .cartcontrol
57     font-size 0
58     .cart-decrease
59       display inline-block
60       padding 6px
61       opacity: 1
62       transform: translate3d(0, 0, 0)
63       .inner
64         display inline-block
65         line-height 24px
66         font-size 24px
67         color rgb(0, 160, 220)
68         transition: all 0.4s linear
69         transform rotate(0)
70       &.move-enter-active, &.move-leave-active
71         transition: all 0.4s linear
72       &.move-enter, &.move-leave-active //刚进入和离开后的状态
73         opacity: 0
74         transform: translate3d(24px, 0, 0)
75         .inner
76           transform: rotate(180deg)
77     .cart-count
78       display inline-block
79       vertical-align top
80       width 12px
81       padding-top 6px
82       line-height 24px
83       text-align center
84       font-size 12px
85       color rgb(147, 153, 159)
86     .cart-add
87       display inline-block
88       padding 6px
89       line-height 24px
90       font-size 24px
91       color rgb(0, 160, 220)
92 </style>
cartcontrol.vue
  1 <template>
  2   <div>
  3     <div class="shopcart">
  4       <div class="content" @click="toggleList">
  5         <div class="content-left">
  6           <div class="logo-wrapper">
  7             <div class="logo" :class="{'highlight':totalCount>0}">
  8               <i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i>
  9             </div>
 10             <div class="num" v-show="totalCount>0">{{totalCount}}</div>
 11           </div>
 12           <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
 13           <div class="desc">另需配送费¥{{deliveryPrice}}元</div>
 14         </div>
 15         <div class="content-right" @click.stop.prevent="pay">
 16           <div class="pay" :class="payClass">{{payDesc}}</div>
 17         </div>
 18       </div>
 19       <div class="ball-contain">
 20         <div v-for="ball in balls">
 21           <transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop">
 22             <!--inner-hook用于内层元素设置横向动画时选择-->
 23             <div class="ball" v-show="ball.show">
 24               <div class="inner inner-hook"></div>
 25             </div>
 26           </transition>
 27         </div>
 28       </div>
 29       <transition name="fold">
 30         <div class="shopcart-list" v-show="listShow">
 31           <div class="list-header">
 32             <h1 class="title">购物车</h1>
 33             <span class="empty" @click="empty">清空</span>
 34           </div>
 35           <div class="list-content" ref="listContent">
 36             <ul>
 37               <li class="food border-1px" v-for="food in selectFoods">
 38                 <span class="name">{{food.name}}</span>
 39                 <div class="price">
 40                   <span>¥{{food.price * food.count}}</span>
 41                 </div>
 42                 <div class="cartcontrol-wrapper">
 43                   <cartcontrol :food="food" @add="addFood"></cartcontrol>
 44                 </div>
 45               </li>
 46             </ul>
 47           </div>
 48         </div>
 49       </transition>
 50     </div>
 51     <transition name="fade">
 52       <div class="list-mask" @click="hideList" v-show="listShow"></div>
 53     </transition>
 54   </div>
 55 </template>
 56 
 57 <script type="text/ecmascript-6">
 58   import BScroll from 'better-scroll';
 59   import cartcontrol from '../../components/cartcontrol/cartcontrol';
 60 
 61   export default {
 62     props: {
 63       selectFoods: {
 64         type: Array,
 65         default() {
 66           return [];
 67         }
 68       },
 69       deliveryPrice: {
 70         type: Number,
 71         default: 0
 72       },
 73       minPrice: {
 74         type: Number,
 75         default: 0
 76       }
 77     },
 78     data() {
 79       return {
 80         balls: [
 81           {
 82             show: false
 83           },
 84           {
 85             show: false
 86           },
 87           {
 88             show: false
 89           },
 90           {
 91             show: false
 92           },
 93           {
 94             show: false
 95           }
 96         ],
 97         dropBalls: [],   // 已经下落的小球
 98         fold: true       // 默认折叠状态
 99       };
100     },
101     computed: {
102       totalPrice() {
103         let total = 0;
104         this.selectFoods.forEach((food) => {
105           total += food.price * food.count;
106         });
107         return total;
108       },
109       totalCount() {
110         let count = 0;
111         this.selectFoods.forEach((food) => {
112           count += food.count;
113         });
114         return count;
115       },
116       payDesc() {
117         if (this.totalPrice === 0) {
118           return `¥${this.minPrice}元起送`;
119         } else if (this.totalPrice < this.minPrice) {
120           let diff = this.minPrice - this.totalPrice;
121           return `还差¥${diff}元起送`;
122         } else {
123           return '去结算';
124         }
125       },
126       payClass() {
127         if (this.totalPrice < this.minPrice) {
128           return 'not-enough';
129         } else {
130           return 'enough';
131         }
132       },
133       listShow() {
134         if (!this.totalCount) {
135           this.fold = true;
136           return false;
137         }
138         let show = !this.fold;
139         if (show) {
140           this.$nextTick(() => {
141             if (!this.scroll) {
142               // 初始化
143               this.scroll = new BScroll(this.$refs.listContent, {
144                 click: true
145               });
146             } else {
147               this.scroll.refresh();
148             }
149           });
150         }
151         return show;
152       }
153     },
154     methods: {
155 //      此方法在小球动画中似乎没有用到
156       addFood(target) {
157         this.drop(target);
158 //        console.log(target);
159       },
160       drop(el) {
161 //        console.log(el);
162         for (let i = 0; i < this.balls.length; i++) {
163           let ball = this.balls[i];
164           if (!ball.show) {
165             ball.show = true;   // 触发动画
166             ball.el = el;
167             this.dropBalls.push(ball);
168             // eslint-disable-next-line
169             return;
170           }
171         }
172       },
173       beforeDrop(el) {
174         // 遍历所有show为true的小球,给这些小球加上下落动画
175 //        console.log('beforeDrop' + el);
176         let count = this.balls.length;
177         while (count--) {
178           let ball = this.balls[count];
179           if (ball.show) {
180             // getBoundingClientRect:获取DOM元素相对于视口的位置,返回值的left和top就是相对于视口的偏移
181             let rect = ball.el.getBoundingClientRect();
182             let x = rect.left - 32;
183             // y值本身是负值,所以取反
184             let y = -(window.innerHeight - rect.top - 22);
185             // 因为v-show会将display设置为none,即不显示;这里设置为空,即显示
186             el.style.display = '';
187             // 兼容性设置
188             // 外层元素设置纵向动画
189             el.style.webkitTransform = `translate3d(0,${y}px,0)`;
190             el.style.transform = `translate3d(0,${y}px,0)`;
191             // 内层元素设置横向动画
192             let inner = el.getElementsByClassName('inner-hook')[0];
193 //            console.log('inner' + inner);
194             inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
195             inner.style.transform = `translate3d(${x}px,0,0)`;
196           }
197         }
198       },
199       // 下落动画完成,小球进入购物车的状态
200       dropping(el) {
201         // 需要先触发浏览器重绘,然后重置样式,单纯字符串,没有变量时,要用单引号而不是反引号
202         // re变量之后是不用的,所以要写一行eslint,让其跳过该检查
203         /* eslint-disable no-unused-vars */
204         let rf = el.offsetHeight;
205         this.$nextTick(() => {
206           el.style.webkitTransform = 'translate3d(0,0,0)';
207           el.style.transform = 'translate3d(0,0,0)';
208           let inner = el.getElementsByClassName('inner-hook')[0];
209           inner.style.webkitTransform = 'translate3d(0,0,0)';
210           inner.style.transform = 'translate3d(0,0,0)';
211         });
212       },
213       // 动画完成,将小球状态重置
214       afterDrop(el) {
215         // 将小球取出
216         let ball = this.dropBalls.shift();
217         if (ball) {
218           ball.show = false;
219           el.style.display = 'none';
220         }
221       },
222       toggleList() {
223         if (!this.totalCount) {
224           // eslint-disable-next-line
225           return;
226         }
227         // 取反
228         this.fold = !this.fold;
229       },
230       hideList() {
231         this.fold = true;
232       },
233       empty() {
234         this.selectFoods.forEach((food) => {
235           food.count = 0;
236         });
237       },
238       pay() {
239         if (this.totalPrice < this.minPrice) {
240           // eslint-disable-next-line
241           return;
242         }
243         window.alert(`支付${this.totalPrice}元`);
244       }
245     },
246     components: {
247       cartcontrol
248     }
249   };
250 </script>
251 
252 <style lang="stylus" rel="stylesheet/stylus">
253   @import "../../common/stylus/mixin.styl"
254 
255   .shopcart
256     position fixed
257     left 0
258     bottom 0
259     /*多出的区块(购物车)要盖住上面的区块*/
260     z-index 50
261     width 100%
262     height 48px
263     .content
264       display flex
265       background #141d27
266       /*消除空白间隙影响*/
267       font-size 0
268       .content-left
269         flex 1
270         color rgba(255, 255, 255, 0.4)
271         .logo-wrapper
272           display inline-block
273           vertical-align top
274           position relative
275           top -10px
276           margin 0 12px
277           padding 6px
278           /*此处IE盒模型,包括padding,border在内*/
279           width 56px
280           height 56px
281           box-sizing border-box
282           border-radius 50%
283           background #141d27
284           .logo
285             width 100%
286             height 100%
287             border-radius 50%
288             text-align center
289             background #2b343c
290             &.highlight
291               background rgb(0, 160, 220)
292             .icon-shopping_cart
293               line-height 44px
294               font-size 24px
295               color #80858a
296               &.highlight
297                 color #fff
298           .num
299             position absolute
300             top 0
301             right 0
302             /*绝对定位,要指定宽高,才能撑开*/
303             width 24px
304             heigt 16px
305             line-height 16px
306             text-align center
307             border-radius 16px
308             font-size 9px
309             font-weight 700
310             color #ffffff
311             background rgb(240, 20, 20)
312             box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.4)
313         .price
314           display inline-block
315           vertical-align top
316           margin-top 12px
317           padding-right 12px
318           line-height 24px
319           box-sizing border-box
320           border-right 1px solid rgba(255, 255, 255, 0.1)
321           font-size 16px
322           font-weight 700
323           &.highlight
324             color #fff
325         .desc
326           display inline-block
327           vertical-align top
328           margin 12px 0 0 12px
329           line-height 24px
330           font-size 10px
331       .content-right
332         flex 0 0 105px
333         width 105px
334         color rgba(255, 255, 255, 0.4)
335         .pay
336           height 48px
337           text-align center
338           line-height 48px
339           font-size 12px
340           font-weight 700
341           &.not-enough
342             background #2b333b
343           &.enough
344             background #00b43c
345             color #fff
346     .ball-contain
347       .ball
348         /*相对于视口进行变化*/
349         position fixed
350         left 32px
351         bottom 22px
352         z-index 200
353         &.drop-enter-active, &.drop-leave-active
354           transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
355         .inner
356           width: 16px
357           height: 16px
358           border-radius: 50%
359           background: rgb(0, 160, 220)
360           transition: all 0.4s linear
361     .shopcart-list
362       position absolute
363       top 0
364       left 0
365       /*因为这个列表是从底部穿出来的,要设置z-index属性*/
366       z-index -1
367       width 100%
368       transform: translate3d(0, -100%, 0)
369       &.fold-enter-active, &.fold-leave-active
370         transition: all 0.5s
371       &.fold-enter, &.fold-leave-active
372         transform translate3d(0, 0, 0)
373       .list-header
374         height 40px
375         line-height 40px
376         padding 0 18px
377         background #f3f5f7
378         border-bottom 1px solid rgba(7, 17, 27, 0.1)
379         .title
380           float left
381           font-size 14px
382           color rgb(7, 17, 27)
383         .empty
384           float right
385           font-size 14px
386           color rgb(0, 160, 220)
387       .list-content
388         padding 0 18px
389         max-height 217px
390         overflow hidden
391         background #fff
392         .food
393           position relative
394           padding 12px 0
395           box-sizing border-box
396           border-1px(rgba(7, 17, 27, 0.1))
397           .name
398             line-height 24px
399             font-size 14px
400             color rgb(7, 17, 27)
401           .price
402             position absolute
403             right 90px
404             bottom 12px
405             line-height 24px
406             font-size 14px
407             font-weight 700
408             color rgb(240, 20, 20)
409           .cartcontrol-wrapper
410             position absolute
411             right 0
412             /*因为cartcontrol组件中设置了加号按钮有6像素边距*/
413             bottom 6px
414 
415   .list-mask
416     position fixed
417     left 0
418     top 0
419     width 100%
420     height 100%
421     /*z-index的值要小于shopcart层的z-index的值,因为是背景*/
422     z-index 40
423     backdrop-filter blur(10px)
424     opacity 1
425     background rgba(7, 17, 27, 0.6)
426     &.fade-enter-active, &.fade-leave-active
427       transition: all 0.5s
428     &.fade-enter, &.fade-leave-active
429       opacity 0
430       background: rgba(7, 17, 27, 0)
431 </style>
shopcart.vue
posted @ 2018-02-26 16:09  Emily恩  阅读(1322)  评论(0编辑  收藏  举报