作者大大的地址是:https://github.com/JinwenXie/vue-ele-project
还是老办法,先运行项目看看效果
我不算是外卖爱好者,不过觉得那个添加商品到购物车的动画效果很好看很有趣
最近的框架好多,简直应接不暇,比如上篇的博客,我甚至以为作者大大自己封装一系列组件写项目,
搜索了一下才发现居然是van有赞小程序。
这个demo用到的是cube-ui也是一个框架,好像是滴滴的。
我一下子就惊吓到了。新的技术层出不穷,我会的和用到的都比较少,做观众欣赏别人的代码和产品也很有意思。
我们还是看代码吧
//main.js
import Vue from 'vue'
import App from './App.vue'
// 引入cube-ui
import './cube-ui'
// 引入注册组件文件
import './register'
import 'common/stylus/index.styl'
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
主要说的意思就是页面是从app.vue进行渲染的,先看看register中引入了组件
//register.js
// cube-ui的用法
import { createAPI } from 'cube-ui'
import Vue from 'vue'
import HeaderDetail from 'components/header-detail/header-detail'
import ShopCartList from 'components/shop-cart-list/shop-cart-list'
import ShopCartStikcy from 'components/shop-cart-sticky/shop-cart-sticky'
import Food from 'components/food/food'
createAPI(Vue, HeaderDetail)
createAPI(Vue, ShopCartList)
createAPI(Vue, ShopCartStikcy)
createAPI(Vue, Food)
接下来看app.vue中做了什么
先看最上面的
引入了组件
//v-header.vue
<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}}分钟送达
</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}}个</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">
// text/ecmascript-6 用来让编辑器识别vue文件中的es6语法
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"
.header
position: relative
overflow: hidden
color: $color-white
background: $color-background-ss
.content-wrapper
position: relative
display: flex
align-items: center
padding: 24px 12px 18px 24px
.avatar
flex: 0 0 64px
width: 64px
margin-right: 16px
img
border-radius: 2px
.content
flex: 1
.title
display: flex
align-items: center
margin-bottom: 8px
.brand
width: 30px
height: 18px
bg-image('brand')
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: $fontsize-large
font-weight: bold
.description
margin-bottom: 8px
line-height: 12px
font-size: $fontsize-small
.support
display: flex
align-items: center
.support-ico
margin-right: 4px
.text
line-height: 12px
font-size: $fontsize-small-s
.support-count
position: absolute
right: 12px
bottom: 14px
display: flex
align-items: center
padding: 0 8px
height: 24px
line-height: 24px
text-align: center
border-radius: 14px
background: $color-background-sss
.count
font-size: $fontsize-small-s
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: $fontsize-small-s
.bulletin-wrapper
position: relative
display: flex
align-items: center
height: 28px
line-height: 28px
padding: 0 8px
background: $color-background-sss
.bulletin-title
flex: 0 0 22px
width: 22px
height: 12px
margin-right: 4px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
flex: 1
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
font-size: $fontsize-small-s
.icon-keyboard_arrow_right
flex: 0 0 10px
width: 10px
font-size: $fontsize-small-s
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
filter: blur(10px)
</style>
看看v-header里面的SupportIco
//不能理解里面写的是什么意思
<template>
<span class="support-ico" :class="iconCls"></span>
</template>
<script>
export default {
name: 'support-ico',
props: {
size: {
type: Number
},
type: {
type: Number
}
},
computed: {
iconCls() {
const classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
return `icon-${this.size} ${classMap[this.type]}`
}
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/mixin"
.support-ico
display: inline-block
background-repeat: no-repeat
.icon-1
width: 12px
height: 12px
background-size: 12px 12px
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.icon-2
width: 16px
height: 16px
background-size: 16px 16px
&.decrease
bg-image('decrease_2')
&.discount
bg-image('discount_2')
&.guarantee
bg-image('guarantee_2')
&.invoice
bg-image('invoice_2')
&.special
bg-image('special_2')
.icon-3
width: 12px
height: 12px
background-size: 12px 12px
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.icon-4
width: 16px
height: 16px
background-size: 16px 16px
&.decrease
bg-image('decrease_4')
&.discount
bg-image('discount_4')
&.guarantee
bg-image('guarantee_4')
&.invoice
bg-image('invoice_4')
&.special
bg-image('special_4')
</style>
vue提供的 @touchmove.prevent 可以用来阻止滑动,但是这个方法会对其内的子div的滑动事件也禁止掉了,这样会导致中间文字无法滑动。
如果没有中间滑动需求,用 @touchmove.prevent 实现是一个很好的方法。
我们接下来看seller组件
他的页面是
呃呃,其实想不明白,为啥要在header中传入sell组件
//seller组件
<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>起送价</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>平均配送时间</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">公告与活动</h1>
<div class="content-wrapper border-bottom-1px">
<p class="content">{{seller.bulletin}}</p>
</div>
<ul v-if="seller.supports" class="supports">
<li
class="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">商家实景</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>
<li
class="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,
scrollY: false,
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.favorite
saveToLocal(this.seller.id, 'favorite', this.favorite)
}
},
components: {
SupportIco,
Star,
Split
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/variable"
@import "~common/stylus/mixin"
.seller
height: 100%
text-align: left
.overview
position: relative
padding: 18px
.title
margin-bottom: 8px
line-height: 14px
font-size: $fontsize-medium
color: $color-dark-grey
.desc
display: flex
align-items: center
padding-bottom: 18px
.star
margin-right: 8px
.text
margin-right: 12px
line-height: 18px
font-size: $fontsize-small-s
color: $color-grey
.remark
display: flex
padding-top: 18px
.block
flex: 1
text-align: center
border-right: 1px solid $color-col-line
&:last-child
border: none
h2
margin-bottom: 4px
line-height: 10px
font-size: $fontsize-small-s
color: $color-light-grey
.content
line-height: 24px
font-size: $fontsize-small-s
color: $color-dark-grey
.stress
font-size: $fontsize-large-xxx
.favorite
position: absolute
width: 50px
right: 11px
top: 18px
text-align: center
.icon-favorite
display: block
margin-bottom: 4px
line-height: 24px
font-size: $fontsize-large-xxx
color: $color-light-grey-s
&.active
color: $color-red
.text
line-height: 10px
font-size: $fontsize-small-s
color: $color-grey
.bulletin
padding: 18px 18px 0 18px
white-space: normal
.title
margin-bottom: 8px
line-height: 14px
color: $color-dark-grey
font-size: $fontsize-medium
.content-wrapper
padding: 0 12px 16px 12px
.content
line-height: 24px
font-size: $fontsize-small
color: $color-red
.supports
.support-item
display: flex
align-items: center
padding: 16px 12px
&:last-child
border-none()
.support-ico
margin-right: 6px
.text
line-height: 16px
font-size: $fontsize-small
color: $color-dark-grey
.pics
padding: 18px
.title
margin-bottom: 12px
line-height: 14px
color: $color-dark-grey
font-size: $fontsize-medium
.pic-wrapper
display: flex
align-items: center
.pic-list
.pic-item
display: inline-block
margin-right: 6px
width: 120px
height: 90px
&:last-child
margin: 0
.info
padding: 18px 18px 0 18px
color: $color-dark-grey
.title
padding-bottom: 12px
line-height: 14px
font-size: $fontsize-medium
.info-item
padding: 16px 12px
line-height: 16px
font-size: $fontsize-small
&:last-child
border-none()
</style>
看一下common/js/storage
//src\common\js\storage.js
import storage from 'good-storage'
// vue搜索历史本地存储-good-storage
const SELLER_KEY = '__seller__'
export function saveToLocal(id, key, val) {
const seller = storage.get(SELLER_KEY, {})
if (!seller[id]) {
seller[id] = {}
}
seller[id][key] = val
storage.set(SELLER_KEY, seller)
}
export function loadFromLocal(id, key, def) {
const seller = storage.get(SELLER_KEY, {})
if (!seller[id]) {
return def
}
return seller[id][key] || def
}
//src\components\star\star.vue
<template>
<div class="star" :class="starType">
<span v-for="(itemClass,index) in itemClasses" :class="itemClass" class="star-item" :key="index"></span>
</div>
</template>
<script>
const LENGTH = 5
const CLS_ON = 'on'
const CLS_HALF = 'half'
const CLS_OFF = 'off'
export default {
props: {
size: {
type: Number
},
score: {
type: Number
}
},
computed: {
starType() {
return 'star-' + this.size
},
itemClasses() {
let result = []
const score = Math.floor(this.score * 2) / 2
const hasDecimal = score % 1 !== 0
const integer = Math.floor(score)
for (let i = 0; i < integer; i++) {
result.push(CLS_ON)
}
if (hasDecimal) {
result.push(CLS_HALF)
}
while (result.length < LENGTH) {
result.push(CLS_OFF)
}
return result
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/mixin.styl"
.star
display: flex
align-items: center
justify-content: center
.star-item
background-repeat: no-repeat
&.star-48
.star-item
width: 20px
height: 20px
margin-right: 22px
background-size: 20px 20px
&:last-child
margin-right: 0
&.on
bg-image('star48_on')
&.half
bg-image('star48_half')
&.off
bg-image('star48_off')
&.star-36
.star-item
width: 15px
height: 15px
margin-right: 6px
background-size: 15px 15px
&:last-child
margin-right: 0
&.on
bg-image('star36_on')
&.half
bg-image('star36_half')
&.off
bg-image('star36_off')
&.star-24
.star-item
width: 10px
height: 10px
margin-right: 3px
background-size: 10px 10px
&:last-child
margin-right: 0
&.on
bg-image('star24_on')
&.half
bg-image('star24_half')
&.off
bg-image('star24_off')
</style>
上面内容没有看懂
//split.vue
<template>
<div class="split"></div>
</template>
<script>
export default {
name: 'split'
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/variable"
.split
width: 100%
height: 16px
border-top: 1px solid $color-row-line
border-bottom: 1px solid $color-row-line
background: $color-background-ssss
</style>
//src\components\support-ico\support-ico.vue
<template>
<span class="support-ico" :class="iconCls"></span>
</template>
<script>
export default {
name: 'support-ico',
props: {
size: {
type: Number
},
type: {
type: Number
}
},
computed: {
iconCls() {
const classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
return `icon-${this.size} ${classMap[this.type]}`
}
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/mixin"
.support-ico
display: inline-block
background-repeat: no-repeat
.icon-1
width: 12px
height: 12px
background-size: 12px 12px
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.icon-2
width: 16px
height: 16px
background-size: 16px 16px
&.decrease
bg-image('decrease_2')
&.discount
bg-image('discount_2')
&.guarantee
bg-image('guarantee_2')
&.invoice
bg-image('invoice_2')
&.special
bg-image('special_2')
.icon-3
width: 12px
height: 12px
background-size: 12px 12px
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.icon-4
width: 16px
height: 16px
background-size: 16px 16px
&.decrease
bg-image('decrease_4')
&.discount
bg-image('discount_4')
&.guarantee
bg-image('guarantee_4')
&.invoice
bg-image('invoice_4')
&.special
bg-image('special_4')
</style>
上面内容我没有看懂
接下来看tab里面的内容
在App.vue中引入了goods组件
//goods.vue
<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-bar
direction="vertical"
:labels="props.labels"
:txts="barTxts"
:current="props.current"
>
<template slot-scope="props">
<div class="text">
<support-ico
v-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-panel
v-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>好评率{{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-cart
ref="shopCart"
:select-foods="selectFoods"
:delivery-price="seller.deliveryPrice"
:min-price="seller.minPrice"></shop-cart>
</div>
<!--<food :food="selectedFood" ref="food"></food>-->
</div>
</template>
<script>
import { getGoods } from 'api'
import CartControl from 'components/cart-control/cart-control'
import ShopCart from 'components/shop-cart/shop-cart'
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) => {
console.log('good....', good)
// 将数据渲染在页面中
good.foods.forEach((food) => {
if (food.count) {
foods.push(food)
}
})
})
return foods
},
barTxts() {
let ret = []
this.goods.forEach((good) => {
console.log('......good', good)
const { type, name, foods } = good
let count = 0
foods.forEach((food) => {
count += food.count || 0
})
ret.push({
type,
name,
count
})
})
return ret
}
},
methods: {
fetch() {
if (!this.fetched) {
this.fetched = true
getGoods({
id: this.seller.id
}).then((goods) => {
console.log('.......app', goods)
this.goods = goods
})
}
},
selectFood(food) {
this.selectedFood = food
this._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
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/mixin"
@import "~common/stylus/variable"
.goods
position: relative
text-align: left
height: 100%
.scroll-nav-wrapper
position: absolute
width: 100%
top: 0
left: 0
bottom: 48px
>>> .cube-scroll-nav-bar
width: 80px
white-space: normal
overflow: hidden
>>> .cube-scroll-nav-bar-item
padding: 0 10px
display: flex
align-items: center
height: 56px
line-height: 14px
font-size: $fontsize-small
background: $color-background-ssss
.text
flex: 1
position: relative
.num
position: absolute
right: -8px
top: -10px
.support-ico
display: inline-block
vertical-align: top
margin-right: 4px
>>> .cube-scroll-nav-bar-item_active
background: $color-white
color: $color-dark-grey
>>> .cube-scroll-nav-panel-title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid $color-col-line
font-size: $fontsize-small
color: $color-grey
background: $color-background-ssss
.food-item
display: flex
margin: 18px
padding-bottom: 18px
position: relative
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
img
height: auto
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: $fontsize-medium
color: $color-dark-grey
.desc, .extra
line-height: 10px
font-size: $fontsize-small-s
color: $color-light-grey
.desc
line-height: 12px
margin-bottom: 8px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: $fontsize-medium
color: $color-red
.old
text-decoration: line-through
font-size: $fontsize-small-s
color: $color-light-grey
.cart-control-wrapper
position: absolute
right: 0
bottom: 12px
.shop-cart-wrapper
position: absolute
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
</style>
//src\components\cart-control\cart-control.vue
<template>
<div class="cartcontrol">
<transition name="move">
<div class="cart-decrease" v-show="food.count>0" @click.stop="decrease">
<span class="inner icon-remove_circle_outline"></span>
</div>
</transition>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click.stop="add"></div>
</div>
</template>
<script>
const EVENT_ADD = 'add'
export default {
name: 'cart-control',
props: {
food: {
type: Object
}
},
methods: {
add(event) {
if (!this.food.count) {
this.$set(this.food, 'count', 1)
} else {
this.food.count++
}
this.$emit(EVENT_ADD, event.target)
},
decrease() {
if (this.food.count) {
this.food.count--
}
}
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/variable"
.cartcontrol
display: flex
align-items: center
.cart-decrease
display: inline-block
padding: 6px
opacity: 1
.inner
display: inline-block
line-height: 24px
font-size: $fontsize-large-xxx
color: $color-blue
transition: all 0.4s linear
transform: rotate(0)
&.move-enter-active, &.move-leave-active
transition: all 0.4s linear
&.move-enter, &.move-leave-active
opacity: 0
transform: translate3d(24px, 0, 0)
.inner
transform: rotate(180deg)
.cart-count
width: 12px
line-height: 24px
text-align: center
font-size: $fontsize-small-s
color: $color-grey
.cart-add
display: inline-block
padding: 6px
line-height: 24px
font-size: $fontsize-large-xxx
color: $color-blue
</style>
//src\components\shop-cart\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">另需配送费¥{{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 = 10
const 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 = 0
this.selectFoods.forEach((food) => {
total += food.price * food.count
})
return total
},
totalCount() {
let count = 0
this.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.totalPrice
return `还差¥${diff}元起送`
} else {
return '去结算'
}
},
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 = false
this._showShopCartList()
this._showShopCartSticky()
} else {
this.listFold = true
this._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 = true
ball.el = el
this.dropBalls.push(ball)
return
}
}
},
// 丢之前
beforeDrop(el) {
const ball = this.dropBalls[this.dropBalls.length - 1]
const rect = ball.el.getBoundingClientRect()
// getBoundingClientRect用于获取元素相对与浏览器视口的位置
const x = rect.left - 32
const 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.offsetHeight
el.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 = false
el.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.shopCartListComp
list.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"
.shopcart
height: 100%
.content
display: flex
background: $color-background
font-size: 0
color: $color-light-grey
.content-left
flex: 1
.logo-wrapper
display: inline-block
vertical-align: top
position: relative
top: -10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
box-sizing: border-box
border-radius: 50%
background: $color-background
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: $color-dark-grey
&.highlight
background: $color-blue
.icon-shopping_cart
line-height: 44px
font-size: $fontsize-large-xxx
color: $color-light-grey
&.highlight
color: $color-white
.num
position: absolute
top: 0
right: 0
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255, 255, 255, 0.1)
font-weight: 700
font-size: $fontsize-large
&.highlight
color: $color-white
.desc
display: inline-block
vertical-align: top
margin: 12px 0 0 12px
line-height: 24px
font-size: $fontsize-small-s
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-weight: 700
font-size: $fontsize-small
&.not-enough
background: $color-dark-grey
&.enough
background: $color-green
color: $color-white
.ball-container
.ball
position: fixed
left: 32px
bottom: 22px
z-index: 200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width: 16px
height: 16px
border-radius: 50%
background: $color-blue
transition: all 0.4s linear
</style>
//src\components\bubble\bubble.vue
<template>
<span class="bubble">
{{ num }}
</span>
</template>
<script>
export default {
name: 'bubble',
props: {
num: {
type: Number
}
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/variable"
.bubble
display: inline-block
padding: 0 5px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-family: Helvetica
font-weight: 700
font-size: $fontsize-small-s
color: $color-white
background: linear-gradient(to right, $color-orange, $color-red)
</style>
再看rating组件
<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">综合评分</div>
<div class="rank">高于周边商家{{seller.rankRate}}%</div>
</div>
<div class="overview-right">
<div class="score-wrapper">
<span class="title">服务态度</span>
<star :size="36" :score="seller.serviceScore"></star>
<span class="score">{{seller.serviceScore}}</span>
</div>
<div class="score-wrapper">
<span class="title">商品评分</span>
<star :size="36" :score="seller.foodScore"></star>
<span class="score">{{seller.foodScore}}</span>
</div>
<div class="delivery-wrapper">
<span class="title">送达时间</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>
<li
v-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>
<span
class="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 = true
getRatings({
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"
.ratings
position: relative
text-align: left
white-space: normal
height: 100%
.overview
display: flex
padding: 18px 0
.overview-left
flex: 0 0 137px
padding: 6px 0
width: 137px
border-right: 1px solid $color-col-line
text-align: center
@media only screen and (max-width: 320px)
flex: 0 0 120px
width: 120px
.score
margin-bottom: 6px
line-height: 28px
font-size: $fontsize-large-xxx
color: $color-orange
.title
margin-bottom: 8px
line-height: 12px
font-size: $fontsize-small
color: $color-dark-grey
.rank
line-height: 10px
font-size: $fontsize-small-s
color: $color-light-grey
.overview-right
flex: 1
padding: 6px 0 6px 24px
@media only screen and (max-width: 320px)
padding-left: 6px
.score-wrapper
display: flex
align-items: center
margin-bottom: 8px
.title
line-height: 18px
font-size: $fontsize-small
color: $color-dark-grey
.star
margin: 0 12px
.score
line-height: 18px
font-size: $fontsize-small
color: $color-orange
.delivery-wrapper
display: flex
align-items: center
.title
line-height: 18px
font-size: $fontsize-small
color: $color-dark-grey
.delivery
margin-left: 12px
font-size: $fontsize-small
color: $color-light-grey
.rating-wrapper
padding: 0 18px
.rating-item
display: flex
padding: 18px 0
&:last-child
border-none()
.avatar
flex: 0 0 28px
width: 28px
margin-right: 12px
img
height: auto
border-radius: 50%
.content
position: relative
flex: 1
.name
margin-bottom: 4px
line-height: 12px
font-size: $fontsize-small-s
color: $color-dark-grey
.star-wrapper
margin-bottom: 6px
display: flex
align-items: center
.star
margin-right: 6px
.delivery
font-size: $fontsize-small-s
color: $color-light-grey
.text
margin-bottom: 8px
line-height: 18px
color: $color-dark-grey
font-size: $fontsize-small
.recommend
display: flex
align-items: center
flex-wrap: wrap
line-height: 16px
.icon-thumb_up, .item
margin: 0 8px 4px 0
font-size: $fontsize-small-s
.icon-thumb_up
color: $color-blue
.item
padding: 0 6px
border: 1px solid $color-row-line
border-radius: 1px
color: $color-light-grey
background: $color-white
.time
position: absolute
top: 0
right: 0
line-height: 12px
font-size: $fontsize-small
color: $color-light-grey
</style>
//src\components\tab\tab.vue
<template>
<div class="tab">
<cube-tab-bar
:useTransition=false
:showSlider=true
v-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.clientWidth
const slideWidth = this.$refs.slide.slide.scrollerWidth
const transform = -pos.x / slideWidth * tabBarWidth
this.$refs.tabBar.setSliderTransform(transform)
},
onChange(current) {
this.index = current
const instance = this.$refs.component[current]
if (instance && instance.fetch) {
instance.fetch()
}
}
}
}
</script>
<style lang="stylus" scoped>
@import "~common/stylus/variable"
.tab
display: flex
flex-direction: column
height: 100%
>>> .cube-tab
padding: 10px 0
.slide-wrapper
flex: 1
overflow: hidden
</style>
上面的代码:我并没有看懂,应该花一段时间专门学习,还有我发现有些基础知识点我不会。
原文地址:https://www.cnblogs.com/smart-girl/p/11138943.html
时间: 2024-10-12 05:12:56