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

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签