【vue】餓了么項目-goods商品列表頁開發


1.flex 屬性是 flex-grow、flex-shrink 和 flex-basis 屬性的簡寫屬性。

flex-grow 一個數字,規定項目將相對於其他靈活的項目進行擴展的量。
flex-shrink 一個數字,規定項目將相對於其他靈活的項目進行收縮的量。
flex-basis 項目的長度。合法值:"auto"、"inherit" 或一個后跟 "%"、"px"、"em" 或任何其他長度單位的數字。

2.采用絕對定位,相對於父元素

.good
  display flex
  position absolute
  width 100%
  top 174px
  bottom 46px
  overflow hidden

3.使用vue-resourse獲取json並應用到模板

現在越來越多的數據傳輸方式都是json數據格式,包括用jquery開發時,也有很好用的$.ajax來進行數據請求與處理,那么vue-resource提供了一種類似的,並且api更加簡潔易用,壓縮后文件更小。配合ES 6的Lambda寫法,更加優雅

官網:https://github.com/pagekit/vue-resource/blob/master/docs/http.md

props: {
      seller: {
        type: Object
      }
    },
    data () {
      return {
        goods: [], //一開始goods為空
        listHeight: [],
        scrolly: 0,
        selectedFood: {}
      };
    },
    created() {   //當這個組件被調用的時候,通過后端獲得數據賦值給goods this.$http.get('/api/goods').then((response) => {  // '/api/goods'請求的是data.json下的goods數組
        response = response.body;
        if (response.errno === ERR_OK) {
         this.goods = response.data;
        this.$nextTick(() => {  //可以用 $nextTick 來確保Dom變化后再執行一些事情 this._initScroll();
         this._calculateHeight();
      });
       }
      });this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
    },

注:vue更新到2.0之后,作者就宣告不再對vue-resource更新,而是推薦的axios,它的基本用法可以參考:http://www.kancloud.cn/yunye/axios/234845

4.遍歷取數據

        <span class="text">
            <span v-show="item.type>0" class=" icon" :class="classMap[item.type]"></span>{{item.name}}
          </span>   

classMap[item.type]是一個數組,通過item.type去取對應的class,item.type是data.json中mock的數據

5.display table

此元素會作為塊級表格來顯示(類似 <table>),表格前后帶有換行符。

在table中可用vertical-align middle實現垂直居中

6.添加better-scroll依賴

鏈接:https://github.com/ustbhuangyi/better-scroll

<div class="menu-wrapper" ref="menuWrapper">
      <ul>
        <li v-for="(item, index) in goods" class="menu-item border-1px" :class="{'current':currentIndex === index}"
            @click="selectMenu(index, $event)">
          <span class="text">
            <span v-show="item.type>0" class=" icon" :class="classMap[item.type]"></span>{{item.name}}
          </span>
        </li>
      </ul>
    </div>
    <div class="foods-wrapper" ref="foodWrapper">
      <ul>
        <li v-for="item in goods" class="food-list food-list-hook">
          <h1 class="title">{{item.name}}</h1>
          <ul>
            <li v-for="food in item.foods" class="food-item" @click="selectFood(food, $event)">
              <div class="icon">
                <img :src="food.icon" alt="" width="57">
              </div>
              <div class="content">
                <h2 class="name">{{food.name}}</h2>
                <p class="desc">{{food.description}}</p>
                <div class="extra">
                  <span class="count">月售{{food.sellCount}}</span><span class="count">好評{{food.rating}}</span>
                </div>
                <div class="price">
                  <span class="now">¥{{food.price}}</span><span class="old"
                                                                v-show="food.oldPrice">¥{{food.oldPrice}}</span>
                </div>
                <div class="cartControl-wrapper">
                  <cartControl :food="food" @increment="incrementTotal"></cartControl>
                </div>
              </div>
            </li>
          </ul>
        </li>
      </ul>
    </div>

6.1 $refs 的使用是vue 2 操作dom的一種方式

ref 被用來給元素或子組件注冊引用信息。引用信息將會注冊在父組件的 $refs 對象上。

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子組件上,引用就指向組件實例:

      _initScroll(){
      //初始化scroll區域
        this.menuScroll = new BScroll(this.$refs.menuWrapper, {
          click: true //結合BScroll的接口使用,是否將click事件傳遞,默認被攔截了
        });
        this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
          probeType: 3 //結合BScroll的接口使用,3實時派發scroll事件,探針的作用
        });
        //結合BScroll的接口使用,監聽scroll事件(實時派發的),並獲取鼠標坐標,當滾動時能實時暴露出scroll
        this.foodsScroll.on('scroll', (pos) => { //事件的回調函數 this.scrollY = Math.abs(Math.round(pos.y));//滾動坐標會出現負的,並且是小數,所以需要處理一下,實時取得scrollY
  }) }

