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