一、内容介绍
1、内容
本篇文章主要实现的是商品详情页的展示,主要包括商品图片展示、商品信息展示和商品评价展示
2、效果
二、具体实现
1、组件传值
goods.vue
<Food :food="selectedFood" ref="food"></Food>
food.vue通过props属性接收
props: {
food: {
type: Object
}
// selectedCount: {
// type: Number,
// default: 0
// }
},
2、点击事件
当点击goods组件中的商品的时候展开商品详情页,因此给商品添加点击事件,让他能够选择food并保存到selectedFood 中
<li @click="selectedFoods(food,$event)"
v-for="(food,index) in item.foods" :key="index" class="food-item">
<div class="icon">
<img :src="food.icon" alt="" width="57" height="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>好评率{{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">
<Cartcontrol :food="food" v-on:car-add="carAdd"></Cartcontrol>
</div>
</div>
</li>
selectedFoods (food, event) {
if (!event._constructed) {
return
}
this.selectedFood = food
this.$refs.food.show()
},
可以给food组件设置show函数。
由于子组件无法直接使用父组件的函数,父组件可以调用子组件的方法
通过观察food组件中的showflag的值实现展开
data () {
return {
showFlag: false,
selectType: ALL,
onlyContent: true,
desc: {
all: '全部',
positive: '推荐',
negative: '吐槽'
}
}
},
show () {
// 每次加载之前进行初始化
this.showFlag = true
this.selectType = ALL
this.onlyContent = true
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
})
} else {
this.scroll.refresh()
}
})
},
在父组件goods中调用子组件的方法,使用refs
<Food :food="selectedFood" ref="food"></Food>
在selectedfoods中使用
selectedFoods (food, event) {
if (!event._constructed) {
return
}
this.selectedFood = food
// console.log('1')
console.log(this.selectedFood)
this.$refs.food.show()
},
3、图片展示
图片是固定在顶部的,由于图片的高度是不固定的因此一开始不能将模块的高度固定下来,但是可以使用
height: 0;
padding-top: 100%;
将图片达到等比的效果。
<div class="image-header">
<img :src="food.image" alt="" />
<div class="back" @click="hide">
<i class="iconfont icon-fanhui"></i>
</div>
</div>
4、加入购物车
在商品详情页中也可以进行购物,该商品的选择数量为0的时候,展示加入购物车,当选择的数量不为0时,展示cartcontrol组件。
<div class="content">
<h1 class="title">{{ food.name }}</h1>
<div class="food-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>
<div
class="buy"
v-show="!food.count || food.count === 0"
@click.stop.prevent="addFirst"
>
加入购物车
</div>
<div class="cartcontrol-wrapper">
<CartControl :food="food"></CartControl>
</div>
</div>
加入购物车,点击加入购物车的时候,派发car-add事件,并且将this.food的count设置为1。
addFirst () {
if (!event._constructed) {
return
}
this.$emit('car-add', event.target)
this.$set(this.food, 'count', 1)
},
5、分隔条组件
// 分割条组件
<template>
<div class="split"></div>
</template>
<script>
export default {
}
</script>
<style>
.split{
width: 100%;
height: 16px;
border-top: 1px solid rgba(7, 17, 27, 0.1);
border-bottom: 1px solid rgba(7, 17, 27, 0.1);
background: #f3f5f7;
}
</style>
6、评价展示
商品评价部分主要包括评价的展示还有对不同类型的评价的筛选过滤。
布局
<div class="rating">
<h1 class="title">商品评价</h1>
<RatingSelect
:select-type="selectType"
:only-content="onlyContent"
:desc="desc"
:ratings="food.ratings"
@type-select="typeSelect"
@content-toggle="conToggle"></RatingSelect>
<div class="rating-wrapper">
<ul v-show="food.ratings">
<li v-show="needShow(rating.rateType,rating.text)" v-for="(rating, index) in food.ratings" :key="index" class="rating-item">
<div class="user">
<span class="name">{{rating.username}}</span>
<img :src="rating.avatar" class="avatar" width="12" height="12" alt="">
</div>
<div class="time">{{rating.rateTime | formatDate}}</div>
<p class="text">
<span class="iconfont" :class='{"icon-dianzan":rating.rateType===0,"icon-chaping":rating.rateType===1}'></span>{{rating.text}}
</p>
</li>
</ul>
<div class="no-rating" v-show="!food.ratings">暂无评价</div>
</div>
</div>
评价筛选组件
数据接收,需要接收来自父组件的评价、评价类型、是否只看有内容等信息
props: {
ratings: {
type: Array,
default () {
return []
}
},
// 评价的类型
selectType: {
type: Number,
default: ALL
},
// 只看哪一部分
onlyContent: {
type: Boolean,
default: false
},
// 评价描述
desc: {
type: Object,
default () {
return {
// 推荐吐槽可以通过使用组件的时候通过参数传入进来
all: '全部',
positive: '满意',
negative: '不满意'
}
}
}
},
在评价类型的时候,我们定义了三个常量用来表示正向评价负面评价和全部评价
// 正向评价为0,负向评价为1,所有评价为2
const POSITIVE = 0
const NEGATIVE = 1
const ALL = 2
布局:
<template>
<div class="ratingselect">
<div class="rating-type">
<span @click="select(2,$event)" class="block positive" :class="{active1:typeSelected===2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span>
<span @click="select(0,$event)" class="block positive" :class="{active1:typeSelected===0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
<span @click="select(1,$event)" class="block negative" :class="{active2:typeSelected===1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
</div>
<div @click="toggleContent" class="switch" :class="{on:contOnly}">
<span class="iconfont icon-success1"></span>
<span class="text">只看内容的评价</span>
</div>
</div>
</template>
父组件数据的定义和传入
定义:
data () {
return {
showFlag: false,
selectType: ALL,
onlyContent: true,
desc: {
all: '全部',
positive: '推荐',
negative: '吐槽'
}
}
},
传入:
<RatingSelect
:select-type="selectType"
:only-content="onlyContent"
:desc="desc"
:ratings="food.ratings"
@type-select="typeSelect"
@content-toggle="conToggle"></RatingSelect>
由于每次使用RatingSelect组件的时候,可能都是传入的不同的状态,因此每次在使用的时候我们都应该在父组件food.vue中进行初始化
show () {
// 每次加载之前进行初始化
this.showFlag = true
this.selectType = ALL
this.onlyContent = true
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
})
} else {
this.scroll.refresh()
}
})
},
添加点击事件,实现评价类型切换。由于我们在父组件food.vue中使用selectType来控制初始的评价类型,而在子组件RatingSelect.vue中无法直接改变父组件中的数据,因此触发一个事件,交给父组件来处理。在select函数中,我们将评价类型和事件作为参数。
RatingSelect.vue
select (type, event) {
if (!event._constructed) {
return
}
// console.log('1')
this.typeSelected = type
// 子组件派发事件,父组件监听事件
this.$emit('type-select', type)
},
food.vue
// 评价类型切换
typeSelect (type) {
this.selectType = type
// 由于改变selectType的时候DOM是没有更新的,因此还是需要异步更新
this.$nextTick(() => {
this.scroll.refresh()
})
},
同样只展示内容也是一样的子组件派发事件,父组件进行数据的修改
RatingSelect.vue
// 按钮改变
toggleContent (event) {
if (!event._constructed) {
return
}
this.contOnly = !this.contOnly
this.$emit('content-toggle', this.contOnly)
}
food.vue
// 有无内容展示
conToggle (onlyContent) {
this.onlyContent = onlyContent
this.$nextTick(() => {
this.scroll.refresh()
})
}
},
计算正向和负向评价
computed: {
// 计算正向评价
positives () {
return this.ratings.filter((rating) => {
return rating.rateType === POSITIVE
})
},
// 计算吐槽评价
negatives () {
return this.ratings.filter((rating) => {
return rating.rateType === NEGATIVE
})
}
}
点击显示对应条件的评价
当我们点击不同的评价类型的时候,需要切换显示对象条件的评价,在这里我们利用v-show来控制
<li v-show="needShow(rating.rateType,rating.text)" v-for="(rating, index) in food.ratings" :key="index" class="rating-item">
<div class="user">
<span class="name">{{rating.username}}</span>
<img :src="rating.avatar" class="avatar" width="12" height="12" alt="">
</div>
<div class="time">{{rating.rateTime | formatDate}}</div>
<p class="text">
<span class="iconfont" :class='{"icon-dianzan":rating.rateType===0,"icon-chaping":rating.rateType===1}'></span>{{rating.text}}
</p>
</li>
通过needShow函数返回的Boolean值来实现该功能
needShow (type, text) {
// 判断是否要显示内容
if (this.onlyContent && !text) {
return false
}
// 判断选择的类型
if (this.selectType === ALL) {
return true
} else {
return (type === this.selectType)
}
},
时间展示
由于服务器拿到的时间为时间戳,我们在展示的时候需要将时间转化成字符串,因此在这里我们定义一个filters实现
<div class="time">{{rating.rateTime | formatDate}}</div>
filters: {
formatDate (time) {
let date = new Date(time)
return formatDates(date, 'yyyy-MM-dd hh:mm')
}
}
formatDate函数的实现
文件位置common/js/date.js
export function formatDates (date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + ''.substr(4 - RegExp.$1.length)))
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + ''
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str))
}
}
return fmt
}
function padLeftZero (str) {
return ('00' + str).substr(str.length)
}
三、源码
goods.vue
<template>
<div>
<div class="goods">
<!-- 左侧菜单 -->
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li
v-for="(item,index) in goods"
:key="index"
class="menu-item"
: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="foodsWrapper">
<ul>
<li v-for="(item,index) in goods" :key="index" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li @click="selectedFoods(food,$event)"
v-for="(food,index) in item.foods" :key="index" class="food-item">
<div class="icon">
<img :src="food.icon" alt="" width="57" height="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>好评率{{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">
<Cartcontrol :food="food" v-on:car-add="carAdd"></Cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<!-- 购物车 -->
<ShopCart
ref="shopcart"
:delivery-price="seller.deliveryPrice"
:min-price="seller.minPrice"
:select-foods="selectFoods"></ShopCart>
</div>
<Food :food="selectedFood" ref="food"></Food>
</div>
</template>
<script>
import axios from 'axios'
import BScroll from 'better-scroll'
import ShopCart from '@/components/shopcart/shopcart'
import Cartcontrol from '@/components/cartcontrol/cartcontrol'
import Food from '@/components/food/food'
const ERR_OK = 0
export default {
props: {
seller: {
type: Object
}
},
data () {
return {
goods: [],
listHeight: [], // 用来存储每个区间的高度
scrollY: 0,
selectedFood: {}
}
},
components: {
ShopCart,
Cartcontrol,
Food
},
computed: {
// 计算对应切换的菜单下标
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
},
// 选中的food
selectFoods () {
let foods = []
// 找到所有被选择的foods
this.goods.forEach((good) => {
good.foods.forEach((food) => {
// 如果food.count不为0的话,表示已经被选中过,将food push进foods中
if (food.count) {
foods.push(food)
}
})
})
return foods
}
},
created () {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
axios.get('/api/goods').then((response) => {
response = response.data
// console.log(response)
if (response.errno === ERR_OK) {
this.goods = response.data
console.log(this.goods)
this.$nextTick(() => {
// 由于DOM对象是异步更新的
// $nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM
this._initScroll()
// 计算每一模块的高度,实现左右联动
this._calculateHeight()
})
}
})
},
methods: {
// 滚动函数
_initScroll () {
// BScroll()有两个参数,第一个是dom对象,第二个是可选对象。给dom对象增加ref属性
this.menuScroll = new BScroll(this.$refs.menuWrapper, {
click: true
})
this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
click: true,
// 获取实时滚动的位置
probeType: 3
})
// 监听滚动事件
this.foodsScroll.on('scroll', (pos) => {
this.scrollY = Math.abs(Math.round(pos.y))
})
},
_calculateHeight () {
// 使用原生DOM的方法获取高度
// 通过food-list-hook获取每一个区间DOM
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook')
// 高度初始值为0
let height = 0
this.listHeight.push(height)
for (let i = 0; i < foodList.length; i++) {
let item = foodList[i]
// 函数clientHeight得到的DOM对象的高度
height += item.clientHeight
this.listHeight.push(height)
}
},
// 点击左侧menu切换
// 点击左边的menu列表时,根据index,通过scrollToElement把右边的列表滚动到对应的位置
selectMenu (index, event) {
// 当自己触发一个事件的时候event._constructed为true,但是浏览器没有这个event._constructed的话为false
if (!event._constructed) {
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook')
let el = foodList[index]
this.foodsScroll.scrollToElement(el, 300)
// console.log(index)
},
selectedFoods (food, event) {
if (!event._constructed) {
return
}
this.selectedFood = food
// console.log('1')
console.log(this.selectedFood)
this.$refs.food.show()
},
carAdd (target) {
this.$refs.shopcart.drop(target)
}
}
}
</script>
<style>
.goods{
display: flex;
position: absolute;
width: 100%;
top: 174px;
bottom: 46px;
overflow: hidden;
}
.goods .menu-wrapper{
flex: 0 0 80px;
width: 80px;
background: #f3f5f7;
}
.goods .menu-wrapper .menu-item{
display: table; /**垂直居中,不管是一行还是两行 */
height: 54px;
width: 56px;
padding: 0 12px;
line-height: 14px;
}
.goods .menu-wrapper .current{
font-size: 12px;
position: relative;
margin-top: -1px;
z-index: 10;
background: #fff;
font-weight: 700;
}
.goods .menu-wrapper .menu-item .icon{
display: inline-block;
width: 12px;
height: 12px;
margin-right: 2px;
background-size: 12px 12px;
background-repeat: no-repeat;
}
.goods .menu-wrapper .menu-item .decrease{
background-image: url('./decrease_4@3x.png')
}
.goods .menu-wrapper .menu-item .discount{
background-image: url('./discount_4@3x.png')
}
.goods .menu-wrapper .menu-item .guarantee{
background-image: url('./guarantee_4@3x.png')
}
.goods .menu-wrapper .menu-item .invoice{
background-image: url('./invoice_4@3x.png')
}
.goods .menu-wrapper .menu-item .special{
background-image: url('./special_4@3x.png')
}
.goods .menu-wrapper .menu-item .text{
font-size: 12px;
display: table-cell;
width: 56px;
vertical-align: middle; /**垂直居中 */
}
.goods .foods-wrapper{
flex: 1;
}
.goods .foods-wrapper .title{
padding-left:14px;
height: 26px;
line-height: 26px;
border-left: 2px solid #d9dde1;
font-size: 12px;
color: rgb(147, 153, 159);
background: #f3f5f7;
}
.goods .foods-wrapper .food-item{
display: flex;
margin: 18px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(7, 17, 27, 0.1);
}
.goods .foods-wrapper .food-item .icon{
flex: 0 0 57px;
margin-right: 10px;
}
.goods .foods-wrapper .food-item .content{
flex: 1;
}
.goods .foods-wrapper .food-item .content .name{
margin: 2px 0 8px 0;
height: 14px;
line-height: 14px;
font-size: 14px;
color: rgb(7, 17, 27);
}
.goods .foods-wrapper .food-item .content .desc,
.goods .foods-wrapper .food-item .content .extra{
line-height: 10px;
font-size: 10px;
color: rgb(147, 153, 159);
}
.goods .foods-wrapper .food-item .content .desc{
margin-bottom: 8px;
line-height: 12px;
}
.goods .foods-wrapper .food-item .content .extra .count{
margin-right: 12px;
}
.goods .foods-wrapper .food-item .content .price{
font-weight: 700;
line-height: 24px;
}
.goods .foods-wrapper .food-item .content .price .now{
margin-right: 18px;
font-size: 14px;
color: rgb(240, 20, 20);
}
.goods .foods-wrapper .food-item .content .price .old{
text-decoration: line-through;
font-size: 10px;
color: rgb(147, 153, 159);
}
.goods .foods-wrapper .food-item .content .cart-control{
position: absolute;
right: 0;
/* bottom: 1px; */
}
</style>
food.vue
<template>
<transition name="move">
<div class="food" v-show="showFlag" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" alt="" />
<div class="back" @click="hide">
<i class="iconfont icon-fanhui"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{ food.name }}</h1>
<div class="food-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>
<div
class="buy"
v-show="!food.count || food.count === 0"
@click.stop.prevent="addFirst"
>
加入购物车
</div>
<div class="cartcontrol-wrapper">
<CartControl :food="food"></CartControl>
</div>
</div>
<Split v-show="food.info"></Split>
<div class="info" v-show="food.info">
<h1 class="title">商品信息</h1>
<p class="text">{{ food.info }}</p>
</div>
<Split></Split>
<div class="rating">
<h1 class="title">商品评价</h1>
<RatingSelect
:select-type="selectType"
:only-content="onlyContent"
:desc="desc"
:ratings="food.ratings"
@type-select="typeSelect"
@content-toggle="conToggle"></RatingSelect>
<div class="rating-wrapper">
<ul v-show="food.ratings">
<li v-show="needShow(rating.rateType,rating.text)" v-for="(rating, index) in food.ratings" :key="index" class="rating-item">
<div class="user">
<span class="name">{{rating.username}}</span>
<img :src="rating.avatar" class="avatar" width="12" height="12" alt="">
</div>
<div class="time">{{rating.rateTime | formatDate}}</div>
<p class="text">
<span class="iconfont" :class='{"icon-dianzan":rating.rateType===0,"icon-chaping":rating.rateType===1}'></span>{{rating.text}}
</p>
</li>
</ul>
<div class="no-rating" v-show="!food.ratings">暂无评价</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll'
import CartControl from '@/components/cartcontrol/cartcontrol'
import Split from '@/components/split/split'
import RatingSelect from '@/components/ratingSelect/ratingSelect'
import {formatDates} from '@/common/js/date.js'
// const POSITIVE = 0
// const NEGATIVE = 1
const ALL = 2
export default {
props: {
food: {
type: Object
}
// selectedCount: {
// type: Number,
// default: 0
// }
},
data () {
return {
showFlag: false,
selectType: ALL,
onlyContent: true,
desc: {
all: '全部',
positive: '推荐',
negative: '吐槽'
}
}
},
components: {
CartControl,
Split,
RatingSelect
},
methods: {
show () {
// 每次加载之前进行初始化
this.showFlag = true
this.selectType = ALL
this.onlyContent = true
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
})
} else {
this.scroll.refresh()
}
})
},
// 关闭商品详情页
hide () {
this.showFlag = false
},
addFirst () {
if (!event._constructed) {
return
}
this.$emit('car-add', event.target)
this.$set(this.food, 'count', 1)
},
needShow (type, text) {
// 判断是否要显示内容
if (this.onlyContent && !text) {
return false
}
// 判断选择的类型
if (this.selectType === ALL) {
return true
} else {
return (type === this.selectType)
}
},
// 评价类型切换
typeSelect (type) {
this.selectType = type
// 由于改变selectType的时候DOM是没有更新的,因此还是需要异步更新
this.$nextTick(() => {
this.scroll.refresh()
})
},
// 有无内容展示
conToggle (onlyContent) {
this.onlyContent = onlyContent
this.$nextTick(() => {
this.scroll.refresh()
})
}
},
//
filters: {
formatDate (time) {
let date = new Date(time)
return formatDates(date, 'yyyy-MM-dd hh:mm')
}
}
}
</script>
<style scoped>
.food {
/* 覆盖整个屏幕 */
position: fixed;
left: 0;
/* 底部有个购物车 */
bottom: 48px;
top: 0;
z-index: 30;
width: 100%;
background: #fff;
}
.move-enter-active,
.move-leave-active {
transform: translate3d(0, 0, 0);
transition: all 0.3s linear;
}
.move-enter,
.move-leave {
transform: translate3d(100%, 0, 0);
}
.food .food-content .image-header {
position: relative;
width: 100%;
height: 0;
/* 由于手机的屏幕宽度的不确定,因此图片的高度不确定 */
padding-top: 100%;
}
.food .food-content .image-header img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.food .food-content .image-header .back {
position: absolute;
top: 10px;
left: 0;
}
.food .food-content .image-header .iconfont {
display: block;
/* 增大点击区域 */
padding: 10px;
font-size: 20px;
color: #fff;
}
.food .food-content .content {
padding: 18px;
}
.food .food-content .content .title {
font-size: 14px;
line-height: 14px;
margin-bottom: 8px;
font-weight: 700;
color: rgb(7, 17, 27);
}
.food .food-content .content .food-detail {
margin-bottom: 18px;
line-height: 10px;
font-size: 0;
height: 10px;
}
.food .food-content .content .food-detail .sell-count,
.food .food-content .content .food-detail .rating {
font-size: 10px;
color: rgb(147, 153, 159);
}
.food .food-content .content .food-detail .sell-count {
margin-right: 12px;
}
.food .food-content .content .price {
font-weight: 700;
line-height: 24px;
}
.food .food-content .content .price .now {
margin-right: 8px;
font-size: 14px;
color: rgb(240, 20, 20);
}
.food .food-content .content .price .old {
text-decoration: line-through;
font-size: 10px;
color: rgb(147, 153, 159);
}
.food .food-content .content{
position: relative;
}
.food .food-content .cartcontrol-wrapper {
position: absolute;
right: 12px;
bottom: 12px;
}
.food .food-content .buy {
position: absolute;
right: 18px;
bottom: 28px;
z-index: 10;
height: 24px;
line-height: 24px;
padding: 0 12px;
box-sizing: border-box;
font-size: 10px;
border-radius: 12px;
color: #fff;
background: rgb(0, 160, 220);
}
.food .food-content .info{
padding: 18px;
}
.food .food-content .info .title{
line-height: 14px;
margin-bottom: 6px;
font-size: 14px;
color: rgb(7,17,17);
}
.food .food-content .info .text{
line-height: 24px;
font-size: 12px;
padding: 0 8px;
color: rgb(77,85,93);
}
.food .food-content .rating{
padding-top: 18px;
}
.food .food-content .rating .title{
line-height: 14px;
margin-left: 18px;
font-size: 14px;
color: rgb(7,17,17);
}
.food .food-content .rating .rating-wrapper{
padding: 0 18px;
}
.food .food-content .rating .rating-wrapper .rating-item{
position: relative;
padding: 16px 0;
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
}
.food .food-content .rating .rating-wrapper .rating-item .user{
position: absolute;
right: 0;
top: 16px;
font-size: 0;
line-height: 12px;
}
.food .food-content .rating .rating-wrapper .rating-item .user .name{
display: inline-block;
vertical-align: top;
margin-right: 6px;
font-size: 10px;
color: rgb(147, 153, 159);
}
.food .food-content .rating .rating-wrapper .rating-item .user .avatar{
border-radius: 50%;
}
.food .food-content .rating .rating-wrapper .rating-item .time{
margin-bottom: 6px;
line-height: 12px;
font-size: 10px;
color: rgb(147, 153, 159);
}
.food .food-content .rating .rating-wrapper .rating-item .text{
line-height: 16px;
font-size: 12px;
color: rgb(7, 17, 27);
}
.icon-dianzan, .icon-chaping{
margin-right: 4px;
line-height: 16px;
font-size: 12px;
}
.icon-dianzan{
color: rgb(0, 160, 220);
}
.icon-chaping{
color: rgb(147, 153, 159);
}
.food .food-content .rating .rating-wrapper .no-rating{
padding: 16px 0;
font-size: 12px;
color: rgb(147, 153, 159);
}
</style>
ratingSelect.vue
<template>
<div class="ratingselect">
<div class="rating-type">
<span @click="select(2,$event)" class="block positive" :class="{active1:typeSelected===2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span>
<span @click="select(0,$event)" class="block positive" :class="{active1:typeSelected===0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
<span @click="select(1,$event)" class="block negative" :class="{active2:typeSelected===1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
</div>
<div @click="toggleContent" class="switch" :class="{on:contOnly}">
<span class="iconfont icon-success1"></span>
<span class="text">只看内容的评价</span>
</div>
</div>
</template>
<script>
// 正向评价为0,负向评价为1,所有评价为2
const POSITIVE = 0
const NEGATIVE = 1
const ALL = 2
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: '不满意'
}
}
}
},
data () {
return {
typeSelected: this.selectType,
contOnly: this.onlyContent
}
},
methods: {
// 点击区块外层还是有一个
// 菜单切换
select (type, event) {
if (!event._constructed) {
return
}
// console.log('1')
this.typeSelected = type
// 子组件派发事件,父组件监听事件
this.$emit('type-select', type)
},
// 按钮改变
toggleContent (event) {
if (!event._constructed) {
return
}
this.contOnly = !this.contOnly
this.$emit('content-toggle', this.contOnly)
}
},
computed: {
// 计算正向评价
positives () {
return this.ratings.filter((rating) => {
return rating.rateType === POSITIVE
})
},
// 计算吐槽评价
negatives () {
return this.ratings.filter((rating) => {
return rating.rateType === NEGATIVE
})
}
}
}
</script>
<style scoped>
.ratingselect .rating-type{
padding: 18px 0;
margin: 0 18px;
/* 消除间隙 */
font-size: 0;
border-bottom: 1px solid rgba(7, 17, 27,0.1)
}
.ratingselect .rating-type .block{
display: inline-block;
padding: 8px 12px;
border-radius: 2px;
margin-right: 8px;
line-height: 16px;
font-size: 12px;
color: rgb(77,85,93);
}
.ratingselect .rating-type .block .count{
font-size: 8px;
margin-left: 2px;
}
.ratingselect .rating-type .positive{
background: rgba(0,160,220,0.2);
}
.ratingselect .rating-type .negative{
background: rgba(77,85,93,0.2);
}
/* 权重相同后面的会覆盖前面的 */
.ratingselect .rating-type .active1{
background: rgb(0,160,220);
}
.ratingselect .rating-type .active2{
color: black;
background: rgb(77,85,93);
}
.ratingselect .switch{
padding:12px 18px;
line-height: 24px;
border-bottom: 1px solid rgba(7, 17, 27,0.1);
color: rgb(147,153,159);
font-size: 0;
}
.ratingselect .switch .iconfont{
display: inline-block;
vertical-align: top;
font-size: 24px;
margin-right: 4px;
}
/* 被选中 */
.ratingselect .on .iconfont{
color: #00c850;
}
.ratingselect .switch .text{
font-size: 12px;
display: inline-block;
vertical-align: top;
}
</style>