前言
本节分为四大块:
1. 商品信息(布局样式、第三方插件库better-scroll 的应用、split 组件)
2. 商品评价(ratingselect 组件)
3. 商品评价(评价列表)
PS:本节所有代码在文章底部。
商品信息
1. CSS 设置
1 <style lang="stylus" rel="stylesheet/stylus">
2 @import "../../common/stylus/mixin.styl"
3
4 .food
5 position fixed
6 left 0
7 top 0
8 bottom 48px
9 z-index 30
10 width 100%
11 background #fff
12 transform translate3d(0, 0, 0)
13 &.move-enter-active, &.move-leave-active
14 transition all 0.2s linear
15 &.move-enter, &.move-leave-active
16 transform translate3d(100%, 0, 0)
17 .image-header
18 position relative
19 width 100%
20 height 0
21 padding-top 100%
22 img
23 position absolute
24 left 0
25 top 0
26 width 100%
27 height 100%
28 .back
29 position absolute
30 left 0
31 top 10px
32 .icon-arrow_lift
33 display block
34 /*点击区域变大*/
35 padding 10px
36 font-size 20px
37 color #fff
38 .content
39 position relative
40 padding 18px
41 .title
42 margin-bottom 8px
43 line-height 14px
44 font-size 14px
45 font-weight 700
46 color rgb(7, 17, 27)
47 .detail
48 margin-bottom 18px
49 height 10px
50 line-height 10px
51 font-size 0
52 .sell-count, .rating
53 font-size 10px
54 color rgb(147, 153, 159)
55 .sell-count
56 margin-right 12px
57 .price
58 font-weight 700
59 line-height 24px
60 .now
61 margin-right 8px
62 font-size 14px
63 color rgb(240, 20, 20)
64 .old
65 text-decoration line-through
66 font-size 10px
67 color rgb(147, 153, 159)
68 .cartcontrol-wrapper
69 position absolute
70 right 12px
71 bottom 12px
72 .buy
73 position absolute
74 right 18px
75 bottom 18px
76 /*因为要盖住cartcontrol组件*/
77 z-index 10
78 height 24px
79 line-height 24px
80 padding 0 12px
81 box-sizing border-box
82 border-radius 12px
83 font-size 10px
84 color #fff
85 background-color rgb(0, 160, 220)
86 opacity 1
87 &.fade-enter-active, &.fade-leave-active
88 transition all 0.2s linear
89 &.fade-enter, &.fade-leave-active
90 opacity 0
91 z-index -1
92 .info
93 padding: 18px
94 .title
95 line-height: 14px
96 margin-bottom: 6px
97 font-size: 14px
98 color: rgb(7, 17, 27)
99 .text
100 line-height: 24px
101 padding: 0 8px
102 font-size: 12px
103 color: rgb(77, 85, 93)
104 .rating
105 padding-top: 18px
106 .title
107 line-height: 14px
108 margin-left: 18px
109 font-size: 14px
110 color: rgb(7, 17, 27)
111 .rating-wrapper
112 padding 0 18px
113 .rating-item
114 position relative
115 padding 16px 0
116 border-1px(rgba(7, 17, 27, 0.1))
117 .user
118 position absolute
119 top 16px
120 right 0
121 line-height 12px
122 font-size 0
123 .username
124 margin-right 6px
125 display inline-block
126 vertical-align top
127 font-size 10px
128 color rgb(147, 153, 159)
129 .avatar
130 border-radius 50%
131 .time
132 margin-bottom 6px
133 line-height 12px
134 font-size 10px
135 color rgb(147, 153, 159)
136 .text
137 line-height 16px
138 font-size 12px
139 color rgb(7, 17, 27)
140 .icon-thumb_up, .icon-thumb_down
141 margin-right 4px
142 line-height 16px
143 font-size 12px
144 .icon-thumb_up
145 color rgb(0, 160, 220)
146 .icon-thumb_down
147 color rgb(147, 153, 159)
148 .no-rating
149 padding 16px 0
150 font-size 12px
151 color rgb(147, 153, 159)
152 </style>
CSS 设置
1)相对于屏幕进行定位,使用 fixed 布局。
2)底部有购物车,所以设置 bottom 为 48px。
3)z-index 的值应该小于购物车详情层的 z-index,因为购物车详情层弹出应该要遮盖住商品详情层。
4)加上 transition 动画
5)图片高度应该和屏幕宽度一样,是动态变化的。图片加载是异步过程,如果不设置图片高度,等到图片加载完毕,页面高度会突然被撑开。如何解决?先把宽高设好,设置 height 0,padding-top 100%。这是 W3C 的一个特定写法,当 padding 的 top 或 bottom 设置为100% 时,会相对于盒子的 width 100%,这就相当于有一个宽高相等的盒子。(同理,当 padding 的 left 或 right 设置为100% 时,会相对于盒子的 height 100%)
2. 数据获取
1 food.vue文件
2 props: {
3 food: {
4 type: Object
5 }
6 },
7
8
9 goods.vue文件
10 <food :food="selectedFood" ref="food"></food>
11
12 import food from '../../components/food/food';
13 data() {
14 return {
15 selectedFood: {}
16 };
17 },
18 methods: {
19 selectFood(food, event) {
20 if (!event._constructed) {
21 // eslint-disable-next-line
22 return;
23 }
24 this.selectedFood = food;
25 },
26 }
数据获取
1)商品详情页关联的是商品本身,也就是食品分类下的每个商品。使用 props 接收 food 对象。
2)在 goods 组件中引用 food 组件,并且传入food,设定变量为 selectedFood(即选中的 food),在 data 中定义该变量,为一个空对象。
3)selectedFood 是选中的 food,那么什么时候选中?当点击 food 时。如何实现:在 li 中添加点击事件 selectFood(food,$event),传入参数 food,因为这里是 foodWrapper,所以点击的时候也要拿到 event,接着在在 methods 中实现该方法。先 return 掉浏览器默认的点击事件,将参数 food 传递给变量 selectedFood。如何检验:在浏览器调试窗口中找到 food 层,取消display属性,看看布局有没有错。
3. show 方法的实现:点击商品时,商品详情层展开
1 food.vue文件
2 <div class="food" v-show="showFlag" ref="food">
3
4 data() {
5 return {
6 showFlag: false
7 }
8 };
9 },
10 methods: {
11 show() {
12 this.showFlag = true;
13 },
14 }
15
16 goods.vue文件
17 methods: {
18 selectFood(food, event) {
19 this.$refs.food.show();
20 }
21 }
show方法
在 food 组件中定义 show 方法,show 方法通过改变 showFlag 的值来实现商品详情层的展开功能,在此之前,需要在 data 中先定义 showFlag。然后在 goods 组件中通过 ref 获取组件,并通过 $refs 调用子组件的 show 方法。
PS:1)父组件可以调用子组件方法,子组件不能调用父组件方法。2)设计规范:下划线开头是私有方法(_drop)。
4. 给返回按钮添加点击事件 hide
1 <div class="back" @click="hide">
2
3 methods: {
4 hide() {
5 this.showFlag = false;
6 }
7 }
hide方法
1)CSS设置:(display block,padding 10px)先设置display,才能设置padding,使点击区域变大。
2)hide 方法通过改变 showFlag 的值为 false 来实现返回功能。
设置其他样式
5. better-scroll 的使用
1 <div class="food" v-show="showFlag" ref="food">
2 <div class="food-content">
3 </div>
4 </div>
5
6 methods: {
7 show() {
8 this.$nextTick(() => {
9 if (!this.scroll) {
10 this.scroll = new BScroll(this.$refs.food, {
11 click: true
12 });
13 } else {
14 this.scroll.refresh();
15 }
16 });
17 }
18 }
better-scroll 的使用
原理:当内部(content)的内容高度大于视口高度,则产生滚动效果
固定框架:外层是一个 wrapper,里面有一个 content,并且这个 content 高度是由其中的内容撑开的。
使用:better-scroll 应该绑定在最外层(.food)。依旧是通过 ref 获取组件,并通过 $refs 调用组件。better-scroll 应该在商品详情层展开时就进行初始化,所以其初始化应该在 show 方法中。
6. 购物车模块
1 food.vue文件:
2 <div class="cartcontrol-wrapper">
3 <cartcontrol :food="food"></cartcontrol>
4 </div>
5 <transition name="fade">
6 <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">加入购物车</div>
7 </transition>
8
9 import Vue from 'vue';
10 methods: {
11 addFirst(event) {
12 console.log('click');
13 if (!event._constructed) {
14 // eslint-disable-next-line
15 return;
16 }
17 this.$emit('add', event.target);
18 console.log(event.target);
19 Vue.set(this.food, 'count', 1);
20 }
21 }
22
23 cartcontrol.vue文件:
24 <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
25 <div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div>
购物车模块
有两种样式:当加入商品时,“加入购物车” 文字消失,显示 cartcontrol 组件。所以应该加入两个层,这两个层平级,分别是 buy 层和 cartcontrol 层。
1)这两个层都是绝对定位。
2)buy 层加入 transition 动画及 v-show 指令(当 food.count 不存在或等于 0 时,该层显示)。
3)buy 层进行样式设计:z-index 10,因为要盖住 cartcontrol 组件。
4)添加点击按钮事件 addFirst。首先阻止浏览器默认点击事件,然后引入 vue,利用 Vue.set,将 this.count 置为1,最后使用 $emit
将事件事件传出去。
PS:1)不传参数的话,则默认参数为event。2)不传参数得写为addFirst,而不能写成addFirst()。
Q1:在 buy 层点击时,发现小球位下落置不正确。
A1:因为点击后,count>0,此时 buy 层因为 v-show 指令,会被隐藏,display 瞬间变成 none。所以在 $nextTick 中做动画时就找不到小球初始位置。
R1:将 buy 层的消失做成一个动画(transition,渐隐效果),这样 buy 层就不会立刻隐藏,display 属性不会立刻被设置 none,位置就能被计算。
Q2:点击商品页(goods 组件)的小球,商品详情页会弹出。
A2:这是因为事件冒泡,所以要阻止事件冒泡。
R2:给 cartcontro l组件的加号和减号小球的点击事件添加修饰符(@click.stop.prevent)。
6. 新建组件(components -> split ->split.vue),在 food.vue 引入注册并使用。
1 <template>
2 <div class="split"></div>
3 </template>
4
5 <script type="text/ecmascript-6">
6 export default {};
7 </script>
8
9 <style lang="stylus" rel="stylesheet/stylus">
10 .split
11 width: 100%
12 height: 16px
13 border-top: 1px solid rgba(7, 17, 27, 0.1)
14 border-bottom: 1px solid rgba(7, 17, 27, 0.1)
15 background: #f3f5f7
16 </style>
split.vue
7. food.info 不是每个数据都有,所以 food.info 要加 v-show 指令。
商品评价(ratingselect 组件)
1. 新建组件(components -> ratingselect->ratingselect.vue)。
2. ratingselect 组件需要在 props 中先接收四个参数:一个变量控制评价数组(ratings),一个变量维护描述(desc),一个变量确定是否只看内容(onlyContent),一个变量维护选择的评价类型(selectType)。
1 const POSITIVE = 0; // 正面评价
2 const NEGATIVE = 1; // 负面评价
3 const ALL = 2; // 所有评价
4
5 props: {
6 // 评价数组
7 ratings: {
8 type: Array,
9 default() {
10 return [];
11 }
12 },
13 // 选择评论类型
14 selectType: {
15 type: Number,
16 default: ALL
17 },
18 // 只看有内容评论
19 onlyContent: {
20 type: Boolean,
21 default: false
22 },
23 // 评论描述
24 desc: {
25 type: Object,
26 default() {
27 return {
28 all: '全部',
29 positive: '满意',
30 negative: '不满意'
31 };
32 }
33 }
34 },
props接收的变量
评价类型一般分为所有评价,正面评价和负面评价,将这三个定义为常量。评价类型默认选所有评价。
3.在 food.vue 引入注册并使用 ratingselect 组件,完成布局及CSS设置。
1 <div class="rating">
2 <h1 class="title">商品评价</h1>
3 <ratingselect :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings" @select="selectRating" @toggle="toggleContent"></ratingselect>
4 </div>
5
6 .rating
7 padding-top: 18px
8 .title
9 line-height: 14px
10 margin-left: 18px
11 font-size: 14px
12 color: rgb(7, 17, 27)
13
food.vue
1 <template>
2 <div class="ratingselect">
3 <div class="rating-type border-1px">
4 <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span
5 class="count">{{ratings.length}}</span></span>
6 <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span
7 class="count">{{positives.length}}</span></span>
8 <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span
9 class="count">{{negatives.length}}</span></span>
10 </div>
11 <div @click="toggleContent" class="switch" :class="{'on':onlyContent}">
12 <span class="icon-check_circle"></span>
13 <span class="text">只看有内容的评价</span>
14 </div>
15 </div>
16 </template>
17
18 <style lang="stylus" rel="stylesheet/stylus">
19 @import "../../common/stylus/mixin.styl"
20
21 .ratingselect
22 .rating-type
23 padding: 18px 0
24 margin: 0 18px
25 border-1px(rgba(7, 17, 27, 0.1))
26 /* 消除空白字符影响 */
27 font-size: 0
28 .block
29 display: inline-block
30 padding: 8px 12px
31 margin-right: 8px
32 line-height: 16px
33 border-radius: 1px
34 font-size: 12px
35 color: rgb(77, 85, 93)
36 &.active
37 color #fff
38 .count
39 margin-left: 2px
40 font-size: 8px
41 &.positive
42 background: rgba(0, 160, 220, 0.2)
43 &.active
44 background: rgb(0, 160, 220)
45 &.negative
46 background: rgba(77, 85, 93, 0.2)
47 &.active
48 background: rgb(77, 85, 93)
49 .switch
50 padding: 12px 18px
51 line-height: 24px
52 border-bottom: 1px solid rgba(7, 17, 27, 0.1)
53 color: rgb(147, 153, 159)
54 font-size: 0
55 &.on
56 .icon-check_circle
57 color: #00c850
58 .icon-check_circle
59 /* 设置对齐方式 */
60 display: inline-block
61 vertical-align: top
62 margin-right: 4px
63 font-size: 24px
64 .text
65 display: inline-block
66 vertical-align: top
67 font-size: 12px
68 </style>
ratingselect.vue
1)在 food.vue 文件中:.rating 为什么不设置全padding,而设置padding-top?因为下边有一条边框是贯穿整个界面的。
2)在 ratingselect.vue 文件中:.rating-type 为什么设置 margin 而不是padding?因为下面有一条线,写 padding 会影响到线的位置。
4. 通过父组件传入数据
1 <ratingselect :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings"
2 @select="selectRating" @toggle="toggleContent"></ratingselect>
3
4 const ALL = 2;
5
6 export default {
7 data() {
8 return {
9 showFlag: false,
10 selectType: ALL,
11 onlyContent: true,
12 desc: {
13 all: '全部',
14 positive: '推荐',
15 negative: '吐槽'
16 }
17 };
18 },
19 methods: {
20 show() {
21 this.selectType = ALL;
22 this.onlyContent = false;
23 }
24 }
传入数据
首先在 data 中定义相关数据,然后在 show 中初始化这些数据。为什么要初始化?因为 ratingselect 组件被多个商品使用,当传入不同商品时,希望这些属性的状态都是初始化的状态。最后在父组件中传入相关数据。注意:ratings 是 food.ratings。
5. 添加高亮设置
1 <template>
2 <div class="ratingselect">
3 <div class="rating-type border-1px">
4 <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span
5 class="count">{{ratings.length}}</span></span>
6 <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span
7 class="count">{{positives.length}}</span></span>
8 <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span
9 class="count">{{negatives.length}}</span></span>
10 </div>
11 <div @click="toggleContent" class="switch" :class="{'on':onlyContent}">
12 <span class="icon-check_circle"></span>
13 <span class="text">只看有内容的评价</span>
14 </div>
15 </div>
16 </template>
17
18
19 .rating-type
20 .block
21 display: inline-block
22 padding: 8px 12px
23 margin-right: 8px
24 line-height: 16px
25 border-radius: 1px
26 font-size: 12px
27 color: rgb(77, 85, 93)
28 &.active
29 color #fff
30 .count
31 margin-left: 2px
32 font-size: 8px
33 &.positive
34 background: rgba(0, 160, 220, 0.2)
35 &.active
36 background: rgb(0, 160, 220)
37 &.negative
38 background: rgba(77, 85, 93, 0.2)
39 &.active
40 background: rgb(77, 85, 93)
41 .switch
42 padding: 12px 18px
43 line-height: 24px
44 border-bottom: 1px solid rgba(7, 17, 27, 0.1)
45 color: rgb(147, 153, 159)
46 font-size: 0
47 &.on
48 .icon-check_circle
49 color: #00c850
高亮设置
1)选择类型
从图中可以看到,每个区块都会有一个高亮设置,可以给这些设置加上类名 active,通过判断:比如“全部”按钮,当 selectType===2 时(之前设置过常量:ALL = 2),应用高亮设置。
如何验证:在 food 组件的 show 方法中,在属性的初始化设置时,设 this.selectType = 1 或 0,然后到浏览器中查看效果。
2)“只看有内容的评价”区块
动态绑定 on 样式,只有当 onlyContent 值为 true 时应用样式。
如何验证?在 food 组件的 show 方法中,在属性的初始化设置时,设 this.onlyContent = false,然后到浏览器中查看效果。
6. 添加点击事件
1 <div class="rating-type border-1px">
2 <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span
3 class="count">{{ratings.length}}</span></span>
4 <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span
5 class="count">{{positives.length}}</span></span>
6 <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span
7 class="count">{{negatives.length}}</span></span>
8 </div>
9 <div @click="toggleContent" class="switch" :class="{'on':onlyContent}"></div>
10
11
12 methods: {
13 select(type, event) {
14 if (!event._constructed) {
15 // eslint-disable-next-line
16 return;
17 }
18 this.$emit('select', type);
19 },
20 toggleContent(event) {
21 if (!event._constructed) {
22 // eslint-disable-next-line
23 return;
24 }
25 this.$emit('toggle', event);
26 }
27 },
ratingselect.vue
1 <ratingselect :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings" @select="selectRating" @toggle="toggleContent"></ratingselect>
2
3 methods: {
4 selectRating(type) {
5 this.selectType = type;
6 this.$nextTick(() => {
7 this.scroll.refresh();
8 });
9 },
10 toggleContent() {
11 this.onlyContent = !this.onlyContent;
12 this.$nextTick(() => {
13 this.scroll.refresh();
14 });
15 }
16 }
food.vue
1)给选择类型按钮增加 click 事件 select。
该方法需要传入参数 selectType 和 event。为什么需要event?因为点击该区块,区块外层有 better-scroll,实现点击功能。这里同样是以 $emit 方法将数据传出去。父组件通过 @select 方法来改变 selectType 的值。
2)给 switch(“只看有内容的评价”区块) 增加 click 事件 toggleContent。
这里同样是以 $emit 方法将数据传出去。父组件通过 @toggle 方法来改变 onlyContent 的值。
PS:
1. props双向绑定变成单向
2. vue报错:Module build failed: ParseError: unexpected "indent"
这里是因为 css 代码中注释应该是 /* 消除空白字符影响 */,而写成 // 消除空白字符影响(这是自动补全功能自己添加的注释符号,很奇怪),具体原因有待细究。
7. 评价数量
1 <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}
2 <span class="count">{{ratings.length}}</span>
3 </span>
4 <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}
5 <span class="count">{{positives.length}}</span>
6 </span>
7 <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}
8 <span class="count">{{negatives.length}}</span>
9 </span>
10
11 computed: {
12 // 正面评价数组
13 positives() {
14 return this.ratings.filter((rating) => {
15 return rating.rateType === POSITIVE;
16 });
17 },
18 // 负面评价数组
19 negatives() {
20 return this.ratings.filter((rating) => {
21 return rating.rateType === NEGATIVE;
22 });
23 }
24 }
评价数量
全部评价:ratings.length
正面评价:定义正面评价数组 positives,其值应该可以通过 ratings 的值计算得出,使用 filter 方法对数组进行过滤,返回数组元素中 rateType 值为 0 的数组元素(就是正向评价)。
负面评价:与上同理。
商品评价(评价列表)
评价列表有两种样式:一种是像左边:有评价,一种是像右边:没评价。
1. 布局及CSS设置
1)图标字体:通过 :class 动态绑定相应的类名,这里因为是一个数组,所以可以同时定义两个 key 及其对应的 value 值。
<span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span>
2)右侧是绝对定位,定位到右上角。
PS:在浏览器中检查样式设置时,会发现即便不刷新网页也可以正常显示,这是因为 reload 方法会重新加载。但一般还是刷新一下比较好,因为如果过程中高度撑高(内层 content 的高度是靠里面的内容撑开的),better-scroll高度计算可能不正确,因为不会重新执行 refresh。
2. 点击按钮与列表绑定
通过点击按钮,过滤列表,判断哪些显示哪些不显示。
1)在遍历时添加 v-show 指令,该指令可以绑定一个函数 needShow(rating.rateType,rating.text),最终获得函数计算后的返回值。
2)定义函数:如果选中了“只看有内容的评论(onlyContent)”,并且这条评论的内容(text)为空,则返回 false。否则执行接下来的逻辑:此时 onlyContent 为false 或者 text 不为空,这个情况下纠结着判断selectType类型,如果 selectType === ALL,则返回 true,否则的话,判断当前的 rateType 是不是等于选择类型(rateType),是的话返回 true,不是的话返回 false。
3)给 selectRating 和 toggleContent 方法加上 refresh 方法,即点击按钮选择后重新计算高度,防止列表回弹。因为改变数据时,DOM更新是异步的,所以将 refresh 方法写在 $nextTick 接口中。
PS:v-show绑定对象、属性、字段、函数。
3. 时间戳的转换
需要的格式:2016-07-23 21:52
data.json中的格式:"rateTime": 1469281964000,
可以看到,在实际项目中需要现实的时间格式和数据给的时间格式是不一样的,数据中给的是时间戳,必须转换成时间字符串。
1)使用 vue 的 filters 方法,其语法就是在这之后加上 “ | ”,后面在跟上 filters 的名字 formatDate。
<div class="time">{{rating.rateTime | formatDate}}</div>
2)定义组件级别的 filters,filters 中有一个 formatDate(),formatDate() 接收一个参数,这个参数就是时间戳。
首先把时间戳转换成 Date 类型的对象,存放在变量 date 中。然后写一个名为 formatDate() 的函数,该函数是为了将 date 转换成所需要的年月日时间的形式。给该函数传两个参数,一个是 date,另外一个是格式化的字符串 'yyyy-MM-dd hh:mm'。该方法是通用方法,可以在 common -> js 中单独定义一个 js 文件去实现。
3)使用 js 模块实现函数 formatDate(),在 js 中创建 date.js。
第一步:在 date.js 文件中 export 方法 formatDate(),然后在 food.vue 中调用该模块。
第二步:实现 formatDate() 方法。
date输入是时间类型,输出是字符串。输入除了string类型还有一个格式化的字符串(就是两个参数),输出就是把字符串转成需要的模式。
可以利用正则表达式来写。传入的参数是yyyy-MM-dd hh:mm,可以利用正则表达式遍历这个字符串,将符合规则的表达式替换需要的年月日形式。
除了年是4个,其余都是2个。这里就是将yyyy替换成为真实的数据。
先替换年:定义规则:匹配到以y开头一个或多个的字符串。在字符串中查找匹配前面定义的规则(.test(fmt))。
举个例子:这里传入的fmt="yyyy-MM-dd hh:mm",在正则表达式中:+ 代表匹配前面的子表达式一次或多次。/(y+)/代表的规则是找到以y开头的字符串。所以这里找到的是yyyy。
匹配到相应的字符串后,找到相应数据(比如:2016),然后替换掉符合规则的字符串,即最终替换结果为(2016-MM-dd hh:mm)。
如何实现:字符串有一个replace方法,第一个参数是被替换的字符串,第二个参数是替换的字符串。
RegExp.$1 代表的是找到的符合规则的字符串(即yyyy),getFullYear() 方法可返回一个表示年份的 4 位数字(假设这里是2016)。要通过substr将其转换成 4 位的字符串。
substr(start,length) 方法可在字符串中抽取从 start 下标开始的指定数目的字符。length参数省略的话,则返回从开始位置到结尾的字串。
substr(4 - RegExp.$1.length):
RegExp.$1.length的值是4,则4 - RegExp.$1.length为0,表示从getFullYear()返回的数字中抽取第一个位置到最后一个位置的字串。也就是说,从返回的数字2016中抽取第一个位置到最后一个位置的字串,结果就是2016,不过类型由数字变成字符串,刚好符合replace方法对参数类型的限制。
再举个例子:假设fmt="yy-MM-dd hh:mm",其余不变,则最后从2016抽取第三个位置到最后一个位置的字串,就是16
除了年份比较特殊,其余的都一样,可以先定义一个对象o,这个对象是一个正则表达式,对应所替换的内容。
其中月份比较特殊,getMonth()方法返回的值是0到11,为了取到正确的值,要加1。
定义完表达式对象,接下来遍历该对象。
要先通过RegExp方法去构造一个正则表达式,依旧使用反引号和花括号结合的方式传入变量k,变量k的值是M+、d+这一类,构造完正则表达式,遍历字符串fmt,看看有没有符合规则的,有的话,替换真实数据。
PS:一个是定义死了正则表达式(/(y+)/),一个动态构造正则表达式(RegExp(`(${k})`))
先定义要替换的值,末尾的这一步:+ '',是为了将数字转换成字符串。
替换这里需要注意,不能直接替换掉符合正则表达式规则的字符串,因为:比如月份中1到9月是单个数字,如果匹配到的字符串是M,就没问题,如果匹配到的字符串是MM,要先在9前面加上一个0。所以要判断匹配到的字符串的长度,如果是1,直接替换,否则补0。
定义一个补0的方法padLeftZero,如果str为9,补全之后为009,str长度为1,则取得字串是从抽取第二个位置到最后一个位置的字串,即09;
再举一例:如果str为12,补全之后为0012,str长度为2,则取得字串是从抽取第二个位置到最后一个位置的字串,即12;
PS:
1. 用毫秒时间戳初始化日期对象
var timestamp=new Date().getTime(); console.log( new Date(timestamp) ); //Tue Jun 06 2017 11:06:59 GMT+0800 (中国标准时间)
var date3 = new Date( timestamp - 1 * 60 * 60 * 1000); console.log(date3); // Tue Jun 06 2017 10:06:59 GMT+0800 (中国标准时间)
说明:时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。时间戳唯一地标识某一刻的时间。
2. 这就是 js 模块化编程思想。将一个通用的方法抽象成模块,要使用的时候引入即可。
3. 两种引入方式的区别:
import {formatDate} from '../../common/js/date';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
cartcontrol 组件时是通过 export default 导出的,而模块是通过 export function 导出的(不是默认)。也就是说这个模块其实可以同时 export 多个 function,也可以 import 多个模块:{formatDate,a}。
实现{formatDate}方法:
date输入是时间类型,输出是字符串。输入除了string类型还有一个格式化的字符串(就是两个参数),输出就是把字符串转成需要的模式。
可以利用正则表达式来写。传入的参数是yyyy-MM-dd hh:mm,可以利用正则表达式遍历这个字符串,将符合规则的表达式替换需要的年月日形式。
除了年是4个,其余都是2个。这里就是将yyyy替换成为真实的数据。
先替换年:定义规则:匹配到以y开头一个或多个的字符串。在字符串中查找匹配前面定义的规则(.test(fmt))。
举个例子:这里传入的fmt="yyyy-MM-dd hh:mm",在正则表达式中:+ 代表匹配前面的子表达式一次或多次。/(y+)/代表的规则是找到以y开头的字符串。所以这里找到的是yyyy。
匹配到相应的字符串后,找到相应数据(比如:2016),然后替换掉符合规则的字符串,即最终替换结果为(2016-MM-dd hh:mm)。
如何实现:字符串有一个replace方法,第一个参数是被替换的字符串,第二个参数是替换的字符串。
RegExp.$1 代表的是找到的符合规则的字符串(即yyyy),getFullYear() 方法可返回一个表示年份的 4 位数字(假设这里是2016)。要通过substr将其转换成 4 位的字符串。
substr(start,length) 方法可在字符串中抽取从 start 下标开始的指定数目的字符。length参数省略的话,则返回从开始位置到结尾的字串。
substr(4 - RegExp.$1.length):
RegExp.$1.length的值是4,则4 - RegExp.$1.length为0,表示从getFullYear()返回的数字中抽取第一个位置到最后一个位置的字串。也就是说,从返回的数字2016中抽取第一个位置到最后一个位置的字串,结果就是2016,不过类型由数字变成字符串,刚好符合replace方法对参数类型的限制。
再举个例子:假设fmt="yy-MM-dd hh:mm",其余不变,则最后从2016抽取第三个位置到最后一个位置的字串,就是16
除了年份比较特殊,其余的都一样,可以先定义一个对象o,这个对象是一个正则表达式,对应所替换的内容。
其中月份比较特殊,getMonth()方法返回的值是0到11,为了取到正确的值,要加1。
定义完表达式对象,接下来遍历该对象。
要先通过RegExp方法去构造一个正则表达式,依旧使用反引号和花括号结合的方式传入变量k,变量k的值是M+、d+这一类,构造完正则表达式,遍历字符串fmt,看看有没有符合规则的,有的话,替换真实数据。
PS:一个是定义死了正则表达式(/(y+)/),一个动态构造正则表达式(RegExp(`(${k})`))
先定义要替换的值,末尾的这一步:+ '',是为了将数字转换成字符串。
替换这里需要注意,不能直接替换掉符合正则表达式规则的字符串,因为:比如月份中1到9月是单个数字,如果匹配到的字符串是M,就没问题,如果匹配到的字符串是MM,要先在9前面加上一个0。所以要判断匹配到的字符串的长度,如果是1,直接替换,否则补0。
定义一个补0的方法padLeftZero,如果str为9,补全之后为009,str长度为1,则取得字串是从抽取第二个位置到最后一个位置的字串,即09;
再举一例:如果str为12,补全之后为0012,str长度为2,则取得字串是从抽取第二个位置到最后一个位置的字串,即12;
至此,food 组件已全部完成。
附代码:
1 <template>
2 <transition name="move">
3 <div class="food" v-show="showFlag" ref="food">
4 <div class="food-content">
5 <div class="image-header">
6 <img :src="food.image">
7 <div class="back" @click="hide">
8 <i class="icon-arrow_lift"></i>
9 </div>
10 </div>
11 <div class="content">
12 <h1 class="title">{{food.name}}</h1>
13 <div class="detail">
14 <span class="sell-count">月售{{food.sellCount}}份</span>
15 <span class="rating">好评率{{food.rating}}%</span>
16 </div>
17 <div class="price">
18 <span class="now">¥{{food.price}}</span><span v-show="food.oldPrice" class="old">¥{{food.oldPrice}}</span>
19 </div>
20 <div class="cartcontrol-wrapper">
21 <cartcontrol :food="food"></cartcontrol>
22 </div>
23 <transition name="fade">
24 <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">加入购物车</div>
25 </transition>
26 </div>
27 <split></split>
28 <div class="info" v-show="food.info">
29 <h1 class="title">商品信息</h1>
30 <p class="text">{{food.info}}</p>
31 </div>
32 <split></split>
33 <div class="rating">
34 <h1 class="title">商品评价</h1>
35 <ratingselect :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings"
36 @select="selectRating" @toggle="toggleContent"></ratingselect>
37 <!--评价列表-->
38 <div class="rating-wrapper">
39 <!--有列表且列表不为空-->
40 <ul v-show="food.ratings && food.ratings.length">
41 <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings"
42 class="rating-item border-1px">
43 <div class="user">
44 <span class="username">{{rating.username}}</span>
45 <img class="avatar" :src="rating.avatar" width="12" height="12">
46 </div>
47 <div class="time">{{rating.rateTime | formatDate}}</div>
48 <p class="text">
49 <span
50 :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span>{{rating.text}}
51 </p>
52 </li>
53 </ul>
54 <div class="no-rating" v-show="!food.ratings || !food.ratings.length">暂无评价</div>
55 </div>
56 </div>
57 </div>
58 </div>
59 </transition>
60
61 </template>
62
63 <script type="text/ecmascript-6">
64 import Vue from 'vue';
65 import BScroll from 'better-scroll';
66 import {formatDate} from '../../common/js/date';
67 import cartcontrol from '../../components/cartcontrol/cartcontrol';
68 import split from '../../components/split/split';
69 import ratingselect from '../../components/ratingselect/ratingselect';
70
71 const ALL = 2;
72
73 export default {
74 props: {
75 food: {
76 type: Object
77 }
78 },
79 data() {
80 return {
81 showFlag: false,
82 selectType: ALL,
83 onlyContent: true,
84 desc: {
85 all: '全部',
86 positive: '推荐',
87 negative: '吐槽'
88 }
89 };
90 },
91 methods: {
92 show() {
93 this.showFlag = true;
94 // 保持初始化
95 this.selectType = ALL;
96 this.onlyContent = false;
97 this.$nextTick(() => {
98 if (!this.scroll) {
99 this.scroll = new BScroll(this.$refs.food, {
100 click: true
101 });
102 } else {
103 this.scroll.refresh();
104 }
105 });
106 },
107 hide() {
108 this.showFlag = false;
109 },
110 addFirst(event) {
111 console.log('click');
112 if (!event._constructed) {
113 // eslint-disable-next-line
114 return;
115 }
116 this.$emit('add', event.target);
117 console.log(event.target);
118 Vue.set(this.food, 'count', 1);
119 },
120 selectRating(type) {
121 this.selectType = type;
122 this.$nextTick(() => {
123 this.scroll.refresh();
124 });
125 },
126 toggleContent() {
127 this.onlyContent = !this.onlyContent;
128 this.$nextTick(() => {
129 this.scroll.refresh();
130 });
131 },
132 needShow(rateType, text) {
133 // 如果选中了“只显示有内容的评论”,并且这条记录的内容为空
134 if (this.onlyContent && !text) {
135 return false;
136 }
137 // 此时 onlyContent 为 false 或者有文本时
138 if (this.selectType === ALL) {
139 return true;
140 } else {
141 return rateType === this.selectType;
142 }
143 }
144 },
145 filters: {
146 formatDate(time) {
147 let date = new Date(time);
148 return formatDate(date, 'yyyy-MM-dd hh:mm');
149 }
150 },
151 components: {
152 cartcontrol,
153 split,
154 ratingselect
155 }
156 };
157 </script>
158
159 <style lang="stylus" rel="stylesheet/stylus">
160 @import "../../common/stylus/mixin.styl"
161
162 .food
163 position fixed
164 left 0
165 top 0
166 bottom 48px
167 z-index 30
168 width 100%
169 background #fff
170 transform translate3d(0, 0, 0)
171 &.move-enter-active, &.move-leave-active
172 transition all 0.2s linear
173 &.move-enter, &.move-leave-active
174 transform translate3d(100%, 0, 0)
175 .image-header
176 position relative
177 width 100%
178 height 0
179 padding-top 100%
180 img
181 position absolute
182 left 0
183 top 0
184 width 100%
185 height 100%
186 .back
187 position absolute
188 left 0
189 top 10px
190 .icon-arrow_lift
191 display block
192 /*点击区域变大*/
193 padding 10px
194 font-size 20px
195 color #fff
196 .content
197 position relative
198 padding 18px
199 .title
200 margin-bottom 8px
201 line-height 14px
202 font-size 14px
203 font-weight 700
204 color rgb(7, 17, 27)
205 .detail
206 margin-bottom 18px
207 height 10px
208 line-height 10px
209 font-size 0
210 .sell-count, .rating
211 font-size 10px
212 color rgb(147, 153, 159)
213 .sell-count
214 margin-right 12px
215 .price
216 font-weight 700
217 line-height 24px
218 .now
219 margin-right 8px
220 font-size 14px
221 color rgb(240, 20, 20)
222 .old
223 text-decoration line-through
224 font-size 10px
225 color rgb(147, 153, 159)
226 .cartcontrol-wrapper
227 position absolute
228 right 12px
229 bottom 12px
230 .buy
231 position absolute
232 right 18px
233 bottom 18px
234 /*因为要盖住cartcontrol组件*/
235 z-index 10
236 height 24px
237 line-height 24px
238 padding 0 12px
239 box-sizing border-box
240 border-radius 12px
241 font-size 10px
242 color #fff
243 background-color rgb(0, 160, 220)
244 opacity 1
245 &.fade-enter-active, &.fade-leave-active
246 transition all 0.2s linear
247 &.fade-enter, &.fade-leave-active
248 opacity 0
249 z-index -1
250 .info
251 padding: 18px
252 .title
253 line-height: 14px
254 margin-bottom: 6px
255 font-size: 14px
256 color: rgb(7, 17, 27)
257 .text
258 line-height: 24px
259 padding: 0 8px
260 font-size: 12px
261 color: rgb(77, 85, 93)
262 .rating
263 padding-top: 18px
264 .title
265 line-height: 14px
266 margin-left: 18px
267 font-size: 14px
268 color: rgb(7, 17, 27)
269 .rating-wrapper
270 padding 0 18px
271 .rating-item
272 position relative
273 padding 16px 0
274 border-1px(rgba(7, 17, 27, 0.1))
275 .user
276 position absolute
277 top 16px
278 right 0
279 line-height 12px
280 font-size 0
281 .username
282 margin-right 6px
283 display inline-block
284 vertical-align top
285 font-size 10px
286 color rgb(147, 153, 159)
287 .avatar
288 border-radius 50%
289 .time
290 margin-bottom 6px
291 line-height 12px
292 font-size 10px
293 color rgb(147, 153, 159)
294 .text
295 line-height 16px
296 font-size 12px
297 color rgb(7, 17, 27)
298 .icon-thumb_up, .icon-thumb_down
299 margin-right 4px
300 line-height 16px
301 font-size 12px
302 .icon-thumb_up
303 color rgb(0, 160, 220)
304 .icon-thumb_down
305 color rgb(147, 153, 159)
306 .no-rating
307 padding 16px 0
308 font-size 12px
309 color rgb(147, 153, 159)
310 </style>
food.vue
1 <template>
2 <div class="split"></div>
3 </template>
4
5 <script type="text/ecmascript-6">
6 export default {};
7 </script>
8
9 <style lang="stylus" rel="stylesheet/stylus">
10 .split
11 width: 100%
12 height: 16px
13 border-top: 1px solid rgba(7, 17, 27, 0.1)
14 border-bottom: 1px solid rgba(7, 17, 27, 0.1)
15 background: #f3f5f7
16 </style>
split.vue
1 <template>
2 <div class="ratingselect">
3 <div class="rating-type border-1px">
4 <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span
5 class="count">{{ratings.length}}</span></span>
6 <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span
7 class="count">{{positives.length}}</span></span>
8 <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span
9 class="count">{{negatives.length}}</span></span>
10 </div>
11 <div @click="toggleContent" class="switch" :class="{'on':onlyContent}">
12 <span class="icon-check_circle"></span>
13 <span class="text">只看有内容的评价</span>
14 </div>
15 </div>
16 </template>
17
18 <script type="text/ecmascript-6">
19 const POSITIVE = 0; // 正面评价
20 const NEGATIVE = 1; // 负面评价
21 const ALL = 2; // 所有评价
22
23 export default {
24 props: {
25 ratings: {
26 type: Array,
27 default() {
28 return [];
29 }
30 },
31 selectType: {
32 type: Number,
33 default: ALL
34 },
35 // 只看有内容评论
36 onlyContent: {
37 type: Boolean,
38 default: false
39 },
40 // 评论
41 desc: {
42 type: Object,
43 default() {
44 return {
45 all: '全部',
46 positive: '满意',
47 negative: '不满意'
48 };
49 }
50 }
51 },
52 methods: {
53 select(type, event) {
54 if (!event._constructed) {
55 // eslint-disable-next-line
56 return;
57 }
58 this.$emit('select', type);
59 },
60 toggleContent(event) {
61 if (!event._constructed) {
62 // eslint-disable-next-line
63 return;
64 }
65 this.$emit('toggle', event);
66 }
67 },
68 computed: {
69 // 正面评价数组
70 positives() {
71 return this.ratings.filter((rating) => {
72 return rating.rateType === POSITIVE;
73 });
74 },
75 // 负面评价数组
76 negatives() {
77 return this.ratings.filter((rating) => {
78 return rating.rateType === NEGATIVE;
79 });
80 }
81 }
82 };
83 </script>
84
85 <style lang="stylus" rel="stylesheet/stylus">
86 @import "../../common/stylus/mixin.styl"
87
88 .ratingselect
89 .rating-type
90 padding: 18px 0
91 margin: 0 18px
92 border-1px(rgba(7, 17, 27, 0.1))
93 /* 消除空白字符影响 */
94 font-size: 0
95 .block
96 display: inline-block
97 padding: 8px 12px
98 margin-right: 8px
99 line-height: 16px
100 border-radius: 1px
101 font-size: 12px
102 color: rgb(77, 85, 93)
103 &.active
104 color #fff
105 .count
106 margin-left: 2px
107 font-size: 8px
108 &.positive
109 background: rgba(0, 160, 220, 0.2)
110 &.active
111 background: rgb(0, 160, 220)
112 &.negative
113 background: rgba(77, 85, 93, 0.2)
114 &.active
115 background: rgb(77, 85, 93)
116 .switch
117 padding: 12px 18px
118 line-height: 24px
119 border-bottom: 1px solid rgba(7, 17, 27, 0.1)
120 color: rgb(147, 153, 159)
121 font-size: 0
122 &.on
123 .icon-check_circle
124 color: #00c850
125 .icon-check_circle
126 /* 设置对齐方式 */
127 display: inline-block
128 vertical-align: top
129 margin-right: 4px
130 font-size: 24px
131 .text
132 display: inline-block
133 vertical-align: top
134 font-size: 12px
135 </style>
ratingselect.vue
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>
goods.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>
cartcontrol.vue