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