Vue.js仿饿了么外卖App--(5)评价列表页实现

本文介绍了一个评价列表组件的实现过程,包括数据获取、布局设计、星星评分和筛选组件的复用。通过Vue和Better-scroll实现了滚动加载和动态展示功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、内容介绍

1、内容

本篇文章主要介绍的是评价列表的实现。由于评价列表中包含的内容在前面的学习过程中我们都开发过其中的组件,例如星星组件和评价筛选组件。因此在这一部分我们只需要对组件进行复用,然后稍作改动即可。

2、效果

在这里插入图片描述

二、具体实现

1、数据的获取

在评价列表展示页实现过程中我们要从服务器端获取评价信息,并且评价列表展示页是可以滚动的,因此使用better-scroll

  created () {
     axios.get('/api/ratings').then((response) => {
      response = response.data
      // console.log(response)
      if (response.errno === ERR_OK) {
        this.ratings = response.data
        // console.log(this.ratings)
        this.$nextTick(() => {
          this.scroll = new BScroll(this.$refs.ratings, {
            click: true
          })
        })
      }
    })
  },

2、具体布局

template>
  <div class="ratings" ref="ratings">
    <div class="ratings-content">
      <div class="overview">
        <div class="overview-left">
          <h1 class="score">{{seller.score}}</h1>
          <div class="title">综合评价</div>
          <div class="rank">高于周边商家{{seller.rankRate}}</div>
        </div>
        <div class="overview-right">
          <div class="score-wrapper">
            <span class="title">服务态度</span>
            <Star :size="36" :score="seller.serviceScore" class="star"></Star>
            <span class="score">{{seller.serviceScore}}</span>
          </div>
          <div class="score-wrapper">
            <span class="title">商品评分</span>
            <Star :size="36" :score="seller.foodScore"></Star>
            <span class="score">{{seller.foodScore}}</span>
          </div>
          <div class="delivery-wrapper">
            <span class="title">送达时间</span>
            <span class="time">{{seller.deliveryTime}}分钟</span>
          </div>
        </div>
      </div>
      <Split></Split>
      <RatingSelect
           :select-type="selectType"
           :only-content="onlyContent"
           :ratings="ratings"
           @type-select="typeSelect"
           @content-toggle="conToggle">
      </RatingSelect>
      <div class="ratings-wapper">
        <ul>
          <li
           v-for="(rating, index) in ratings" :key="index"
           class="rating-item"
           v-show="needShow(rating.rateType,rating.text)">
            <div class="avatar">
              <img :src="rating.avatar" width="28" height="28" alt="">
            </div>
            <div class="content">
              <h1 class="name">{{rating.username}}</h1>
              <div class="star-wrapper">
                <Star :score="rating.score" class="star"></Star>
                <span class="time" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span>
              </div>
              <p class="text">{{rating.text}}</p>
              <div class="recommend" v-show="rating.recommend">
                <span class="iconfont icon-dianzan"></span>
                <span class="item" v-for="(item, index) in rating.recommend" :key="index">{{item}}</span>
              </div>
              <div class="time">{{rating.rateTime | formatDate}}</div>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

3、star组件

<template>
  <div class="star">
    <span
     v-for="(item,index) in item"
      :key="index"
       :class="item"
       class="star-item"></span>
  </div>
</template>

定义总的星星的数量和状态

// 定义总的星星的数量和状态
  const LENGTH = 5
  const CLS_ON = 'on'
  const CLS_OFF = 'off'
  const CLS_HALF = 'half'

通过计算属性根据获取的分数来计算出星星的数量和状态

 computed: {
    item () {
      let result = []
      let score = Math.floor(this.score * 2) / 2
      let hasDecimal = score % 1 !== 0
      let integer = Math.floor(score)
      for (let i = 0; i < integer; i++) {
        result.push(CLS_ON)
      }
      if (hasDecimal) {
        result.push(CLS_HALF)
      }
      while (result.length < LENGTH) {
        result.push(CLS_OFF)
      }
      return result
    }
  }

4、ratingSelect组件

      <RatingSelect
           :select-type="selectType"
           :only-content="onlyContent"
           :ratings="ratings"
           @type-select="typeSelect"
           @content-toggle="conToggle">
      </RatingSelect>

其中的方法和函数跟(4)的实现一样,在这里不详细介绍

 methods: {
    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')
    }
  }

三、源码

ratings.vue

