1 布局编写
整个goods组件采用绝对定位布在header页面下方
左侧目录menu-wrapper,右侧商品展示foods-wrapper。
2 左侧目录menu编写
Goods在create函数中请求数据goods
然后在目录中通过列表<ul><li>循环遍历展示
注意:垂直居中布局可以父容器display:table,子元素设为display:table-cell,
vertical-align:middle。
3 右侧商品列表goods编写
使用<ul>和<li>标签双重遍历goods和goods中的foods,并进行渲染。
4 通过better-scroll实现目录和商品列表滚动联动的效果
4.1 better-scroll为第三方js插件库,重点解决移动端(已支持 PC)各种滚动场景需求。使用时需要npm install安装。
4.2 在_initScroll()中通过ref引用取到目录和商品列表dom对象,并生成对应的betterscroll对象menuScroll和foodScroll。foodScroll监听scroll事件,回调函数取到y坐标赋值给scrollY
_initScroll() {
// this.$refs.menuWrapper对应ref="menuWrapper"(驼峰命名),ref用来给元素或子组件注册引用信息,
// 引用信息将会注册在父组件的$refs对象上.
// BScroll第一个参数是dom对象,第二个参数是options对象
this.menuScroll=new BScroll(this.$refs.menuWrapper, { click: true }); // click: true这样better-scroll可以取消事件修饰符,响应点击事件
this.foodScroll=new BScroll(this.$refs.foodWrapper, {click: true, probeType: 3});// probeType:3是探针实时告诉滚动位置?
// foodScroll监听scroll事件,回调函数返回位置参数
this.foodScroll.on('scroll', (pos)=> {
this.scrollY=Math.abs(Math.round(pos.y));
})
},
4.3 在_calculateHeight()中维护一个每个商品列表高度范围的数组
_calculateHeight() {
// food-list-hook的命名方式表示不实际产生样式,用于被js代码操作
let foodList=this.$refs.foodWrapper.getElementsByClassName('food-list-hook');
let height=0;
this.listHeight.push(height)
for (let i=0; i<foodList.length; i++) {
let item=foodList[i];// 取到每一个类元素为food-list-hook的dom
height=height+item.clientHeight;// 通过原生dom的clientHeight接口取到li区域的高度并和之前的高度累加
this.listHeight.push(height)
}
},
4.4通过计算属性currentIndex判断scrollY的滚动范围来设置目录哪一个li该高亮的样式。
currentIndex() {
// currentIndex表示左侧我当前的索引应该在哪
for (let i=0; i<this.listHeight.length; i++) {
let height1=this.listHeight[i];
let height2=this.listHeight[i+1];
if (!height2 ||(this.scrollY>=height1 && this.scrollY<height2)) {
return i;
}
}
return 0;
},
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li class="menu-item" v-for="(item,index) in goods" :key="index" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>
{{item.name}}</span>
</li>
</ul>
</div>
.menu-item
display:table
height 54px
width:56px
padding:0 12px
line-height:14px
&.current
position:relative
z-index:10
margin-top:-1px
background:#fff
font-weight:700
.text
border-none()
以上4步,双向滚动联动即可实现。
需要注意的地方:
A.vue2.0中dom通过ref=“驼峰命名”,ref用来给元素或子组件注册引用信息。引用信息将会注册在父组件的$refs对象上,js中通过this.$refs.menuWrapper即可取到dom对象。
具体细节可参考https://www.jianshu.com/p/6d1c0f82c401?utm_campaign
B._initScroll()和_calculateHeight()要在取到goods数据后当前对象的$nextTick函数中执行,因为数据改变后,vue要在$nextTick中才会执行渲染。
代码如下:
created() {
console.log('goods.vue created 执行')
this.$http.get('/api/goods').then((response) => {
console.log('goods ajax get success')
console.log(response)
let responseJson = response.body
console.log(responseJson)
console.log(responseJson.errno)
if (responseJson.errno === ERR_OK) {
this.goods = responseJson.data
console.log(this.goods)
this.$nextTick(()=>{
// 取到goods数据在下一tick界面渲染后initScroll
this._initScroll()
this._calculateHeight()
})
}
}
},
4.5 通过点击目录,商品列表划到指定的页面。
代码如下:
<li class="menu-item" v-for="(item,index) in goods" :key="index" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
selectMenu(indexAlias, event) {
if (!event._constructed) {
// 浏览器原生点击事件没有_constructed属性,自己派发的事件才有
return
}
let foodList=this.$refs.foodWrapper.getElementsByClassName('food-list-hook');
let el=foodList[indexAlias];// 拿到指定index的dom
this.foodScroll.scrollToElement(el, 300); // 动画时间300ms
}
注意的地方:
a.浏览器原生点击事件没有_constructed属性,通过这一点可以将原生点击事件return,避免出现在浏览器模式下点击执行两次的情况。
b.通过foodScroll.scrollToElement接口实现滚动到具体位置。
5 购物车组件实现
goods.vue:<shopcart></shopcart>要传两个参数
配送费:derlivery-price
起送费 :min-price
<shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.inPrice">
goods.vue中的seller是在app.vue中通过router-view传入的
<router-view :seller="seller"></router-view>
然后shopcart接收参数进行渲染,shopcart购物车是对选择商品的映射。
详情参见shopcart.vue代码
注意涉及到的新知识:
a.动态样式的绑定,例如:class="{'highlight':totalPrice>0}"
b.计算属性的应用和关联
c.es6反引号取变量用法.例如return `¥${this.minPrice}元起送`
6 cartcontrol添加和减少组件实现
Cartcontrol组件在goods.vue中引入,接收遍历的food传参,通过vue接口修改food不存在的属性count。
// this.food.count=1 count属性不存在,vue检测不到
Vue.set(this.food, 'count', 1);// 通过vue接口添加不存在的属性,变化就会被观测到
从而影响到goods.vue中的计算属性selectFoods。
selectFoods() {
let foods = [];
this.goods.forEach((good)=>{
good.foods.forEach((food)=>{
if(food.count>0) {
foods.push(food)
}
})
})
return foods
}
},
selectFoods又传入shopcart组件中
<v-shopcart ref="shopCart" :selectFoods="selectFoods" :deliveryprice="seller.deliveryPrice" :minprice="seller.minPrice"> </v-shopcart>
从而联动配送费,购物结算等数据的显示。
Cartcontrol动画实现:
主要实现 平移 滚动 透明度 的效果
代码如下:
<transition name="move">
<div class="cart-decrease" v-show="food.count>0"
@click="decreaseCart">
<span class="inner icon-remove_circle_outline"></span>
</div>
</transition>
.cart-decrease
display:inline-block
padding:6px
.inner
display:inline-block
line-height:24px
font-size:24px
color:rgb(0,160,220)
&.move-enter-active,&.move-leave-active
transition:all 0.5s linear
&.move-enter,&.move-leave-active
opacity:0
transform:translateX(24px) rotate(180deg)
Vue过渡动画知识点:
从不可见到可见,是enter 相关的样式:.xx-enter,.xx-enter-to,.xx-enter-active;
从可见到不可见,是leave相关的样式:.xx-leave, .xx-leave-to, .xx-leave-active;
<style lang="stylus" rel="stylesheet/stylus">
/* 2.写 .fade-enter和.fade-enter-active的样式。在Vue中会在包裹了transition的元素添加过渡动画。并且在动画的第一帧添加 .fade-enter和.fade-enter-active类。所以例子中 .fade-enter将opacity设置成0。当动画运行到第二帧的时候会将fade-enter类去掉。这时候opacity的值会变回默认值1。这时候.fade-enter-active中的transition检测到了opacity的变化。就将此变化改成3秒完成。 */
.fade-enter {
opacity: 0;
}
.fade-enter-active {
transition: opacity 6s;
}
/* 3.同样这里原理差不多,就是在元素隐藏的第一帧是有一个.fade-leave和.fade-leave-active的样式.该样式的opacity的默认值是1。在动画第二帧,会添加上.fade-leave-to的样式。这时候opacity的样式被设置为0
这时候.fade-leave-active中的transition检测到了opacity的变化。于是将此变化过渡为3秒。 */
.fade-leave-to {
opacity: 0;
}
.fade-leave-active {
transition: opacity 6s;
}
</style>
7 购物车小球动画实现
思路:多种过渡动画的实现往往通过内外两层夹层或多层来实现,每一层实现不同的transition.要实现小球从不可见到可见再消失,需要用到javascript的钩子函数beforeEnter,enter,afterEnter。
首先是在cartcontrol组件中点击添加商品按钮,要给他派发一个事件传递到父组件goods中去:
addCart(event) {
if(!event._constructed) {
return
}
if (!this.food.count) {
// this.food.count=1 count属性不存在,vue检测不到
Vue.set(this.food, 'count', 1);// 通过vue接口添加不存在的属性,变化就会被观测到
} else {
this.food.count++
}
// this.$dispatch('cart.add')
this.$emit('cart-add', event.target)
},
goods父组件中接收:
<div class="cartcontrol-wrapper">
<v-cartcontrol :food="food" @cart-add="cartAdd"></v-cartcontrol>
</div>
再获取到购物车的DOM元素:
<v-shopcart ref="shopCart" :selectFoods="selectFoods" :deliveryprice="seller.deliveryPrice" :minprice="seller.minPrice"> </v-shopcart>
将cartcontrol组件中添加按钮的dom传到购物车shopcart的drop方法中
cartAdd(el) {
this.$nextTick(()=>{
this.$refs.shopCart.drop(el)
})
}
在购物车shopcart组件中实现小球动画:
<div class="ball-container">
<div v-for="ball in balls" :key="ball.id">
<transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
<div class="ball" v-show="ball.show">
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
data() {
return {
// balls数组维护每个小球当前的状态
balls: [
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
}
],
dropBalls:[]
}
methods: {
drop(el) {
console.log('drop')
// console.log(el)
for(var i=0; i<this.balls.length; i++) {
let ball=this.balls[i]
if(!ball.show) {
ball.show=true
ball.el=el
this.dropBalls.push(ball)
console.log(this.balls)
console.log(this.dropBalls)
return
}
}
},
beforeEnter(el) {
console.log('beforeEnter')
console.log(el)
let count=this.balls.length
while(count--) {
let ball=this.balls[count]
if(ball.show) {
// 计算+按钮到购物车xy坐标的偏移值
let rect=ball.el.getBoundingClientRect()
let x = rect.left-32
let y = -(window.innerHeight-rect.top-22)
console.log(x, y)
el.style.display=''
el.style.webkitTransform=`translateY(${y}px)`// 外层做纵向运动
el.style.transform=`translateY(${y}px)`
let inner = el.getElementsByClassName('inner-hook')[0]
inner.style.webkitTransform=`translate3d(${x}px,0,0)`
inner.style.transform=`translate3d(${x}px,0,0)`
}
}
},
enter(el) {
console.log('enter')
/* eslint-disable no-unused-vars */
let rf = el.offsetHeight // 必须重绘,再进行transform才有用
this.$nextTick(()=>{
el.style.webkitTransform='translate3d(0,0,0)'
el.style.transform='translate3d(0,0,0)'
let inner = el.getElementsByClassName('inner-hook')[0]
inner.style.webkitTransform='translate3d(0,0,0)'
inner.style.transform='translate3d(0,0,0)'
})
},
afterEnter(el) {
console.log('afterEnter')
let ball=this.dropBalls.shift();
if(ball) {
ball.show=false
// el.style.display='none'
}
}
}
ball-container
.ball
position:fixed
left:32px
bottom:22px
z-index:200
&.drop-enter-active
transition:all 1s cubic-bezier(0.49,-0.29, 0.75, 0.41)
.inner
width:16px
height:16px
border-radius:50%
background:rgb(0,160,220)
transition:all 1s linear
生活不易,作者除了平时当码工以外还在兼职卖些手办,感兴趣的朋友可以看看,支持一下,谢谢!