<template> <transition name="move"> <!--要實現這個商品詳情頁的內容滾動,因此須要有一個顯示標誌和一個dom綁定--> <div v-show="showFlag" class="food" ref="food"> </div> </transition> </template>
用ref綁定food的DOM元素,爲了被bscroll作滾動處理css
用transition包裹了整個food,爲了實現這個頁面的進入和退出動畫html
import BScroll from 'better-scroll'; export default { props: { food: { type: Object } }, data(){ return { showFlag: false } }, methods: { show(){ this.showFlag = true; this.$nextTick(() => { //異步添加滾動綁定 if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }) }, hide(){ this.showFlag = false; } }
經過跟hide方法切換showFlag的值來實現顯示隱藏vue
由於整個頁面是比較長的,須要作滾動,因此異步加載bscrolljson
.food position: fixed //霸佔屏幕,全屏顯示,因此用fixed佈局 left: 0 top: 0 bottom: 48px //保留底部購物車底部欄的位置 z-index: 30 //z-index的數值是有考究的,要控制好各個頁面的z-index縱深 width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active //用vue的動畫配置實現動畫 transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0)
對於整個商品詳情頁的打開和關閉效果體驗,須要對其作動畫處理,transitionsegmentfault
動畫是3d變形,從右往左(改變的是x座標),直線移動(linear)數組
<div class="image-header"> <img :src="food.image"> <!--有一個返回按鈕,綁定一個hide方法--> <div class="back" @click="hide"> <!--使用icon--> <i class="icon-arrow_lift"></i> </div> </div>
export default { props: { food: { //用goods.vue組件傳入的food數據 type: Object } } }
在goods.vue組件上傳入的food數據<food @add="addFood" :food="selectedFood" ref="food"></food>
app
.image-header position: relative width: 100% height: 0 padding-top: 100% //這是一個css-hack技巧 img position: absolute top: 0 left: 0 width: 100% //圖片撐滿整個img的div height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift //返回按鈕,使用的是icon的那個css display: block padding: 10px font-size: 20px color: #fff
在w3c規定裏面,padding設置100%的時候,這個值的計算是相對於這個盒子模型的寬度計算的,而後如今是寬是100%,因此padding也是100%,而後使用的是padding-top,那麼就是內上邊距的高度就是寬度的值,因此這樣就能天然撐開一個正方形,這樣的目的在於頁面打開的時候更天然,不會出現閃爍dom
<div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好評率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span><span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <!--引入cartcontrol組件,而且用一個div包裹他--> <div class="cartcontrol-wrapper"> <cartcontrol @add="addFood" :food="food"></cartcontrol> </div> <transition name="fade"> <!--使用.stop.prevent阻止冒泡和默認事件,避免穿透--> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入購物車 </div> </transition> </div>
加入購物車按鈕有一個動畫fade,而且由於他的位置跟cartcontrol有重疊,避免穿透,因此直接.click.stop.prevent
異步
加入購物車按鈕的顯示是經過判斷food.count
實現的,這個屬性默認是沒有的,當點擊購物車按鈕的時候強制生成Vue.set(this.food, 'count', 1);
,這樣作的目的是比較直觀的增長一個屬性去實現功能ide
cartcontrol的使用跟通常組件使用差很少.
import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { props: { food: { type: Object } } methods: { addFirst(event){ //點擊加入購物車按鈕,傳入事件 if (!event._constructed) { //由於購物車按鈕在bscroll裏面,因此須要處理掉bscroll的事件類型 return; } this.$emit('add', event.target); //觸發當前實例food上的事件add(在goods組件上綁定在food組件的add方法) Vue.set(this.food, 'count', 1); }, addFood(target) { //跟add關聯的addFood方法 this.$emit('add', target); //觸發當前實例food上的事件add(在goods組件上綁定在food組件的add方法) } }, components: { cartcontrol } }
兩次觸發當前實例的事件add是由於兩個操做都是同一個動做,這個動做是綁定在food組件上的add方法,而food組件會在goods組件中被導入<food @add="addFood" :food="selectedFood" ref="food"></food>
,而在goods組件裏面,addFood方法就會指向當前goods組件的方法_drop
,繼而使用shopcart的小球拋物線動畫this.$refs.shopcart.drop(target);
,這樣就是實現了使用跨組件調用方法的效果.
.content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 //注意行內元素的空格 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price //這裏的css樣式就是在goods.vue出現過的樣式,因此直接貼過來使用,也能夠作成組件 font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper //固定cartcontrol組件的樣式 position: absolute right: 12px bottom: 12px .buy //加入購物車按鈕的樣式 position: absolute right: 18px bottom: 18px z-index: 10 //顯示在cartcontrol的更外面 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box //不但願padding和border把整個div撐大 border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 //加入動畫,一個是爲了體驗,另一個是爲了延遲觸發隱藏,避免小球拋物線動畫出現問題 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1
這裏的z-index是10,由於加入購物車按鈕和cartcontrol的按鈕重疊了,因此須要設置一個比較靠外的z座標
這裏的box-sizing能夠直觀的設置盒子模型的實際大小,而不用計算邊框和內邊距的影響,
這是box-sizing:border-size以後的圖,注意這個auto,這是由於盒子的大小被固定了,邊框和內邊距也是固定的,而後內容會根據被固定的大小進行auto適配,這種從外到內的空間設置比較直觀和方便
這裏加入動畫的緣由是當點擊加入購物車的按鈕時候(addFirst),由於food.count被添加了數據Vue.set(this.food, 'count', 1);
,因此加入購物車按鈕會被隱藏(v-show的display:none),但同時會執行this.$emit('add', event.target);
,只不過這個是異步執行的,而且這個被異步執行的方法add是拋物線小球動畫計算初始目標高度的地方,因此當傳入的購物車按鈕被設置爲display:none
的時候,動畫的目標初始高度沒法計算,就會去使用父層div的高度,從而影響了拋物線小球動畫效果.
設置一個動畫延遲隱藏購物車按鈕,從而給足夠的時間vue將數據傳遞到異步執行的方法,不影響拋物線小球的動畫初始目標計算
<!--引入split組件負責隔離行--> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div>
import split from '../../components/split/split'; export default { props: { food: { type: Object } }, components: { split } }
.info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93)
<div class="rating"> <h1 class="title">商品評價</h1> <!--ratingselect組件--> <ratingselect @select="selectRating" @toggle="toggleContent" :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <!--根據ratings長度顯示ratings--> <ul v-show="food.ratings && food.ratings.length"> <!--根據不一樣類型的rateType來切換不一樣類型的rate--> <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar"> </div> <!--使用vue過濾器filter來處理時間--> <div class="time">{{rating.rateTime | formatDate}}</div> <p class="text"> <!--根據不一樣類型的rateType來控制icon的顯示--> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <!--沒有rate的時候顯示--> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div>
綁定一個ratingselect組件,負責處理rate選擇的
而後劃分一個rating-wrapper區域,負責顯示相關rate的信息
在子組件ratingselect選擇的信息會影響到父組件的rating-wrapper區域內容的變化
needShow控制當前rate的內容顯示,包括不一樣的type和是否排除沒內容的rate
綁定icon-thumb_up和icon-thumb_down的class來控制贊和批評的icon顯示
根據json數據裏面的ratings數組來控制是否顯示rate
const ALL = 2; //將控制type類轉爲常量控制 import Vue from 'vue'; import ratingselect from '../../components/ratingselect/ratingselect'; //引入ratingselect組件 export default { props: { food: { type: Object } }, data(){ return { //這些是傳入ratingselect組件的數據,而且初始化值 selectType: ALL, //默認全部rate onlyContent: true, //默認只顯示有內容的rate desc: { all: '所有', positive: '推薦', negative: '吐槽' } } }, methods: { needShow(type, text){ if (this.onlyContent && !text) { //只顯示有內容的 而且 沒有內容就返回false return false; } if (this.selectType === ALL) { //顯示所有類型的rate return true; } else { //只顯示對應的類型的rate return type === this.selectType; } }, selectRating(type) { //設置rate的類型 this.selectType = type; this.$nextTick(() => {//切換的時候須要從新刷新bscroll this.scroll.refresh(); }); }, toggleContent() { //切換顯示是否有內容的rate this.onlyContent = !this.onlyContent; this.$nextTick(() => { //切換的時候須要從新刷新bscroll this.scroll.refresh(); }); } }, components: { ratingselect } }
selectRating
和toggleContent
使用異步$nextTick
是由於vue是異步更新dom的,當改變了vue屬性時候,當前的dom不是當即更新的(會致使頁面的高度變化了,可是bscroll來不及更新,影響滾動體驗),而是會放進去異步更新隊列裏面等候更新,即便這個隊列的等待時間不長,可是也來不及立刻更新dom,因此使用$nextTick
強制刷新這個隊列
在food.vue組件使用selectRating
和toggleContent
來更新food.vue組件的屬性,而不能在子組件ratingselect裏面更新,由於vue限制了子組件不能更改父組件的屬性,因此經過使用相似this.$emit('select', type);
來調用父組件的方法來更改
.rating padding-top: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative; padding: 16px 0; border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 //注意行內元素的空隙 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159)