<template>
  <div class="ratings" ref="ratings">
    <div class="ratings-content">
      <div class="overview">
        <div class="overview-left">
          <h1 class="score">{{seller.score}}</h1>
          <div class="title">综合评价</div>
          <div class="rank">高于周边商家{{seller.rankRate}}</div>
        </div>
        <div class="overview-right">
          <div class="score-wrapper">
            <span class="title">服务态度</span>
            <Star :size="36" :score="seller.serviceScore" class="star"></Star>
            <span class="score">{{seller.serviceScore}}</span>
          </div>
          <div class="score-wrapper">
            <span class="title">商品评分</span>
            <Star :size="36" :score="seller.foodScore"></Star>
            <span class="score">{{seller.foodScore}}</span>
          </div>
          <div class="delivery-wrapper">
            <span class="title">送达时间</span>
            <span class="time">{{seller.deliveryTime}}分钟</span>
          </div>
        </div>
      </div>
      <Split></Split>
      <RatingSelect
           :select-type="selectType"
           :only-content="onlyContent"
           :ratings="ratings"
           @type-select="typeSelect"
           @content-toggle="conToggle">
      </RatingSelect>
      <div class="ratings-wapper">
        <ul>
          <li
           v-for="(rating, index) in ratings" :key="index"
           class="rating-item"
           v-show="needShow(rating.rateType,rating.text)">
            <div class="avatar">
              <img :src="rating.avatar" width="28" height="28" alt="">
            </div>
            <div class="content">
              <h1 class="name">{{rating.username}}</h1>
              <div class="star-wrapper">
                <Star :score="rating.score" class="star"></Star>
                <span class="time" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span>
              </div>
              <p class="text">{{rating.text}}</p>
              <div class="recommend" v-show="rating.recommend">
                <span class="iconfont icon-dianzan"></span>
                <span class="item" v-for="(item, index) in rating.recommend" :key="index">{{item}}</span>
              </div>
              <div class="time">{{rating.rateTime | formatDate}}</div>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'