vue中更改數據,DOM會跟着做映射,但vue更新DOM是異步的,用 $nextTick ()來確保Dom變化后能調用到_initScroll()方法。調用_initScroll()方法能計算內層ul的高度,當內層ul的高度大於外層wrapper的高度時,可以實現滾動。

6.2 左右兩邊聯動

  • 在vue實例生命周期的開始created分別加載 _initScroll 和 _calculateHeight

  • 通過 _calculateHeight 計算foods內部每一個塊的高度,組成一個數組listHeight

  • 在_initScroll里面,設置了bscroll插件的一個監聽事件scroll,將food區域當前的滾動到的位置的y坐標設置到一個vue實例屬性scrollY this.scrollY = Math.abs(Math.round(pos.y));

  • 通過計算屬性currentIndex,獲取到food滾動區域對應的menu區域的子塊的索引,然后通過設置一個class來做樣式切換變化 :class="{'current':currentIndex === index} ,實現聯動

  • 另外當點擊menu 區域的時候,會觸發selectMenu事件,也會根據點擊到的menu子塊的索引然后去觸發food區域滾動到對應的高度區塊區間 this.foodsScroll.scrollToElement(el, 300);scrollToElement():是better-scroll中的方法,滾動到某個元素,el(必填)表示 dom 元素,time 表示動畫時間,offsetX 和 offsetY 表示坐標偏移量,easing 表示緩動函數

  • 這樣完成整個對應

_calculateHeight()方法計算各個右側區間的高度

 _calculateHeight(){
        let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); //獲取每一個food的dom對象
        let height = 0;
        this.listHeight.push(height); //初始化第一個高度為0
        for (let i = 0; i < foodList.length; i++) {
          let item = foodList[i]; //每一個item都是剛才獲取的food的每一個dom
          height += item.clientHeight; //主要是為了獲取每一個foods內部塊的高度
          this.listHeight.push(height);
        }
      }
    }

實時取得scrollY的值后,需要與左邊進行映射,利用計算屬性:

computed: {
      currentIndex(){ //計算到達哪個區域的區間的時候的對應的索引值
        for (let i = 0; i < this.listHeight.length; i++) {
          let height1 = this.listHeight[i]; //當前menu子塊的高度
          let height2 = this.listHeight[i + 1]; //下一個menu子塊的高度
          //滾動到底部的時候,height2為undefined,需要考慮這種情況
          //需要確定是在兩個menu子塊的高度區間
          if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
            return i; //返回這個menu子塊的索引
          }
        }
        return 0;
      },
      selectFoods() { //自動將所有的goods.food添加一個count屬性,方便做數量運算
        let foods = [];
        this.goods.forEach((good) => {
          good.foods.forEach((food) => {
            if (food.count) {
              foods.push(food);
            }
          });
        });
        return foods;
      }
    }

:class="{'current':currentIndex === index}"當currentIndex === index時才設置current這個class

點擊左側 ,右側響應:

關於在selectMenu中點擊,在pc界面會出現兩次事件,在移動端就只出現一次事件的問題:

原因:bsScrooler會監聽事件(例如touchmove,click之類),並且阻止默認事件(prevent stop),並且他只會監聽移動端的,pc端的沒有監聽

在pc頁面上 bsScroller也派發了一次click事件,原生也派發了一次click事件

//bsScroll的事件,有_constructed: true
MouseEvent {isTrusted: false, _constructed: true, screenX: 0, screenY: 0, clientX: 0…}
//pc的事件
MouseEvent {isTrusted: true, screenX: -1867, screenY: 520, clientX: 53, clientY: 400…}

解決:針對bsScroole的事件,有_constructed: true,所以做處理,return掉非bsScroll的事件

selectMenu(index, event){
        if (!event._constructed) { //去掉自帶的click事件點擊,即pc端直接返回
          return;
        }
        let foodsList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
        let el = foodsList[index];
        //類似jump to的功能,通過這個方法,跳轉到指定的dom
        this.foodsScroll.scrollToElement(el, 300);
      },

7.shopcart組件

也是采用flex布局,右側部分固定寬度(flex 0 0 105px),左邊自適應寬度(flex 1)

采用固定定位,定位在底部(position fixed)

橫向排列display:inline-block

包含購物車圖標的div超出了父元素的高度,我們使用position:relative,並設置top為負來實現

box-sizing: border-box;  則div 設置的寬高將包含 邊框及 padding

border-radius 50%,形成一個圓

選擇了多少商品:定義成數組,底欄其余部分的變化都基於這個對象的變化而變化

selectFoods: {
        type: Array,
        default() {
          return [{price: 20, count: 2}];
        }
      }

計算部分(都基於selectFoods進行相應計算)computed中的函數可以直接在Tempplate中以指針的形式引用

