手機(jī)做直播官方網(wǎng)站西安seo關(guān)鍵詞排名優(yōu)化
本文將介紹一款仿“餓了么”商家頁面的App。該案例是基于 Vue2.0 + Vue Router + webpack + ES6
等技術(shù)棧實(shí)現(xiàn)的一款外賣類App,適合初學(xué)者進(jìn)行學(xué)習(xí)。
項(xiàng)目源碼下載鏈接在文章末尾
1 項(xiàng)目概述
該項(xiàng)目是一款仿“餓了么”商家頁面的外賣類App,主要有以下功能。
- 商品導(dǎo)航。
- 商品列表使用手勢(shì)上下滑動(dòng)。
- 購物車中商品的添加和刪除操作。
- 點(diǎn)擊商品查看詳情。
- 商家評(píng)價(jià)。
- 商家信息。
1.1 開發(fā)環(huán)境
首先需要安裝Node.js 12以上的版本,因?yàn)镹ode.js中已經(jīng)繼承了NPM,所以無需在單獨(dú)安裝NPM。然后再安裝Vue腳手架(Vue-CLI)以及創(chuàng)建項(xiàng)目。
項(xiàng)目的調(diào)試使用Google Chrome瀏覽器的控制臺(tái)進(jìn)行,在瀏覽器中按下F12鍵,然后單擊“切換設(shè)備工具欄”,進(jìn)入移動(dòng)端的調(diào)試界面,可以選擇相應(yīng)的設(shè)備進(jìn)行調(diào)試,效果如圖1 所示。
圖 1 項(xiàng)目效果圖
1.2 項(xiàng)目結(jié)構(gòu)
項(xiàng)目結(jié)構(gòu)如圖2所示,其中src文件夾是項(xiàng)目的源文件目錄,src文件夾下的項(xiàng)目結(jié)構(gòu)如圖3所示。
圖2 項(xiàng)目結(jié)構(gòu)
圖3 src文件夾
項(xiàng)目結(jié)構(gòu)中主要文件說明如下。
- dist:項(xiàng)目打包后的靜態(tài)文件存放目錄。
- node_modules:項(xiàng)目依賴管理目錄。
- public:項(xiàng)目的靜態(tài)文件存放目錄,也是本地服務(wù)器的根目錄。
- src:項(xiàng)目源文件存放目錄。
- package.json:項(xiàng)目npm配置文件。
src文件夾目錄說明如下。
- assets:靜態(tài)資源文件存放目。
- components:公共組件存放目錄。
- router:路由配置文件存放目錄。
- store:狀態(tài)管理配置存放目錄。
- views:視圖組件存放目錄。
- App.vue:項(xiàng)目的根組件。
- main.js:項(xiàng)目的入口文件。
2 入口文件
項(xiàng)目的入口文件有 index.html、main.js和App.vue三個(gè)文件,這些入口文件的具體內(nèi)容介紹如下。
2.1 項(xiàng)目入口頁面
index.html是項(xiàng)目默認(rèn)的主渲染頁面文件,主要用于Vue實(shí)例掛載點(diǎn)的聲明與DOM渲染。代碼如下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><link rel="icon" href="<%= BASE_URL %>favicon.ico"><title><%= htmlWebpackPlugin.options.title %></title></head><body><div id="app"></div></body>
</html>
2.2 程序入口文件
main.js是程序的入口文件,主要用于加載各種公共組件和初始化Vue實(shí)例。本項(xiàng)目中的路由設(shè)置和引用的Vant UI組件庫就是在該文件中定義的。代碼如下:
import Vue from 'vue'
import App from './App.vue'
import './cube-ui'
import './register'import 'common/stylus/index.styl'Vue.config.productionTip = falsenew Vue({render: h => h(App)
}).$mount('#app')
本項(xiàng)目案例使用了 Cube UI 組件庫,在項(xiàng)目src目錄下創(chuàng)建 cube-ui.js 文件,用于引入項(xiàng)目中要用到的組件,代碼如下:
import Vue from 'vue'
import {Style,TabBar,Popup,Dialog,Scroll,Slide,ScrollNav,ScrollNavBar
} from 'cube-ui'Vue.use(TabBar)
Vue.use(Popup)
Vue.use(Dialog)
Vue.use(Scroll)
Vue.use(Slide)
Vue.use(ScrollNav)
Vue.use(ScrollNavBar)
2.3 組件入口文件
App.vue是項(xiàng)目的根組件,所有的頁面都是在App.vue下面切換的,所有的頁面組件都是App.vue的子組件。在App.vue組件內(nèi)只需要使用 組件作為占位符,就可以實(shí)現(xiàn)各個(gè)頁面的引入。代碼如下:
<template><div id="app" @touchmove.prevent><v-header :seller="seller"></v-header><div class="tab-wrapper"><tab :tabs="tabs"></tab></div></div>
</template><script>import qs from 'query-string'import { getSeller } from 'api'import VHeader from 'components/v-header/v-header'import Goods from 'components/goods/goods'import Ratings from 'components/ratings/ratings'import Seller from 'components/seller/seller'import Tab from 'components/tab/tab'export default {data() {return {seller: {id: qs.parse(location.search).id}}},computed: {tabs() {return [{label: '商品',component: Goods,data: {seller: this.seller}},{label: '評(píng)論',component: Ratings,data: {seller: this.seller}},{label: '商家',component: Seller,data: {seller: this.seller}}]}},created() {this._getSeller()},methods: {_getSeller() {getSeller({id: this.seller.id}).then((seller) => {this.seller = Object.assign({}, this.seller, seller)})}},components: {Tab,VHeader}}
</script><style lang="stylus" scoped>#app.tab-wrapperposition: fixedtop: 136pxleft: 0right: 0bottom: 0
</style>
3 項(xiàng)目組件
項(xiàng)目中所有頁面組件都在views文件夾中定義,具體組件內(nèi)容介紹如下。
3.1 頭部組件
頭部組件主要展示商家的基本信息,如圖4所示。
圖 4 頭部組件效果
代碼如下:
<template><div class="header" @click="showDetail"><div class="content-wrapper"><div class="avatar"><img width="64" height="64" :src="seller.avatar"></div><div class="content"><div class="title"><span class="brand"></span><span class="name">{{seller.name}}</span></div><div class="description">{{seller.description}}/{{seller.deliveryTime}}分鐘送達(dá)</div><div v-if="seller.supports" class="support"><support-ico :size=1 :type="seller.supports[0].type"></support-ico><span class="text">{{seller.supports[0].description}}</span></div></div><div v-if="seller.supports" class="support-count"><span class="count">{{seller.supports.length}}個(gè)</span><i class="icon-keyboard_arrow_right"></i></div></div><div class="bulletin-wrapper"><span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span><i class="icon-keyboard_arrow_right"></i></div><div class="background"><img :src="seller.avatar" width="100%" height="100%"></div></div>
</template><script type="text/ecmascript-6">import SupportIco from 'components/support-ico/support-ico'export default {name: 'v-header',props: {seller: {type: Object,default() {return {}}}},methods: {showDetail() {this.headerDetailComp = this.headerDetailComp || this.$createHeaderDetail({$props: {seller: 'seller'}})this.headerDetailComp.show()}},components: {SupportIco}}
</script><style lang="stylus" rel="stylesheet/stylus">@import "~common/stylus/mixin"@import "~common/stylus/variable".headerposition: relativeoverflow: hiddencolor: $color-whitebackground: $color-background-ss.content-wrapperposition: relativedisplay: flexalign-items: centerpadding: 24px 12px 18px 24px.avatarflex: 0 0 64pxwidth: 64pxmargin-right: 16pximgborder-radius: 2px.contentflex: 1.titledisplay: flexalign-items: centermargin-bottom: 8px.brandwidth: 30pxheight: 18pxbg-image('brand')background-size: 30px 18pxbackground-repeat: no-repeat.namemargin-left: 6pxfont-size: $fontsize-largefont-weight: bold.descriptionmargin-bottom: 8pxline-height: 12pxfont-size: $fontsize-small.supportdisplay: flexalign-items: center.support-icomargin-right: 4px.textline-height: 12pxfont-size: $fontsize-small-s.support-countposition: absoluteright: 12pxbottom: 14pxdisplay: flexalign-items: centerpadding: 0 8pxheight: 24pxline-height: 24pxtext-align: centerborder-radius: 14pxbackground: $color-background-sss.countfont-size: $fontsize-small-s.icon-keyboard_arrow_rightmargin-left: 2pxline-height: 24pxfont-size: $fontsize-small-s.bulletin-wrapperposition: relativedisplay: flexalign-items: centerheight: 28pxline-height: 28pxpadding: 0 8pxbackground: $color-background-sss.bulletin-titleflex: 0 0 22pxwidth: 22pxheight: 12pxmargin-right: 4pxbg-image('bulletin')background-size: 22px 12pxbackground-repeat: no-repeat.bulletin-textflex: 1white-space: nowrapoverflow: hiddentext-overflow: ellipsisfont-size: $fontsize-small-s.icon-keyboard_arrow_rightflex: 0 0 10pxwidth: 10pxfont-size: $fontsize-small-s.backgroundposition: absolutetop: 0left: 0width: 100%height: 100%z-index: -1filter: blur(10px)
</style>
3.2 商品標(biāo)簽欄與側(cè)邊導(dǎo)航組件
在商家信息下方,通過商品標(biāo)簽欄實(shí)現(xiàn)商品、評(píng)價(jià)和商家信息的切換,在商品標(biāo)簽中,通過側(cè)邊導(dǎo)航實(shí)現(xiàn)對(duì)商品列表的滾動(dòng)和分類展示等功能。效果如圖5所示。
圖 5 商品標(biāo)簽欄效果
代碼如下:
<template><div class="tab"><cube-tab-bar:useTransition=false:showSlider=truev-model="selectedLabel":data="tabs"ref="tabBar"class="border-bottom-1px"></cube-tab-bar><div class="slide-wrapper"><cube-slide:loop=false:auto-play=false:show-dots=false:initial-index="index"ref="slide":options="slideOptions"@scroll="onScroll"@change="onChange"><cube-slide-item v-for="(tab,index) in tabs" :key="index"><component ref="component" :is="tab.component" :data="tab.data"></component></cube-slide-item></cube-slide></div></div>
</template><script>export default {name: 'tab',props: {tabs: {type: Array,default() {return []}},initialIndex: {type: Number,default: 0}},data() {return {index: this.initialIndex,slideOptions: {listenScroll: true,probeType: 3,directionLockThreshold: 0}}},computed: {selectedLabel: {get() {return this.tabs[this.index].label},set(newVal) {this.index = this.tabs.findIndex((value) => {return value.label === newVal})}}},mounted() {this.onChange(this.index)},methods: {onScroll(pos) {const tabBarWidth = this.$refs.tabBar.$el.clientWidthconst slideWidth = this.$refs.slide.slide.scrollerWidthconst transform = -pos.x / slideWidth * tabBarWidththis.$refs.tabBar.setSliderTransform(transform)},onChange(current) {this.index = currentconst instance = this.$refs.component[current]if (instance && instance.fetch) {instance.fetch()}}}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable".tabdisplay: flexflex-direction: columnheight: 100%>>> .cube-tabpadding: 10px 0.slide-wrapperflex: 1overflow: hidden
</style>
3.3 購物車組件
在購物車組件中,當(dāng)沒有任何商品的情況下,無法直接選擇,效果如圖6所示。當(dāng)選擇商品后,購物車將被激活,效果如圖7所示。
圖 6 購物車默認(rèn)狀態(tài)
圖 7 選擇商品后的狀態(tài)
當(dāng)點(diǎn)擊購物車圖標(biāo)后,將顯示用戶選中的商品,效果如圖8所示,在購物車商品列表頁面中可以對(duì)商品進(jìn)行加減操作,也可以直接清空購物車。
圖8 購物車商品列表
當(dāng)點(diǎn)擊“去結(jié)算”按鈕時(shí),將彈出購買商品花費(fèi)的金額提示對(duì)話框,效果如圖9所示。
圖9 提示對(duì)話框
具體實(shí)現(xiàn)的代碼如下。
商品購物車組件 shop-cart.vue 文件代碼如下:
<template><div><div class="shopcart"><div class="content" @click="toggleList"><div class="content-left"><div class="logo-wrapper"><div class="logo" :class="{'highlight':totalCount>0}"><i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i></div><div class="num" v-show="totalCount>0"><bubble :num="totalCount"></bubble></div></div><div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div><div class="desc">另需配送費(fèi)¥{{deliveryPrice}}元</div></div><div class="content-right" @click="pay"><div class="pay" :class="payClass">{{payDesc}}</div></div></div><div class="ball-container"><div v-for="(ball,index) in balls" :key="index"><transition@before-enter="beforeDrop"@enter="dropping"@after-enter="afterDrop"><div class="ball" v-show="ball.show"><div class="inner inner-hook"></div></div></transition></div></div></div></div>
</template><script>import Bubble from 'components/bubble/bubble'const BALL_LEN = 10const innerClsHook = 'inner-hook'function createBalls() {let balls = []for (let i = 0; i < BALL_LEN; i++) {balls.push({show: false})}return balls}export default {name: 'shop-cart',props: {selectFoods: {type: Array,default() {return []}},deliveryPrice: {type: Number,default: 0},minPrice: {type: Number,default: 0},sticky: {type: Boolean,default: false},fold: {type: Boolean,default: true}},data() {return {balls: createBalls(),listFold: this.fold}},created() {this.dropBalls = []},computed: {totalPrice() {let total = 0this.selectFoods.forEach((food) => {total += food.price * food.count})return total},totalCount() {let count = 0this.selectFoods.forEach((food) => {count += food.count})return count},payDesc() {if (this.totalPrice === 0) {return `¥${this.minPrice}元起送`} else if (this.totalPrice < this.minPrice) {let diff = this.minPrice - this.totalPricereturn `還差¥${diff}元起送`} else {return '去結(jié)算'}},payClass() {if (!this.totalCount || this.totalPrice < this.minPrice) {return 'not-enough'} else {return 'enough'}}},methods: {toggleList() {if (this.listFold) {if (!this.totalCount) {return}this.listFold = falsethis._showShopCartList()this._showShopCartSticky()} else {this.listFold = truethis._hideShopCartList()}},pay(e) {if (this.totalPrice < this.minPrice) {return}this.$createDialog({title: '支付',content: `您需要支付${this.totalPrice}元`}).show()e.stopPropagation()},drop(el) {for (let i = 0; i < this.balls.length; i++) {const ball = this.balls[i]if (!ball.show) {ball.show = trueball.el = elthis.dropBalls.push(ball)return}}},beforeDrop(el) {const ball = this.dropBalls[this.dropBalls.length - 1]const rect = ball.el.getBoundingClientRect()const x = rect.left - 32const y = -(window.innerHeight - rect.top - 22)el.style.display = ''el.style.transform = el.style.webkitTransform = `translate3d(0,${y}px,0)`const inner = el.getElementsByClassName(innerClsHook)[0]inner.style.transform = inner.style.webkitTransform = `translate3d(${x}px,0,0)`},dropping(el, done) {this._reflow = document.body.offsetHeightel.style.transform = el.style.webkitTransform = `translate3d(0,0,0)`const inner = el.getElementsByClassName(innerClsHook)[0]inner.style.transform = inner.style.webkitTransform = `translate3d(0,0,0)`el.addEventListener('transitionend', done)},afterDrop(el) {const ball = this.dropBalls.shift()if (ball) {ball.show = falseel.style.display = 'none'}},_showShopCartList() {this.shopCartListComp = this.shopCartListComp || this.$createShopCartList({$props: {selectFoods: 'selectFoods'},$events: {leave: () => {this._hideShopCartSticky()},hide: () => {this.listFold = true},add: (el) => {this.shopCartStickyComp.drop(el)}}})this.shopCartListComp.show()},_showShopCartSticky() {this.shopCartStickyComp = this.shopCartStickyComp || this.$createShopCartSticky({$props: {selectFoods: 'selectFoods',deliveryPrice: 'deliveryPrice',minPrice: 'minPrice',fold: 'listFold',list: this.shopCartListComp}})this.shopCartStickyComp.show()},_hideShopCartList() {const list = this.sticky ? this.$parent.list : this.shopCartListComplist.hide && list.hide()},_hideShopCartSticky() {this.shopCartStickyComp.hide()}},watch: {fold(newVal) {this.listFold = newVal},totalCount(count) {if (!this.fold && count === 0) {this._hideShopCartList()}}},components: {Bubble}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".shopcartheight: 100%.contentdisplay: flexbackground: $color-backgroundfont-size: 0color: $color-light-grey.content-leftflex: 1.logo-wrapperdisplay: inline-blockvertical-align: topposition: relativetop: -10pxmargin: 0 12pxpadding: 6pxwidth: 56pxheight: 56pxbox-sizing: border-boxborder-radius: 50%background: $color-background.logowidth: 100%height: 100%border-radius: 50%text-align: centerbackground: $color-dark-grey&.highlightbackground: $color-blue.icon-shopping_cartline-height: 44pxfont-size: $fontsize-large-xxxcolor: $color-light-grey&.highlightcolor: $color-white.numposition: absolutetop: 0right: 0.pricedisplay: inline-blockvertical-align: topmargin-top: 12pxline-height: 24pxpadding-right: 12pxbox-sizing: border-boxborder-right: 1px solid rgba(255, 255, 255, 0.1)font-weight: 700font-size: $fontsize-large&.highlightcolor: $color-white.descdisplay: inline-blockvertical-align: topmargin: 12px 0 0 12pxline-height: 24pxfont-size: $fontsize-small-s.content-rightflex: 0 0 105pxwidth: 105px.payheight: 48pxline-height: 48pxtext-align: centerfont-weight: 700font-size: $fontsize-small&.not-enoughbackground: $color-dark-grey&.enoughbackground: $color-greencolor: $color-white.ball-container.ballposition: fixedleft: 32pxbottom: 22pxz-index: 200transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41).innerwidth: 16pxheight: 16pxborder-radius: 50%background: $color-bluetransition: all 0.4s linear
</style>
商品購物車列表組件 shop-cart-list.vue 文件代碼如下:
<template><transition name="fade"><cube-popup:mask-closable=truev-show="visible"@mask-click="maskClick"position="bottom"type="shop-cart-list":z-index=90><transitionname="move"@after-leave="afterLeave"><div v-show="visible"><div class="list-header"><h1 class="title">購物車</h1><span class="empty" @click="empty">清空</span></div><cube-scroll class="list-content" ref="listContent"><ul><liclass="food"v-for="(food,index) in selectFoods":key="index"><span class="name">{{food.name}}</span><div class="price"><span>¥{{food.price*food.count}}</span></div><div class="cart-control-wrapper"><cart-control @add="onAdd" :food="food"></cart-control></div></li></ul></cube-scroll></div></transition></cube-popup></transition>
</template><script>import CartControl from 'components/cart-control/cart-control'import popupMixin from 'common/mixins/popup'const EVENT_SHOW = 'show'const EVENT_ADD = 'add'const EVENT_LEAVE = 'leave'export default {name: 'shop-cart-list',mixins: [popupMixin],props: {selectFoods: {type: Array,default() {return []}}},created() {this.$on(EVENT_SHOW, () => {this.$nextTick(() => {this.$refs.listContent.refresh()})})},methods: {onAdd(target) {this.$emit(EVENT_ADD, target)},afterLeave() {this.$emit(EVENT_LEAVE)},maskClick() {this.hide()},empty() {this.dialogComp = this.$createDialog({type: 'confirm',content: '清空購物車?',$events: {confirm: () => {this.selectFoods.forEach((food) => {food.count = 0})this.hide()}}})this.dialogComp.show()}},components: {CartControl}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable".cube-shop-cart-listbottom: 48px&.fade-enter, &.fade-leave-activeopacity: 0&.fade-enter-active, &.fade-leave-activetransition: all .3s ease-in-out.move-enter, .move-leave-activetransform: translate3d(0, 100%, 0).move-enter-active, .move-leave-activetransition: all .3s ease-in-out.list-headerheight: 40pxline-height: 40pxpadding: 0 18pxbackground: $color-background-ssss.titlefloat: leftfont-size: $fontsize-mediumcolor: $color-dark-grey.emptyfloat: rightfont-size: $fontsize-smallcolor: $color-blue.list-contentpadding: 0 18pxmax-height: 217pxoverflow: hiddenbackground: $color-white.foodposition: relativepadding: 12px 0box-sizing: border-box.nameline-height: 24pxfont-size: $fontsize-mediumcolor: $color-dark-grey.priceposition: absoluteright: 90pxbottom: 12pxline-height: 24pxfont-weight: 700font-size: $fontsize-mediumcolor: $color-red.cart-control-wrapperposition: absoluteright: 0bottom: 6px</style>
3.4 商品列表組件
在商品標(biāo)簽頁面中,商品列表主要展示所有商品的信息,可以點(diǎn)擊商品卡片右側(cè)的加號(hào)添加購物車。效果如圖10所示。
圖 10 商品列表效果
代碼如下:
<template><div class="goods"><div class="scroll-nav-wrapper"><cube-scroll-nav:side=true:data="goods":options="scrollOptions"v-if="goods.length"><template slot="bar" slot-scope="props"><cube-scroll-nav-bardirection="vertical":labels="props.labels":txts="barTxts":current="props.current"><template slot-scope="props"><div class="text"><support-icov-if="props.txt.type>=1":size=3:type="props.txt.type"></support-ico><span>{{props.txt.name}}</span><span class="num" v-if="props.txt.count"><bubble :num="props.txt.count"></bubble></span></div></template></cube-scroll-nav-bar></template><cube-scroll-nav-panelv-for="good in goods":key="good.name":label="good.name":title="good.name"><ul><li@click="selectFood(food)"v-for="food in good.foods":key="food.name"class="food-item"><div class="icon"><img width="57" height="57" :src="food.icon"></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>好評(píng)率{{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="cart-control-wrapper"><cart-control @add="onAdd" :food="food"></cart-control></div></div></li></ul></cube-scroll-nav-panel></cube-scroll-nav></div><div class="shop-cart-wrapper"><shop-cartref="shopCart":select-foods="selectFoods":delivery-price="seller.deliveryPrice":min-price="seller.minPrice"></shop-cart></div></div>
</template><script>import { getGoods } from 'api'import CartControl from 'components/cart-control/cart-control'import ShopCart from 'components/shop-cart/shop-cart'import Food from 'components/food/food'import SupportIco from 'components/support-ico/support-ico'import Bubble from 'components/bubble/bubble'export default {name: 'goods',props: {data: {type: Object,default() {return {}}}},data() {return {goods: [],selectedFood: {},scrollOptions: {click: false,directionLockThreshold: 0}}},computed: {seller() {return this.data.seller},selectFoods() {let foods = []this.goods.forEach((good) => {good.foods.forEach((food) => {if (food.count) {foods.push(food)}})})return foods},barTxts() {let ret = []this.goods.forEach((good) => {const {type, name, foods} = goodlet count = 0foods.forEach((food) => {count += food.count || 0})ret.push({type,name,count})})return ret}},methods: {fetch() {if (!this.fetched) {this.fetched = truegetGoods({id: this.seller.id}).then((goods) => {this.goods = goods})}},selectFood(food) {this.selectedFood = foodthis._showFood()this._showShopCartSticky()},onAdd(target) {this.$refs.shopCart.drop(target)},_showFood() {this.foodComp = this.foodComp || this.$createFood({$props: {food: 'selectedFood'},$events: {add: (target) => {this.shopCartStickyComp.drop(target)},leave: () => {this._hideShopCartSticky()}}})this.foodComp.show()},_showShopCartSticky() {this.shopCartStickyComp = this.shopCartStickyComp || this.$createShopCartSticky({$props: {selectFoods: 'selectFoods',deliveryPrice: this.seller.deliveryPrice,minPrice: this.seller.minPrice,fold: true}})this.shopCartStickyComp.show()},_hideShopCartSticky() {this.shopCartStickyComp.hide()}},components: {Bubble,SupportIco,CartControl,ShopCart,Food}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".goodsposition: relativetext-align: leftheight: 100%.scroll-nav-wrapperposition: absolutewidth: 100%top: 0left: 0bottom: 48px>>> .cube-scroll-nav-barwidth: 80pxwhite-space: normaloverflow: hidden>>> .cube-scroll-nav-bar-itempadding: 0 10pxdisplay: flexalign-items: centerheight: 56pxline-height: 14pxfont-size: $fontsize-smallbackground: $color-background-ssss.textflex: 1position: relative.numposition: absoluteright: -8pxtop: -10px.support-icodisplay: inline-blockvertical-align: topmargin-right: 4px>>> .cube-scroll-nav-bar-item_activebackground: $color-whitecolor: $color-dark-grey>>> .cube-scroll-nav-panel-titlepadding-left: 14pxheight: 26pxline-height: 26pxborder-left: 2px solid $color-col-linefont-size: $fontsize-smallcolor: $color-greybackground: $color-background-ssss.food-itemdisplay: flexmargin: 18pxpadding-bottom: 18pxposition: relative&:last-childborder-none()margin-bottom: 0.iconflex: 0 0 57pxmargin-right: 10pximgheight: auto.contentflex: 1.namemargin: 2px 0 8px 0height: 14pxline-height: 14pxfont-size: $fontsize-mediumcolor: $color-dark-grey.desc, .extraline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.descline-height: 12pxmargin-bottom: 8px.extra.countmargin-right: 12px.pricefont-weight: 700line-height: 24px.nowmargin-right: 8pxfont-size: $fontsize-mediumcolor: $color-red.oldtext-decoration: line-throughfont-size: $fontsize-small-scolor: $color-light-grey.cart-control-wrapperposition: absoluteright: 0bottom: 12px.shop-cart-wrapperposition: absoluteleft: 0bottom: 0z-index: 50width: 100%height: 48px
</style>
3.5 商家公告組件
點(diǎn)擊頭部區(qū)域,會(huì)彈出商家公告的詳細(xì)內(nèi)容,效果如圖11所示。
圖11 商家公告內(nèi)容
代碼如下:
<template><transition name="fade"><div v-show="visible" class="header-detail" @touchmove.stop.prevent><div class="detail-wrapper clear-fix"><div class="detail-main"><h1 class="name">{{seller.name}}</h1><div class="star-wrapper"><star :size="48" :score="seller.score"></star></div><div class="title"><div class="line"></div><div class="text">優(yōu)惠信息</div><div class="line"></div></div><ul v-if="seller.supports" class="supports"><li class="support-item" v-for="(item,index) in seller.supports" :key="item.id"><support-ico :size=2 :type="seller.supports[index].type"></support-ico><span class="text">{{seller.supports[index].description}}</span></li></ul><div class="title"><div class="line"></div><div class="text">商家公告</div><div class="line"></div></div><div class="bulletin"><p class="content">{{seller.bulletin}}</p></div></div></div><div class="detail-close" @click="hide"><i class="icon-close"></i></div></div></transition>
</template><script>import popupMixin from 'common/mixins/popup'import Star from 'components/star/star'import SupportIco from 'components/support-ico/support-ico'export default {name: 'header-detail',mixins: [popupMixin],props: {seller: {type: Object,default() {return {}}}},components: {SupportIco,Star}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".header-detailposition: fixedz-index: 100top: 0left: 0width: 100%height: 100%overflow: autobackdrop-filter: blur(10px)opacity: 1color: $color-whitebackground: $color-background-s&.fade-enter-active, &.fade-leave-activetransition: all 0.5s&.fade-enter, &.fade-leave-activeopacity: 0background: $color-background.detail-wrapperdisplay: inline-blockwidth: 100%min-height: 100%.detail-mainmargin-top: 64pxpadding-bottom: 64px.nameline-height: 16pxtext-align: centerfont-size: $fontsize-largefont-weight: 700.star-wrappermargin-top: 18pxpadding: 2px 0text-align: center.titledisplay: flexwidth: 80%margin: 28px auto 24px auto.lineflex: 1position: relativetop: -6pxborder-bottom: 1px solid rgba(255, 255, 255, 0.2).textpadding: 0 12pxfont-weight: 700font-size: $fontsize-medium.supportswidth: 80%margin: 0 auto.support-itemdisplay: flexalign-items: centerpadding: 0 12pxmargin-bottom: 12px&:last-childmargin-bottom: 0.support-icomargin-right: 6px.textline-height: 16pxfont-size: $fontsize-small.bulletinwidth: 80%margin: 0 auto.contentpadding: 0 12pxline-height: 24pxfont-size: $fontsize-small.detail-closeposition: relativewidth: 30pxheight: 30pxmargin: -64px auto 0 autoclear: bothfont-size: $fontsize-large-xxxx
</style>
3.6 評(píng)價(jià)內(nèi)容組件
在商家評(píng)價(jià)內(nèi)容的組件中,共有兩個(gè)組成部分,一個(gè)是商家的評(píng)分組件,效果如圖12所示;另一個(gè)是評(píng)價(jià)列表內(nèi)容,效果如圖13所示。
圖 12 評(píng)分組件效果
圖 13 評(píng)價(jià)列表效果
商家評(píng)分組件 ratings.vue 文件代碼如下:
<template><cube-scroll ref="scroll" class="ratings" :options="scrollOptions"><div class="ratings-content"><div class="overview"><div class="overview-left"><h1 class="score">{{seller.score}}</h1><div class="title">綜合評(píng)分</div><div class="rank">高于周邊商家{{seller.rankRate}}%</div></div><div class="overview-right"><div class="score-wrapper"><span class="title">服務(wù)態(tài)度</span><star :size="36" :score="seller.serviceScore"></star><span class="score">{{seller.serviceScore}}</span></div><div class="score-wrapper"><span class="title">商品評(píng)分</span><star :size="36" :score="seller.foodScore"></star><span class="score">{{seller.foodScore}}</span></div><div class="delivery-wrapper"><span class="title">送達(dá)時(shí)間</span><span class="delivery">{{seller.deliveryTime}}分鐘</span></div></div></div><split></split><rating-select@select="onSelect"@toggle="onToggle":selectType="selectType":onlyContent="onlyContent":ratings="ratings"></rating-select><div class="rating-wrapper"><ul><liv-for="(rating,index) in computedRatings":key="index"class="rating-item border-bottom-1px"><div class="avatar"><img width="28" height="28" :src="rating.avatar"></div><div class="content"><h1 class="name">{{rating.username}}</h1><div class="star-wrapper"><star :size="24" :score="rating.score"></star><span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span></div><p class="text">{{rating.text}}</p><div class="recommend" v-show="rating.recommend && rating.recommend.length"><span class="icon-thumb_up"></span><spanclass="item"v-for="(item,index) in rating.recommend":key="index">{{item}}</span></div><div class="time">{{format(rating.rateTime)}}</div></div></li></ul></div></div></cube-scroll>
</template><script>import Star from 'components/star/star'import RatingSelect from 'components/rating-select/rating-select'import Split from 'components/split/split'import ratingMixin from 'common/mixins/rating'import { getRatings } from 'api'import moment from 'moment'export default {name: 'ratings',mixins: [ratingMixin],props: {data: {type: Object}},data () {return {ratings: [],scrollOptions: {click: false,directionLockThreshold: 0}}},computed: {seller () {return this.data.seller || {}}},methods: {fetch () {if (!this.fetched) {this.fetched = truegetRatings({id: this.seller.id}).then((ratings) => {this.ratings = ratings})}},format (time) {return moment(time).format('YYYY-MM-DD hh:mm')}},components: {Star,Split,RatingSelect},watch: {selectType () {this.$nextTick(() => {this.$refs.scroll.refresh()})}}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable"@import "~common/stylus/mixin".ratingsposition: relativetext-align: leftwhite-space: normalheight: 100%.overviewdisplay: flexpadding: 18px 0.overview-leftflex: 0 0 137pxpadding: 6px 0width: 137pxborder-right: 1px solid $color-col-linetext-align: center@media only screen and (max-width: 320px)flex: 0 0 120pxwidth: 120px.scoremargin-bottom: 6pxline-height: 28pxfont-size: $fontsize-large-xxxcolor: $color-orange.titlemargin-bottom: 8pxline-height: 12pxfont-size: $fontsize-smallcolor: $color-dark-grey.rankline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.overview-rightflex: 1padding: 6px 0 6px 24px@media only screen and (max-width: 320px)padding-left: 6px.score-wrapperdisplay: flexalign-items: centermargin-bottom: 8px.titleline-height: 18pxfont-size: $fontsize-smallcolor: $color-dark-grey.starmargin: 0 12px.scoreline-height: 18pxfont-size: $fontsize-smallcolor: $color-orange.delivery-wrapperdisplay: flexalign-items: center.titleline-height: 18pxfont-size: $fontsize-smallcolor: $color-dark-grey.deliverymargin-left: 12pxfont-size: $fontsize-smallcolor: $color-light-grey.rating-wrapperpadding: 0 18px.rating-itemdisplay: flexpadding: 18px 0&:last-childborder-none().avatarflex: 0 0 28pxwidth: 28pxmargin-right: 12pximgheight: autoborder-radius: 50%.contentposition: relativeflex: 1.namemargin-bottom: 4pxline-height: 12pxfont-size: $fontsize-small-scolor: $color-dark-grey.star-wrappermargin-bottom: 6pxdisplay: flexalign-items: center.starmargin-right: 6px.deliveryfont-size: $fontsize-small-scolor: $color-light-grey.textmargin-bottom: 8pxline-height: 18pxcolor: $color-dark-greyfont-size: $fontsize-small.recommenddisplay: flexalign-items: centerflex-wrap: wrapline-height: 16px.icon-thumb_up, .itemmargin: 0 8px 4px 0font-size: $fontsize-small-s.icon-thumb_upcolor: $color-blue.itempadding: 0 6pxborder: 1px solid $color-row-lineborder-radius: 1pxcolor: $color-light-greybackground: $color-white.timeposition: absolutetop: 0right: 0line-height: 12pxfont-size: $fontsize-smallcolor: $color-light-grey
</style>
評(píng)價(jià)內(nèi)容列表組件 rating-select.vue 文件代碼如下:
<template><div class="rating-select"><div class="rating-type border-bottom-1px"><span @click="select(2)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<spanclass="count">{{ratings.length}}</span></span><span @click="select(0)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<spanclass="count">{{positives.length}}</span></span><span @click="select(1)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<spanclass="count">{{negatives.length}}</span></span></div><div @click="toggleContent" class="switch" :class="{'on':onlyContent}"><span class="icon-check_circle"></span><span class="text">只看有內(nèi)容的評(píng)價(jià)</span></div></div>
</template>
<script>const POSITIVE = 0const NEGATIVE = 1const ALL = 2const EVENT_TOGGLE = 'toggle'const EVENT_SELECT = 'select'export default {props: {ratings: {type: Array,default() {return []}},selectType: {type: Number,default: ALL},onlyContent: {type: Boolean,default: false},desc: {type: Object,default() {return {all: '全部',positive: '滿意',negative: '不滿意'}}}},computed: {positives() {return this.ratings.filter((rating) => {return rating.rateType === POSITIVE})},negatives() {return this.ratings.filter((rating) => {return rating.rateType === NEGATIVE})}},methods: {select(type) {this.$emit(EVENT_SELECT, type)},toggleContent() {this.$emit(EVENT_TOGGLE)}}}
</script>
<style lang="stylus" rel="stylesheet/stylus">@import "~common/stylus/variable".rating-select.rating-typepadding: 18px 0margin: 0 18px.blockdisplay: inline-blockpadding: 8px 12pxmargin-right: 8pxline-height: 16pxborder-radius: 1pxfont-size: $fontsize-smallcolor: $color-grey&.activecolor: $color-white.countmargin-left: 2px&.positivebackground: $color-light-blue&.activebackground: $color-blue&.negativebackground: $color-light-grey-s&.activebackground: $color-grey.switchdisplay: flexalign-items: centerpadding: 12px 18pxline-height: 24pxborder-bottom: 1px solid $color-row-linecolor: $color-light-grey&.on.icon-check_circlecolor: $color-green.icon-check_circlemargin-right: 4pxfont-size: $fontsize-large-xxx.textfont-size: $fontsize-small
</style>
3.7 商家信息組件
商家信息組件中設(shè)計(jì)了商家的星級(jí)和服務(wù)內(nèi)容,效果如圖14所示。
圖 14 商家服務(wù)信息效果
以及商家的優(yōu)惠活動(dòng)和公告內(nèi)容。效果如圖15所示。
圖15 商家活動(dòng)公告內(nèi)容
代碼如下:
<template><cube-scroll class="seller" :options="sellerScrollOptions"><div class="seller-content"><div class="overview"><h1 class="title">{{seller.name}}</h1><div class="desc border-bottom-1px"><star :size="36" :score="seller.score"></star><span class="text">({{seller.ratingCount}})</span><span class="text">月售{{seller.sellCount}}單</span></div><ul class="remark"><li class="block"><h2>起送價(jià)</h2><div class="content"><span class="stress">{{seller.minPrice}}</span>元</div></li><li class="block"><h2>商家配送</h2><div class="content"><span class="stress">{{seller.deliveryPrice}}</span>元</div></li><li class="block"><h2>平均配送時(shí)間</h2><div class="content"><span class="stress">{{seller.deliveryTime}}</span>分鐘</div></li></ul><div class="favorite" @click="toggleFavorite"><span class="icon-favorite" :class="{'active':favorite}"></span><span class="text">{{favoriteText}}</span></div></div><split></split><div class="bulletin"><h1 class="title">公告與活動(dòng)</h1><div class="content-wrapper border-bottom-1px"><p class="content">{{seller.bulletin}}</p></div><ul v-if="seller.supports" class="supports"><liclass="support-item border-bottom-1px"v-for="(item,index) in seller.supports":key="index"><support-ico :size=4 :type="seller.supports[index].type"></support-ico><span class="text">{{seller.supports[index].description}}</span></li></ul></div><split></split><div class="pics"><h1 class="title">商家實(shí)景</h1><cube-scroll class="pic-wrapper" :options="picScrollOptions"><ul class="pic-list"><li class="pic-item"v-for="(pic,index) in seller.pics":key="index"><img :src="pic" width="120" height="90"></li></ul></cube-scroll></div><split></split><div class="info"><h1 class="title border-bottom-1px">商家信息</h1><ul><liclass="info-item border-bottom-1px"v-for="(info,index) in seller.infos":key="index">{{info}}</li></ul></div></div></cube-scroll>
</template><script>import { saveToLocal, loadFromLocal } from 'common/js/storage'import Star from 'components/star/star'import Split from 'components/split/split'import SupportIco from 'components/support-ico/support-ico'export default {props: {data: {type: Object,default() {return {}}}},data() {return {favorite: false,sellerScrollOptions: {directionLockThreshold: 0,click: false},picScrollOptions: {scrollX: true,stopPropagation: true,directionLockThreshold: 0}}},computed: {seller() {return this.data.seller || {}},favoriteText() {return this.favorite ? '已收藏' : '收藏'}},created() {this.favorite = loadFromLocal(this.seller.id, 'favorite', false)},methods: {toggleFavorite() {this.favorite = !this.favoritesaveToLocal(this.seller.id, 'favorite', this.favorite)}},components: {SupportIco,Star,Split}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable"@import "~common/stylus/mixin".sellerheight: 100%text-align: left.overviewposition: relativepadding: 18px.titlemargin-bottom: 8pxline-height: 14pxfont-size: $fontsize-mediumcolor: $color-dark-grey.descdisplay: flexalign-items: centerpadding-bottom: 18px.starmargin-right: 8px.textmargin-right: 12pxline-height: 18pxfont-size: $fontsize-small-scolor: $color-grey.remarkdisplay: flexpadding-top: 18px.blockflex: 1text-align: centerborder-right: 1px solid $color-col-line&:last-childborder: noneh2margin-bottom: 4pxline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.contentline-height: 24pxfont-size: $fontsize-small-scolor: $color-dark-grey.stressfont-size: $fontsize-large-xxx.favoriteposition: absolutewidth: 50pxright: 11pxtop: 18pxtext-align: center.icon-favoritedisplay: blockmargin-bottom: 4pxline-height: 24pxfont-size: $fontsize-large-xxxcolor: $color-light-grey-s&.activecolor: $color-red.textline-height: 10pxfont-size: $fontsize-small-scolor: $color-grey.bulletinpadding: 18px 18px 0 18pxwhite-space: normal.titlemargin-bottom: 8pxline-height: 14pxcolor: $color-dark-greyfont-size: $fontsize-medium.content-wrapperpadding: 0 12px 16px 12px.contentline-height: 24pxfont-size: $fontsize-smallcolor: $color-red.supports.support-itemdisplay: flexalign-items: centerpadding: 16px 12px&:last-childborder-none().support-icomargin-right: 6px.textline-height: 16pxfont-size: $fontsize-smallcolor: $color-dark-grey.picspadding: 18px.titlemargin-bottom: 12pxline-height: 14pxcolor: $color-dark-greyfont-size: $fontsize-medium.pic-wrapperdisplay: flexalign-items: center.pic-list.pic-itemdisplay: inline-blockmargin-right: 6pxwidth: 120pxheight: 90px&:last-childmargin: 0.infopadding: 18px 18px 0 18pxcolor: $color-dark-grey.titlepadding-bottom: 12pxline-height: 14pxfont-size: $fontsize-medium.info-itempadding: 16px 12pxline-height: 16pxfont-size: $fontsize-small&:last-childborder-none()
</style>
項(xiàng)目源碼下載:
https://download.csdn.net/download/p445098355/89570496