vue高仿饿了么项目学习笔记之三:商品goods组件的实现

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章