computed: {
      totalPrice() {//計算總價,超過起送額度后提示可付款
        let total = 0;
        this.selectFoods.forEach((food) => {
          total += food.price * food.count;
        });
        return total;
      },
      totalCount() {//計算選中的food數量,在購物車圖標處顯示,采用絕對定位,top:0;right:0;顯示在購物車圖標右上角
        let count = 0;
        this.selectFoods.forEach((food) => {
          count += food.count;
        });
        return count;
      }

控制底部右邊內容隨food的變化而變化,payDesc()控制顯示內容,payClass()添加類調整顯示樣式

 在template中  
         <div class="pay" :class="payClass">
            {{payDesc}}
          </div>    
在computed中:
payDesc() {
        if (this.totalPrice === 0) {
          return `¥${this.minPrice}元起送`; //這里使用的是es6中的反引號
        } else if (this.totalPrice < this.minPrice) {
          let diff = this.minPrice - this.totalPrice;
          return `還差¥${diff}元起送`;
        } else {
          return '去結算';                 //單引號,單引號和反引號不同
        }
      },
      payClass() {
        if (this.totalPrice < this.minPrice) {
          return 'not-enough';
        } else {
          return 'enough';
        }
      }

總結:通過以上學習我們能發現,selectFoods()的變化起着關鍵作用,它的變化會引起DOM的變化,並最終體現到界面上,而我們不用關注DOM內部的具體實現,這就是vue的一大好處。如果采用jQuery完成這些功能會略顯繁雜。

8 cartcontrol組件,它是shopcart的子組件

可以給按鈕增加padding,方便用戶點擊

this.foodScroll = new BScroll(this.$refs.foodWrapper, {
          probeType: 3,
          click: true
        });

click: true 是否派發click事件

通過import Vue from 'vue';使用set接口,通過vue.set()添加屬性,當它變化時就能被檢測到,從而父組件能獲取到count值(遍歷選中的商品時使用)

methods: {
      addCart(event) {
        if (!event._constructed) {
          // 去掉自帶click事件的點擊
          return;
        }
        if (!this.food.count) {
          Vue.set(this.food, 'count', 1);
        } else {
          this.food.count++;
        }
//        event.srcElement.outerHTML
        this.$emit('increment', event.target); // 子組件通過 $emit觸發父組件的方法 increment   還
      },
      decreaseCart(event) {
        if (!event._constructed) {
          // 去掉自帶click事件的點擊
          return;
        }
        this.food.count--;
      }
    }
  };

9 為減號按鈕添加平移、滾動的動畫

  <transition name="fade">   //減號和數字平移動畫
        <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart($event)">
          <transition name="inner">    //數字滾動動畫
          <span class="inner iconfont icon-jian"></span>
          </transition>
        </div>
    </transition>