import Star from '@/components/Star/star'
import Split from '@/components/split/split'
import RatingSelect from '@/components/ratingSelect/ratingSelect'
import axios from 'axios'
import {formatDates} from '@/common/js/date.js'
const ALL = 2
const ERR_OK = 0
export default {
  props: {
    seller: {
      type: Object
    }
  },
  components: {
    Star,
    Split,
    RatingSelect
  },
  data () {
    return {
      showFlag: false,
      selectType: ALL,
      ratings: [],
      onlyContent: true
    }
  },
  created () {
     axios.get('/api/ratings').then((response) => {
      response = response.data
      // console.log(response)
      if (response.errno === ERR_OK) {
        this.ratings = response.data
        // console.log(this.ratings)
        this.$nextTick(() => {
          this.scroll = new BScroll(this.$refs.ratings, {
            click: true
          })
        })
      }
    })
  },
  methods: {
    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>
.ratings{
  position: absolute;
  top: 174px;
  bottom: 0;
  left: 0;
  width: 100%;
  overflow: hidden;
}
.ratings .overview{
  display: flex;
  padding: 18px 0;
}
.ratings .overview .overview-left{
  flex: 0 0 137px;
  border-right: 1px solid rgba(7, 17, 27, 0.1);
  text-align: center;
  padding: 6px 0;
}
.ratings .overview .overview-left .score{
  line-height: 28px;
  font-size: 24px;
  color: rgb(255,153,0);
  margin-bottom: 6px;
}
.ratings .overview .overview-left .title{
  font-size: 12px;
  line-height: 12px;
  color: rgb(7, 17, 27);
  margin-bottom: 8px;
}
.ratings .overview .overview-left .rank{
  font-size: 10px;
  line-height: 10px;
  color: rgb(147,153,159);
}
.ratings .overview .overview-right{
  flex: 1;
  padding: 6px 0 6px 24px;
}
.ratings .overview .overview-right .score-wrapper{
  margin-bottom: 8px;
  font-size: 0;
}
.ratings .overview .overview-right .score-wrapper .title{
  display: inline-block;
  vertical-align: top;
  font-size: 12px;
  line-height: 18px;
  color: rgb(7, 17, 27);
}
.ratings .overview .overview-right .score-wrapper .star{
  display: inline-block;
  vertical-align: top;
  margin: 0 12px;
}
.ratings .overview .overview-right .score-wrapper  .score{
  display: inline-block;
  line-height: 18px;
  vertical-align: top;
  font-size: 12px;
  color: rgb(255,153,0);
}
.ratings .overview .overview-right .delivery-wrapper{
  font-size: 0;
}
.ratings .overview .overview-right .delivery-wrapper .title{
  font-size: 12px;
  line-height: 18px;
  color: rgb(7, 17, 27);
}
.ratings .overview .overview-right .delivery-wrapper .time{
  font-size: 12px;
  margin-left: 12px;
  color: rgb(147,153,159);
}
.ratings .ratings-wapper{
  padding: 0 18px;
}
.ratings .ratings-wapper .rating-item{
  display: flex;
  padding: 18px 0;
  border-bottom: 1px solid rgba(7, 17, 27, 0.1);
}
.ratings .ratings-wapper .rating-item .avatar{
  flex: 0 0 28px;
  width: 28px;
  margin-right: 12px;
}
.ratings .ratings-wapper .rating-item .avatar img{
  border-radius: 50%;
}
.ratings .ratings-wapper .rating-item .content{
  position: relative;
  flex: 1;
}
.ratings .ratings-wapper .rating-item .content .name{
  margin-bottom: 4px;
  line-height: 12px;
  font-size: 10px;
  color: rgb(7, 17, 27);
}
.ratings .ratings-wapper .rating-item .content .star-wrapper{
  margin-bottom: 6px;
  font-size: 0;
}
.ratings .ratings-wapper .rating-item .content .star-wrapper .star{
  display: inline-block;
  margin-right: 6px;
  vertical-align: top;
  font-size: 10px;
}
.ratings .ratings-wapper .rating-item .content .star-wrapper .time{
  position: relative;
  display: inline-block;
  vertical-align: top;
  color: rgb(147,153,159);
  font-size: 12px;
  margin-top: 5px;
  line-height: 12px;
}
.ratings .ratings-wapper .rating-item .content .text{
  line-height: 18px;
  color: rgb(7, 17, 27);
  font-size: 12px;
  margin-bottom: 8px;
}
.ratings .ratings-wapper .rating-item .content .recommend{
  line-height: 16px;
  font-size: 0;
}
.ratings .ratings-wapper .rating-item .content .recommend .iconfont,
.ratings .ratings-wapper .rating-item .content .recommend .item{
  display: inline-block;
  margin: 0 8px 4px 0;
  font-size: 9px;
}
.ratings .ratings-wapper .rating-item .content .recommend .iconfont{
  color: rgb(0,160,220);
}
.ratings .ratings-wapper .rating-item .content .recommend .item{
  padding: 0 6px;
  border:  1px solid rgba(7, 17, 27, 0.1);
  border-radius: 1px;
  color: rgb(147,153,159);
  background: #fff;
}
.ratings .ratings-wapper .rating-item .content .time{
  position: absolute;
  top: 0;
  right: 0;
  line-height: 12px;
  font-size: 10px;
  color: rgb(147,153,159);
}
</style>

star.vue

<template>
  <div class="star">
    <span
     v-for="(item,index) in item"
      :key="index"
       :class="item"
       class="star-item"></span>
  </div>
</template>

<script>
// 定义总的星星的数量和状态
  const LENGTH = 5
  const CLS_ON = 'on'
  const CLS_OFF = 'off'
  const CLS_HALF = 'half'
export default {
  props: {
    seller: {
      type: Object
    },
    score: {
      type: Number
    }
  },
  computed: {
    item () {
      let result = []
      let score = Math.floor(this.score * 2) / 2
      let hasDecimal = score % 1 !== 0
      let integer = Math.floor(score)
      for (let i = 0; i < integer; i++) {
        result.push(CLS_ON)
      }
      if (hasDecimal) {
        result.push(CLS_HALF)
      }
      while (result.length < LENGTH) {
        result.push(CLS_OFF)
      }
      return result
    }
  }
}
</script>

<style scoped>
.star{
  font-size: 0;
}
.star .star-item{
  display: inline-block;
  width:15px;
  height: 15px;
  margin-right: 6px;
  background-size: 15px 15px;
  background-repeat: no-repeat;
}
.star .on{
  background-image: url('./star36_on@2x.png');
}
.star .half{
  background-image: url('./star36_half@2x.png');
}
.star .off{
  background-image: url('./star36_off@2x.png');
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值