Vue.js仿饿了么外卖App--(4)商品详情页实现_vue饿了么小程序商品页-程序员宅基地

技术标签: Vue仿饿了么项目  

一、内容介绍

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('./[email protected]')
}
.goods .menu-wrapper .menu-item .discount{
    
  background-image: url('./[email protected]')
}
.goods .menu-wrapper .menu-item .guarantee{
    
  background-image: url('./[email protected]')
}
.goods .menu-wrapper .menu-item .invoice{
    
  background-image: url('./[email protected]')
}
.goods .menu-wrapper .menu-item .special{
    
  background-image: url('./[email protected]')
}
.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>

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/gxgalaxy/article/details/108524802

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文