&.fade-enter-active, &.fade-leave-active {
      transition: all 0.4s linear    <--過渡效果的 CSS 屬性的名稱、過渡效果需要多少時間、速度效果的速度曲線--> }
    &.fade-enter, &.fade-leave-active {
      opacity: 0
      transform translate3d(24px, 0, 0) //這樣可以開啟硬件加速,動畫更流暢,3D旋轉,X軸位移24px }
    .inner
      display inline-block <--設置成inline-block才有高度,才能有動畫-->
      line-height 24px
      font-size 24px
      vertical-align top
      color rgb(0, 160, 220, 0.2)
      &.inner-enter-active, &.inner-leave-active {
        transition: all 0.4s linear
        transform: rotate(0)
      }
      &.inner-enter, &.inner-leave-active {
        opacity: 0
        transform  rotate(180deg)
      }

10 購物小球(拋物線小球)

通過兩個層來控制小球,外層控制一個方向的變化,內層控制另外一個方向的變化(寫兩層才會有拋物線的效果),采用fixed布局(是相對於視口的動畫)

      <div class="ball-container">
        <div v-for="ball in balls">
          <transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">//后面三個為鈎子
            <div v-show="ball.show" class="ball">
              <div class="inner inner-hook">
              </div>
            </div>
          </transition>
        </div>
      </div>

在addCart()方法(在cartControl組件里)中添加(子組件通過 $emit觸發父組件的方法 increment

this.$emit('increment', event.target);

在父組件(goods組件)的template中寫入

 <cartControl :food="food" @increment="incrementTotal"></cartControl>

在(goods組件)method中寫入this.$refs.shopCart指向shopCart組件,該組件中有drop()方法(父組件訪問子組件的方法

首先在HTML中指定ref

當點擊“加號”按鈕時,cartControl組件通過emit觸發父組件goods中的increment方法,並將event.target對象傳入,increment方法將target傳入shopCart子組件中的drop方法,所以drop方法能獲得用戶點擊按鈕的元素,即能獲取點擊按鈕的位置

<shopCart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice"
                :min-price="seller.minPrice" ref="shopCart"></shopCart>
    incrementTotal(target) {
        this.$refs.shopCart.drop(target);
      }
drop(el) {
        for (let 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);
            return;
          }
        }
      }

補充:

但是在vue2.0中$dispatch 和 $broadcast被棄用,因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱,並且這只適用於父子組件間的通信。官方給出的最簡單的升級建議是使用集中的事件處理器,而且也明確說明了 一個空的vue實例就可以做到,因為Vue 實例實現了一個事件分發接口在vue2.0中在初始化vue之前,給data添加一個 名字為eventhub 的空vue對象

某一個組件內調用事件觸發

this.$root.eventHub.$emit('eventName', event.target);

另一個組件內調用事件接收, 在組件銷毀時接除事件綁定,使用$off方法

created:{
    this.$root.eventHub.$on('eventName',(target) => {
    this.functionName(target)
  });
},
method:{
    functionName(target) {
    console.log(target);
    }
}

因為小球是有去無回的動畫過程,這里采用vue中提供的鈎子

<transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">

對應的方法寫在methods中

    beforeEnter(el) {   //找到所以設為true的小球
        let count = this.balls.length;
        while (count--) {
          let ball = this.balls[count];
          if (ball.show) {
            let rect = ball.el.getBoundingClientRect();//返回元素相對於視口偏移的位置
            let x = rect.left - 32;  //點擊的按鈕與小球(fixed)之間x方向的差值
            let y = -(window.innerHeight - rect.top - 22);
            el.style.display = '';    //設置初始位置前,手動置空,覆蓋之前的display:none,使其顯示
            el.style.webkitTransform = `translate3d(0,${y}px,0)`;  //外層元素做縱向的動畫,y是變量
            el.style.transform = `translate3d(0,${y}px,0)`;
            let inner = el.getElementsByClassName('inner-hook')[0];//內層元素做橫向動畫,inner-hook(用於js選擇的樣式名加上-hook,表明只是用                                                                      //於js選擇的,沒有真實的樣式含義)
            inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
            inner.style.transform = `translate3d(${x}px,0,0)`;
          }
        }
      },
      enter(el) {  
//          let rf = el.offestHeight;
        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) {
        let ball = this.dropBalls.shift();  //取到做完動畫的球,再置為false,即重置,它還可以接着被利用
        if (ball) {
          ball.show = false;
          el.style.display = 'none';
        }
      }

關於cubic-bezier(0.49, -0.29, 0.75, 0.41),是動畫拋物曲線(貝塞爾曲線)的配置,可以利用http://cubic-bezier.com/#.23,1.14,.83,.67 進行曲線調試

.ball-container
    .ball
      position fixed
      left 32px
      bottom 22px
      z-index 200
      transition: all 0.6s 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 0.4s linear

點擊小球時有點卡,可采用異步執行的方式回調

incrementTotal(target) {
    this.$nextTick(()=>{
      this.$refs.shopCart.drop(target);
})
}

關於nextTick可以參考:https://segmentfault.com/a/1190000008570874

官方解釋:在下次 DOM 更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM。

11 什么時候需要用的Vue.nextTick()

a.你在Vue生命周期的created()鈎子函數進行的DOM操作一定要放在Vue.nextTick()的回調函數中。原因是什么呢,原因是在created()鈎子函數執行的時候DOM 其實並未進行任何渲染,而此時進行DOM操作無異於徒勞,所以此處一定要將DOM操作的js代碼放進Vue.nextTick()的回調函數中。與之對應的就是mounted鈎子函數,因為該鈎子函數執行時所有的DOM掛載和渲染都已完成,此時在該鈎子函數中進行任何DOM操作都不會有問題 。

b.在數據變化后要執行的某個操作,而這個操作需要使用隨數據改變而改變的DOM結構的時候,這個操作都應該放進Vue.nextTick()的回調函數中。

12 購物車詳情

 transform translate3d(0, -100%, 0),可以使詳情頁高度隨內容的增加而增加

12.1 針對購物車顯示的詳情頁添加滑動插件

listShow() {
        if (!this.totalCount) {
          this.fold = true;
          return false;
        }
        let show = !this.fold;
        if (show) {//如果顯示詳情頁
          this.$nextTick(() => {//數據變化后,DOM並沒有立即生效,而BScroll嚴重依賴於DOM,所以使用nextTick
            if (!this.scroll) {//如果實例不存在,新建
              this.scroll = new BScroll(this.$refs.listContent, {
                click: true
              });
            } else {//實例存在,直接調用refresh接口
              this.scroll.refresh();
            }
          });
        }
        return show;
      }
    }

12.2 購物車清空

setEmpty() {
      this.selectFoods.forEach((food) => {
        food.count = 0
      })
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM