1

字符过长过滤 数字截取小数点后两位过滤

filters: {
    charsFormat: function (data) {
      if (data && data.length > 49) {
        data = data.substring(0, 49) + '...'
      }
      return data
    }
  },
  
  

  formatData: function (data) {
            let patrn = /^-?\d*\.?\d*$/
            if (patrn.test(data)) {
                let strings = data.toString()
                let index = strings.indexOf('.')
                if ( index > 0) {
                    data = parseFloat(strings.substring(0, index + 3))
                    return data
                }
            } else {
                return data
            }
        }

时间过滤组件

index.js
image.png
使用
在别的组件中加载
import './filters'

import Vue from 'vue'
// import moment from 'moment'
import format from 'date-fns/format'
// 自定义过滤器
Vue.filter('date-format', function (value, formatStr='YYYY-MM-DD HH:mm:ss') {
  // return moment(value).format(formatStr)
  return format(value, formatStr)
})

返回异常的问题

router-link加replace

<router-link replace> 

图片懒加载插件vue-lazyload的使用

图片懒加载: vue-lazyload
1) Github:
https://github.com/hilongjw/vue-lazyload
2) 下载包
npm install --save vue-loader
3) 使用
import VueLazyload from 'vue-lazyload'
import loading from './common/img/loading.gif'
Vue.use(VueLazyload, {
loading
})
<img v-lazy="food.image">

打包文件分析与优化

1) vue 脚手架提供了一个用于可视化分析打包文件的包 webpack-bundle-analyzer 和配置
2) 启用打包可视化: npm run build --report

优化: 使用 date-fns 代替 moment
// import moment from 'moment'
// import {format} from 'date-fns'
import format from 'date-fns/format'
import Vue from 'vue'
Vue.filter('dateString', function (value, formatStr) {
// return moment(value).format(format || 'YYYY-MM-DD HH:mm:ss')
return format(value, formatStr || 'YYYY-MM-DD HH:mm:ss')
})

缓存路由组件对象

<keep-alive>
<router-view />
</keep-alive>
好处: 复用路由组件对象, 复用路由组件获取的后台数据

滑动betterScroll

数据更新后执行

 mounted() {
      this.$store.dispatch('getShopGoods', () => {// 数据更新后执行
        this.$nextTick(() => { // 列表数据更新显示后执行

          this._initScroll()
          this._initTops()
        })
      })

    },
    
    //actions/vux
     // 异步获取商家商品列表
  async getShopGoods({commit}, callback) {
    const result = await reqShopGoods()
    if (result.code === 0) {
      const goods = result.data
      commit(RECEIVE_GOODS, {goods})
      // 数据更新了, 通知一下组件
      callback && callback()
    }
  },

完整

<template>
  <div>
    <div class="goods">
      <div class="menu-wrapper">
        <ul>
          <!--current-->
          <li class="menu-item" v-for="(good, index) in goods" :key="index"
              :class="{current: index===currentIndex}" @click="clickMenuItem(index)">
            <span class="text bottom-border-1px">
              <img class="icon" :src="good.icon" v-if="good.icon">
              {{good.name}}
            </span>
          </li>
        </ul>
      </div>
      <div class="foods-wrapper">
        <ul ref="foodsUl">
          <li class="food-list-hook" v-for="(good, index) in goods" :key="index">
            <h1 class="title">{{good.name}}</h1>
            <ul>
              <li class="food-item bottom-border-1px" v-for="(food, index) in good.foods"
                  :key="index" @click="showFood(food)">
                <div class="icon">
                  <img width="57" height="57" :src="food.icon">
                </div>
                <div class="content">
                  <h2 class="name">{{food.name}}</h2>
                  <p class="desc">{{food.description}}</p>
                  <div class="extra">
                    <span class="count">月售{{food.sellCount}}份</span>
                    <span>好评率{{food.rating}}%</span>
                  </div>
                  <div class="price">
                    <span class="now">¥{{food.price}}</span>
                    <span class="old" v-if="food.oldPrice">¥{{food.oldPrice}}</span>
                  </div>
                  <div class="cartcontrol-wrapper">
                    <CartControl :food="food"/>
                  </div>
                </div>
              </li>
            </ul>
          </li>
        </ul>
      </div>
      <ShopCart />
    </div>
    <Food :food="food" ref="food"/>
  </div>
</template>


<script>
  import BScroll from 'better-scroll'
  import {mapState} from 'vuex'
  import CartControl from '../../../components/CartControl/CartControl.vue'
  import Food from '../../../components/Food/Food.vue'
  import ShopCart from '../../../components/ShopCart/ShopCart.vue'


  export default {
    data() {
      return {
        scrollY: 0, // 右侧滑动的Y轴坐标 (滑动过程时实时变化)
        tops: [], // 所有右侧分类li的top组成的数组  (列表第一次显示后就不再变化)
        food: {}, // 需要显示的food
      }
    },
    mounted() {
      this.$store.dispatch('getShopGoods', () => {// 数据更新后执行
        this.$nextTick(() => { // 列表数据更新显示后执行

          this._initScroll()
          this._initTops()
        })
      })

    },
    computed: {
      ...mapState(['goods']),

      // 计算得到当前分类的下标
      currentIndex() {// 初始和相关数据发生了变化
        // 得到条件数据
        const {scrollY, tops} = this
        // 根据条件计算产生一个结果
        const index = tops.findIndex((top, index) => {
          // scrollY>=当前top && scrollY<下一个top
          return scrollY >= top && scrollY < tops[index + 1]
        })
        // 返回结果
        return index
      }
    },

    methods: {
      // 初始化滚动
      _initScroll() {
        // 列表显示之后创建
        new BScroll('.menu-wrapper', {
          click: true
        })
        this.foodsScroll = new BScroll('.foods-wrapper', {
          probeType: 2,  // 因为惯性滑动不会触发
          click: true
        })

        // 给右侧列表绑定scroll监听
        this.foodsScroll.on('scroll', ({x, y}) => {
          console.log(x, y)
          this.scrollY = Math.abs(y)
        })
        // 给右侧列表绑定scroll结束的监听
        this.foodsScroll.on('scrollEnd', ({x, y}) => {
          console.log('scrollEnd', x, y)
          this.scrollY = Math.abs(y)
        })

      },
      // 初始化tops
      _initTops() {
        // 1. 初始化tops
        const tops = []
        let top = 0
        tops.push(top)
        // 2. 收集
        // 找到所有分类的li
        const lis = this.$refs.foodsUl.getElementsByClassName('food-list-hook')
        Array.prototype.slice.call(lis).forEach(li => {
          top += li.clientHeight
          tops.push(top)
        })

        // 3. 更新数据
        this.tops = tops
        console.log(tops)
      },

      clickMenuItem(index) {
        // console.log(index)
        // 使用右侧列表滑动到对应的位置

        // 得到目标位置的scrollY
        const scrollY = this.tops[index]
        // 立即更新scrollY(让点击的分类项成为当前分类)
        this.scrollY = scrollY
        // 平滑滑动右侧列表
        this.foodsScroll.scrollTo(0, -scrollY, 300)
      },

      // 显示点击的food
      showFood (food) {
        // 设置food
        this.food = food
        // 显示food组件 (在父组件中调用子组件对象的方法)
        this.$refs.food.toggleShow()
      }
    },

    components: {
      CartControl,
      Food,
      ShopCart
    }
  }
</script>

控制商品数量的组件cartControl的使用

cartControl组件

<template>
  <div class="cartcontrol">
    <transition name="move">//过渡效果
      <div class="iconfont icon-remove_circle_outline" v-if="food.count" @click.stop="updateFoodCount(false)"></div>
    </transition>
    <div class="cart-count" v-if="food.count">{{food.count}}</div>
    <div class="iconfont icon-add_circle" @click.stop="updateFoodCount(true)"></div>
  </div>
</template>

<script>
  export default {
    props: {
      food: Object
    },

    methods: {
      updateFoodCount (isAdd) {
        this.$store.dispatch('updateFoodCount', {isAdd, food: this.food})
      }
    }
  }
</script>

<style lang="stylus" rel="stylesheet/stylus">
  @import "../../common/stylus/mixins.styl"
  .cartcontrol
    font-size: 0
    .cart-decrease
      display: inline-block
      padding: 6px
      line-height: 24px
      font-size: 24px
      color: rgb(0, 160, 220)

    .icon-remove_circle_outline
      display: inline-block
      padding 6px
      line-height 24px
      font-size 24px
      color $green
      &.move-enter-active, &.move-leave-active
        transition all .3s
      &.move-enter, &.move-leave-to
        opacity 0
        transform translateX(15px) rotate(180deg)
    .cart-count
      display: inline-block
      vertical-align: top
      width: 12px
      padding-top: 6px
      line-height: 24px
      text-align: center
      font-size: 10px
      color: rgb(147, 153, 159)
    .icon-add_circle
      display: inline-block
      padding: 6px
      line-height: 24px
      font-size: 24px
      color $green
</style>
//在使用shopGoods中使用
1 引入和定义
import CartControl from '../../../components/CartControl/CartControl.vue'
 components: {
      CartControl
    }
  2使用与传参
  <li class="food-list-hook" v-for="(good, index) in goods" :key="index">
  <div class="cartcontrol-wrapper">
                    <CartControl :food="food"/> 传每一个商品对象
                  </div>
    </li>
    
    
    
**  在store中定义每一个food 的count以及控制**
**mutation-types**
export const INCREMENT_FOOD_COUNT = 'increment_food_count' // 增加food中的count
export const DECREMENT_FOOD_COUNT = 'decrement_food_count' // 减少food中的count
**
    action**
      // 同步更新food中的count值
  updateFoodCount({commit}, {isAdd, food}) {
    if (isAdd) {
      commit(INCREMENT_FOOD_COUNT, {food})
    } else {
      commit(DECREMENT_FOOD_COUNT, {food})
    }
  },
  
**  mutation**
import Vue from 'vue'

  [INCREMENT_FOOD_COUNT](state, {food}) {
    if(!food.count) { // 第一次增加
      // food.count = 1  // 新增属性(没有数据绑定)
      /*
      对象
      属性名
      属性值
       */
      Vue.set(food, 'count', 1) // 让新增的属性也有数据绑定
      // 将food添加到cartFoods中
      state.cartFoods.push(food)
    } else {
      food.count++
    }
  },
  [DECREMENT_FOOD_COUNT](state, {food}) {
    if(food.count) {// 只有有值才去减
      food.count--
      if(food.count===0) {
        // 将food从cartFoods中移除
        state.cartFoods.splice(state.cartFoods.indexOf(food), 1)
      }
    }
  },
  
**  state**
/*
状态对象
 */
export default {
  latitude: 40.10038, // 纬度
  longitude: 116.36867, // 经度
  address: {}, //地址相关信息对象
  categorys: [], // 食品分类数组
  shops: [], // 商家数组
  userInfo: {}, // 用户信息
  goods: [], // 商品列表
  ratings: [], // 商家评价列表
  info: {}, // 商家信息
  cartFoods: [], // 购物车中食物的列表
  searchShops: [], // 搜索得到的商家列表
}

商品详情弹窗组件

<template>
  <div class="food" v-if="isShow">
    <div class="food-content">
      <div class="image-header">
        <img v-lazy="food.image">
        <p class="foodpanel-desc">{{food.info}}</p>
        <div class="back" @click="toggleShow">
          <i class="iconfont icon-arrow_left"></i>
        </div>
      </div>
      <div class="content">
        <h1 class="title">{{food.name}}</h1>
        <div class="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="cartcontrol-wrapper">
          <CartControl :food="food"/>
        </div>
      </div>
    </div>
    <div class="food-cover" @click="toggleShow"></div>
  </div>
</template>


<script>
  import CartControl from '../CartControl/CartControl.vue'

  export default {
    props: {
      food: Object
    },

    data () {
      return {
        isShow: false
      }
    },

    methods: {
      toggleShow () {
        this.isShow = !this.isShow
      }
    },

    components: {
      CartControl
    }
  }
</script>

<style lang="stylus" rel="stylesheet/stylus" scoped>
  @import "../../common/stylus/mixins.styl"

  .food
    position: fixed
    left: 0
    top: 0
    bottom: 48px
    z-index: 101
    width: 100%
    &.fade-enter-active, &.fade-leave-active
      transition opacity .5s
    &.fade-enter, &.fade-leave-to
      opacity 0
    .food-content
      position absolute
      left 50%
      top 50%
      transform translate(-50%, -50%)
      width 80%
      height 65%
      z-index 66
      background #fff
      border-radius 5px
      .image-header
        position: relative
        width: 100%
        height: 0
        padding-top: 100%
        img
          position: absolute
          top: 0
          left: 0
          width: 100%
          height: 100%
        .foodpanel-desc
          font-size 10px
          color #ddd
          letter-spacing 0
          position absolute
          bottom 0
          left 0
          right 0
          padding 0 10px 10px
        .back
          position: absolute
          top: 10px
          left: 0
          .icon-arrow_left
            display: block
            padding: 10px
            font-size: 20px
            color: #fff
      .content
        position: relative
        padding: 18px
        .title
          line-height: 14px
          margin-bottom: 8px
          font-size: 14px
          font-weight: 700
          color: rgb(7, 17, 27)
        .detail
          margin-bottom: 18px
          line-height: 10px
          height: 10px
          font-size: 0
          .sell-count, .rating
            font-size: 10px
            color: rgb(147, 153, 159)
          .sell-count
            margin-right: 12px
        .price
          font-weight: 700
          line-height: 24px
          .now
            margin-right: 8px
            font-size: 14px
            color: rgb(240, 20, 20)
          .old
            text-decoration: line-through
            font-size: 10px
            color: rgb(147, 153, 159)
        .cartcontrol-wrapper
          position: absolute
          right: 12px
          bottom: 12px
        .buy
          position: absolute
          right: 18px
          bottom: 18px
          z-index: 10
          height: 24px
          line-height: 24px
          padding: 0 12px
          box-sizing: border-box
          border-radius: 12px
          font-size: 10px
          color: #fff
          background: rgb(0, 160, 220)
          &.fade-transition
            transition: all 0.2s
            opacity: 1
          &.fade-enter, &.fade-leave
            opacity: 0
    .food-cover
      position absolute
      top 0
      right 0
      bottom -48px
      left 0
      z-index 55
      background-color rgba(0, 0, 0, 0.5)

</style>

父组件中使用

 
<li class="food-item bottom-border-1px" v-for="(food, index) in good.foods"
                  :key="index" @click="showFood(food)">
                <div class="icon">
                  <img width="57" height="57" :src="food.icon">
                </div>
                <div class="content">
                  <h2 class="name">{{food.name}}</h2>
                  <p class="desc">{{food.description}}</p>
                  <div class="extra">
                    <span class="count">月售{{food.sellCount}}份</span>
                    <span>好评率{{food.rating}}%</span>
                  </div>
                  <div class="price">
                    <span class="now">¥{{food.price}}</span>
                    <span class="old" v-if="food.oldPrice">¥{{food.oldPrice}}</span>
                  </div>
                  <div class="cartcontrol-wrapper">
                    <CartControl :food="food"/>
                  </div>
                </div>
              </li>
<Food :food="food" ref="food"/>
import Food from '../../../components/Food/Food.vue'
  data() {
      return {
        food: {}, // 需要显示的food
      }
    },
components: {
      Food
    }
  // 显示点击的food
      showFood (food) {
        // 设置food
        this.food = food
        // 显示food组件 (在父组件中调用子组件对象的方法)
        this.$refs.food.toggleShow()
      }
      

购物车组件

<template>
  <div>
    <div class="shopcart">
      <div class="content">
        <div class="content-left" @click="toggleShow">
          <div class="logo-wrapper">
            <div class="logo" :class="{highlight: totalCount}">
              <i class="iconfont icon-shopping_cart" :class="{highlight: totalCount}"></i>
            </div>
            <div class="num" v-if="totalCount">{{totalCount}}</div>
          </div>
          <div class="price" :class="{highlight: totalCount}">¥{{totalPrice}}</div>
          <div class="desc">另需配送费¥{{info.deliveryPrice}}元</div>
        </div>
        <div class="content-right">
          <div class="pay" :class="payClass">
            {{payText}}
          </div>
        </div>
      </div>
      <transition name="move">
        <div class="shopcart-list" v-show="listShow">
          <div class="list-header">
            <h1 class="title">购物车</h1>
            <span class="empty" @click="clearCart">清空</span>
          </div>
          <div class="list-content">
            <ul>
              <li class="food" v-for="(food, index) in cartFoods" :key="index">
                <span class="name">{{food.name}}</span>
                <div class="price"><span>¥{{food.price}}</span></div>
                <div class="cartcontrol-wrapper">
                  <CartControl :food="food"/>
                </div>
              </li>
            </ul>
          </div>
        </div>
      </transition>

    </div>
    <div class="list-mask" v-show="listShow" @click="toggleShow"></div>
  </div>
</template>

<script>
  import { MessageBox } from 'mint-ui'
  import BScroll from 'better-scroll'
  import {mapState, mapGetters} from 'vuex'
  import CartControl from '../CartControl/CartControl.vue'

  export default {
    data () {
      return {
        isShow: false
      }
    },

    computed: {
      ...mapState(['cartFoods', 'info']),
      ...mapGetters(['totalCount', 'totalPrice']),
      payClass () {
        const {totalPrice} = this
        const {minPrice} = this.info

        return totalPrice>=minPrice ? 'enough' : 'not-enough'
      },
      payText () {
        const {totalPrice} = this
        const {minPrice} = this.info
        if(totalPrice===0) {
          return `¥${minPrice}元起送`
        } else if(totalPrice<minPrice) {
          return `还差¥${minPrice-totalPrice}元起送`
        } else {
          return '结算'
        }
      },

      listShow () {
        // 如果总数量为0, 直接不显示
        if(this.totalCount===0) {
          this.isShow = false
          return false
        }

        if(this.isShow) {
          this.$nextTick(() => {
            // 实现BScroll的实例是一个单例
            if(!this.scroll) {
              this.scroll = new BScroll('.list-content', {
                click: true
              })
            } else {
              this.scroll.refresh() // 让滚动条刷新一下: 重新统计内容的高度
            }

          })
        }

        return this.isShow
      }
    },


    methods: {
      toggleShow () {
        // 只有当总数量大于0时切换
        if(this.totalCount>0) {
          this.isShow = !this.isShow
        }
      },

      clearCart () {
        MessageBox.confirm('确定清空购物车吗?').then(action => {
          this.$store.dispatch('clearCart')
        }, () => {});
      }
    },
    components: {
      CartControl
    }
  }
</script>

<style lang="stylus" rel="stylesheet/stylus" scoped>
  @import "../../common/stylus/mixins.styl"

  .shopcart
    position fixed
    left 0
    bottom 0
    z-index 50
    width 100%
    height 48px
    .content
      display flex
      background #141d27
      font-size 0
      color rgba(255, 255, 255, 0.4)
      .content-left
        flex 1
        .logo-wrapper
          display inline-block
          vertical-align top
          position relative
          top -10px
          margin 0 12px
          padding 6px
          width 56px
          height 56px
          box-sizing border-box
          border-radius 50%
          background #141d27
          .logo
            width 100%
            height 100%
            border-radius 50%
            text-align center
            background #2b343c
            &.highlight
              background $green
            .icon-shopping_cart
              line-height 44px
              font-size 24px
              color #80858a
              &.highlight
                color #fff
          .num
            position absolute
            top 0
            right 0
            width 24px
            height 16px
            line-height 16px
            text-align center
            border-radius 16px
            font-size 9px
            font-weight 700
            color #ffffff
            background rgb(240, 20, 20)
            box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.4)
        .price
          display inline-block
          vertical-align top
          margin-top 5px
          line-height 24px
          padding-right 12px
          box-sizing border-box
          font-size 16px
          font-weight 700
          color #fff
          &.highlight
            color #fff
        .desc
          display inline-block
          vertical-align bottom
          margin-bottom 15px
          margin-left -45px
          font-size 10px
      .content-right
        flex 0 0 105px
        width 105px
        .pay
          height 48px
          line-height 48px
          text-align center
          font-size 12px
          font-weight 700
          color #fff
          &.not-enough
            background #2b333b
          &.enough
            background #00b43c
            color #fff
    .ball-container
      .ball
        position fixed
        left 32px
        bottom 22px
        z-index 200
        transition all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
        .inner
          width 16px
          height 16px
          border-radius 50%
          background $green
          transition all 0.4s linear
    .shopcart-list
      position absolute
      left 0
      top 0
      z-index -1
      width 100%
      transform translateY(-100%)
      &.move-enter-active, &.move-leave-active
        transition transform .3s
      &.move-enter, &.move-leave-to
        transform translateY(0)
      .list-header
        height 40px
        line-height 40px
        padding 0 18px
        background #f3f5f7
        border-bottom 1px solid rgba(7, 17, 27, 0.1)
        .title
          float left
          font-size 14px
          color rgb(7, 17, 27)
        .empty
          float right
          font-size 12px
          color rgb(0, 160, 220)

      .list-content
        padding 0 18px
        max-height 217px
        overflow hidden
        background #fff
        .food
          position relative
          padding 12px 0
          box-sizing border-box
          bottom-border-1px(rgba(7, 17, 27, 0.1))
          .name
            line-height 24px
            font-size 14px
            color rgb(7, 17, 27)
          .price
            position absolute
            right 90px
            bottom 12px
            line-height 24px
            font-size 14px
            font-weight 700
            color rgb(240, 20, 20)
          .cartcontrol-wrapper
            position absolute
            right 0
            bottom 6px

  .list-mask
    position fixed
    top 0
    left 0
    width 100%
    height 100%
    z-index 40
    backdrop-filter blur(10px)
    opacity 1
    background rgba(7, 17, 27, 0.6)
    &.fade-enter-active, &.fade-leave-active
      transition all 0.5s
    &.fade-enter, &.fade-leave-to
      opacity 0
      background rgba(7, 17, 27, 0)
## </style>
## 
## 对应的getters
/*
包含多个基于state的getter计算属性的对象
 */
export default {

  totalCount (state) {
    return state.cartFoods.reduce((preTotal, food) => preTotal + food.count , 0)
  },

  totalPrice (state) {
    return state.cartFoods.reduce((preTotal, food) => preTotal + food.count*food.price , 0)
  },

  positiveSize (state) {
    return state.ratings.reduce((preTotal, rating) => preTotal + (rating.rateType===0?1:0) , 0)
  }
}

**action.js**

 // 同步更新food中的count值
  updateFoodCount({commit}, {isAdd, food}) {
    if (isAdd) {
      commit(INCREMENT_FOOD_COUNT, {food})
    } else {
      commit(DECREMENT_FOOD_COUNT, {food})
    }
  },

  // 同步清空购物车
  clearCart({commit}) {
    commit(CLEAR_CART)
  },
  
**  mutations.js**
    [CLEAR_CART](state) {

    // 清除food中的count
    state.cartFoods.forEach(food => food.count = 0)
    // 移除购物车中所有购物项
    state.cartFoods = []
  },
  
**  state.js**
  export default {
   cartFoods: [], // 购物车中食物的列表
  }

评价和分类组件

<template>
  <div class="ratings" ref="ratings">
    <div class="ratings-content">
      <div class="overview">
        <div class="overview-left">
          <h1 class="score">{{info.score}}</h1>
          <div class="title">综合评分</div>
          <div class="rank">高于周边商家99%</div>
        </div>
        <div class="overview-right">
          <div class="score-wrapper">
            <span class="title">服务态度</span>
            <Star :score="info.serviceScore" :size="36" />
            <span class="score">{{info.serviceScore}}</span>
          </div>
          <div class="score-wrapper">
            <span class="title">商品评分</span>
            <Star :score="info.foodScore" :size="36" />
            <span class="score">{{info.foodScore}}</span>
          </div>
          <div class="delivery-wrapper">
            <span class="title">送达时间</span>
            <span class="delivery">{{info.deliveryTime}}分钟</span>
          </div>
        </div>
      </div>

      <div class="split"></div>

      <div class="ratingselect">
        <div class="rating-type border-1px">
          <span class="block positive" :class="{active: selectType===2}" @click="setSelectType(2)">
            全部<span class="count">{{ratings.length}}</span>
          </span>
          <span class="block positive" :class="{active: selectType===0}" @click="setSelectType(0)">
            满意<span class="count">{{positiveSize}}</span>
          </span>
          <span class="block negative" :class="{active: selectType===1}" @click="setSelectType(1)">
            不满意<span class="count">{{ratings.length-positiveSize}}</span>
          </span>
        </div>
        <div class="switch" :class="{on: onlyShowText}" @click="toggleOnlyShowText">
          <span class="iconfont icon-check_circle"></span>
          <span class="text">只看有内容的评价</span>
        </div>
      </div>

      <div class="rating-wrapper">
        <ul>
          <li class="rating-item" v-for="(rating, index) in filterRatings" :key="index">
            <div class="avatar">
              <img width="28" height="28" :src="rating.avatar">
            </div>
            <div class="content">
              <h1 class="name">{{rating.username}}</h1>
              <div class="star-wrapper">
                <Star :score="rating.score" :size="24" />
                <span class="delivery">{{rating.deliveryTime}}</span>
              </div>
              <p class="text">{{rating.text}}</p>
              <div class="recommend">
                <span class="iconfont" :class="rating.rateType===0 ? 'icon-thumb_up' : 'icon-thumb_down'"></span>
                <span class="item" v-for="(item, index) in rating.recommend" :key="index">{{item}}</span>
              </div>
              <div class="time">{{rating.rateTime | date-format}}</div>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>


<script>
  import BScroll from 'better-scroll'
  import {mapState, mapGetters} from 'vuex'
  import Star from '../../../components/Star/Star.vue'

  export default {

    data () {
      return {
        onlyShowText: true, // 是否只显示有文本的
        selectType: 2 , // 选择的评价类型: 0满意, 1不满意, 2全部
      }
    },
    mounted () {
      this.$store.dispatch('getShopRatings', () => {
        this.$nextTick(() => {
          new BScroll(this.$refs.ratings, {
            click: true
          })
        })
      })
    },

    computed: {
      ...mapState(['info', 'ratings']),
      ...mapGetters(['positiveSize']),

      filterRatings () {
        // 得到相关的数据
        const {ratings, onlyShowText, selectType} = this

        // 产生一个过滤新数组
        return ratings.filter(rating => {
          const {rateType, text} = rating
          /*
            条件1:
                selectType: 0/1/2
                rateType: 0/1
                selectType===2 || selectType===rateType
            条件2
                onlyShowText: true/false
                text: 有值/没值
                !onlyShowText || text.length>0
           */
          return (selectType===2 || selectType===rateType) && (!onlyShowText || text.length>0)
        })
      }
    },

    methods: {
      setSelectType (selectType) {
        this.selectType = selectType
      },
      toggleOnlyShowText () {
        this.onlyShowText = !this.onlyShowText
      }
    },

    components: {
      Star
    },
  }
</script>

<style lang="stylus" rel="stylesheet/stylus">
  @import "../../../common/stylus/mixins.styl"

  .ratings
    position: absolute
    top: 195px
    bottom: 0
    left: 0
    width: 100%
    overflow: hidden
    background: #fff
    .overview
      display: flex
      padding: 18px 0
      .overview-left
        flex: 0 0 137px
        padding: 6px 0
        width: 137px
        border-right: 1px solid rgba(7, 17, 27, 0.1)
        text-align: center
        @media only screen and (max-width: 320px)
          flex: 0 0 120px
          width: 120px
        .score
          margin-bottom: 6px
          line-height: 28px
          font-size: 24px
          color: rgb(255, 153, 0)
        .title
          margin-bottom: 8px
          line-height: 12px
          font-size: 12px
          color: rgb(7, 17, 27)
        .rank
          line-height: 10px
          font-size: 10px
          color: rgb(147, 153, 159)
      .overview-right
        flex: 1
        padding: 6px 0 6px 24px
        @media only screen and (max-width: 320px)
          padding-left: 6px
        .score-wrapper
          margin-bottom: 8px
          font-size: 0
          .title
            display: inline-block
            line-height: 18px
            vertical-align: top
            font-size: 12px
            color: rgb(7, 17, 27)
          .star
            display: inline-block
            margin: 0 12px
            vertical-align: top
          .score
            display: inline-block
            line-height: 18px
            vertical-align: top
            font-size: 12px
            color: rgb(255, 153, 0)
        .delivery-wrapper
          font-size: 0
          .title
            line-height: 18px
            font-size: 12px
            color: rgb(7, 17, 27)
          .delivery
            margin-left: 12px
            font-size: 12px
            color: rgb(147, 153, 159)
    .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
    .ratingselect
      .rating-type
        padding: 18px 0
        margin: 0 18px
        border-1px(rgba(7, 17, 27, 0.1))
        font-size: 0
        .block
          display: inline-block
          padding: 8px 12px
          margin-right: 8px
          line-height: 16px
          border-radius: 1px
          font-size: 12px
          color: rgb(77, 85, 93)
          background: rgba(77, 85, 93, 0.2)
          &.active
            background: $green
            color: #fff
          .count
            margin-left: 2px
            font-size: 8px
      .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
        &.on
          .icon-check_circle
            color: $green
        .icon-check_circle
          display: inline-block
          vertical-align: top
          margin-right: 4px
          font-size: 24px
        .text
          display: inline-block
          vertical-align: top
          font-size: 12px
    .rating-wrapper
      padding: 0 18px
      .rating-item
        display: flex
        padding: 18px 0
        bottom-border-1px(rgba(7, 17, 27, 0.1))
        .avatar
          flex: 0 0 28px
          width: 28px
          margin-right: 12px
          img
            border-radius: 50%
        .content
          position: relative
          flex: 1
          .name
            margin-bottom: 4px
            line-height: 12px
            font-size: 10px
            color: rgb(7, 17, 27)
          .star-wrapper
            margin-bottom: 6px
            font-size: 0
            .star
              display: inline-block
              margin-right: 6px
              vertical-align: top
            .delivery
              display: inline-block
              vertical-align: top
              line-height: 12px
              font-size: 10px
              color: rgb(147, 153, 159)
          .text
            margin-bottom: 8px
            line-height: 18px
            color: rgb(7, 17, 27)
            font-size: 12px
          .recommend
            line-height: 16px
            font-size: 0
            .icon-thumb_up, .icon-thumb_down, .item
              display: inline-block
              margin: 0 8px 4px 0
              font-size: 9px
            .icon-thumb_up
              color: $yellow
            .icon-thumb_down
              color: rgb(147, 153, 159)

            .item
              padding: 0 6px
              border: 1px solid rgba(7, 17, 27, 0.1)
              border-radius: 1px
              color: rgb(147, 153, 159)
              background: #fff
          .time
            position: absolute
            top: 0
            right: 0
            line-height: 12px
            font-size: 10px
            color: rgb(147, 153, 159)
</style>

getters.js
 positiveSize (state) {
    return state.ratings.reduce((preTotal, rating) => preTotal + (rating.rateType===0?1:0) , 0)
  }
  
  
  actions
   // 异步获取商家评价列表
  async getShopRatings({commit}, callback) {
    const result = await reqShopRatings()
    if (result.code === 0) {
      const ratings = result.data
      commit(RECEIVE_RATINGS, {ratings})
      // 数据更新了, 通知一下组件
      callback && callback()
    }
  },
  
  mutations.js
   [RECEIVE_RATINGS](state, {ratings}) {
    state.ratings = ratings
  },
  
  state.js
    ratings: [], // 商家评价列表

shopinfo组件以及在子组件下刷新数据没回来的处理

<template>
  <div class="shop-info">
    <div class="info-content">
      <section class="section">
        <h3 class="section-title">配送信息</h3>
        <div class="delivery">
          <div>
            <span class="delivery-icon">{{info.description}}</span>
            <span>由商家配送提供配送,约{{info.deliveryTime}}分钟送达,距离{{info.distance}}</span>
          </div>
          <div class="delivery-money">配送费¥{{info.deliveryPrice}}</div>
        </div>
      </section>

      <div class="split"></div>

      <section class="section">
        <h3 class="section-title">活动与服务</h3>
        <div class="activity">
          <div class="activity-item" v-for="(support, index) in info.supports" :key="index"
              :class="supportClasses[support.type]">
            <span class="content-tag">
              <span class="mini-tag">{{support.name}}</span>
            </span>
            <span class="activity-content">{{support.content}}</span>
          </div>
        </div>
      </section>

      <div class="split"></div>

      <section class="section">
        <h3 class="section-title">商家实景</h3>
        <div class="pic-wrapper">
          <ul class="pic-list" ref="picsUl">
            <li class="pic-item" v-for="(pic, index) in info.pics" :key="index">
              <img width="120" height="90" :src="pic"/>
            </li>
          </ul>
        </div>
      </section>

      <div class="split"></div>

      <section class="section">
        <h3 class="section-title">商家信息</h3>
        <ul class="detail">
          <li><span class="bold">品类</span> <span>{{info.category}}</span></li>
          <li><span class="bold">商家电话</span> <span>{{info.phone}}</span></li>
          <li><span class="bold">地址</span> <span>{{info.address}}</span></li>
          <li><span class="bold">营业时间</span> <span>{{info.workTime}}</span></li>
        </ul>
      </section>
    </div>
  </div>
</template>

<script>
  import BScroll from 'better-scroll'
  import {mapState} from 'vuex'
  export default {

    data () {
      return {
        supportClasses: ['activity-green', 'activity-red', 'activity-orange']
      }
    },
    computed: {
      ...mapState(['info'])
    },

    mounted () {
      // 如果数据还没有, 直接结束
      if(!this.info.pics) {
        return
      }

      // 数据有了, 可以创建BScroll对象形成滑动
      this._initScroll()
    },

    methods: {
      _initScroll () {
        new BScroll('.shop-info')
        // 动态计算ul的宽度
        const ul = this.$refs.picsUl
        const liWidth = 120
        const space = 6
        const count = this.info.pics.length
        ul.style.width = (liWidth + space) * count -space + 'px'

        new BScroll('.pic-wrapper', {
          scrollX: true // 水平滑动
        })
      }
    },

    watch: {
      info () {// 刷新流程--> 更新数据
        this.$nextTick(() => {
          this._initScroll()
        })
      }
    }
  }
</script>

<style lang="stylus" rel="stylesheet/stylus">
  @import "../../../common/stylus/mixins.styl"

  .shop-info
    position: absolute
    top: 195px
    bottom: 0
    left: 0
    width: 100%
    background: #fff;
    overflow: hidden
    .section
      padding 16px 14px 14px
      font-size 16px
      background-color #fff
      color #666
      border-bottom 1px solid #eee
      position relative
      .section-title
        color #000
        font-weight 700
        line-height 16px
        > .iconfont
          float right
          color #ccc
      .delivery
        margin-top 16px
        font-size 13px
        line-height 18px
        .delivery-icon
          width 55px
          font-size 11px
          margin-right 10px
          display inline-block
          text-align center
          color #fff
          background-color #0097ff
          padding 1px 0
          border-radius 4px
        .delivery-money
          margin-top 5px

      .activity
        margin-top 16px
        .activity-item
          margin-bottom 12px
          display flex
          font-size 13px
          align-items center
          &.activity-green
            .content-tag
              background-color rgb(112, 188, 70)
          &.activity-red
            .content-tag
              background-color rgb(240, 115, 115)
          &.activity-orange
            .content-tag
              background-color: rgb(241, 136, 79)
          .content-tag
            display inline-block
            border-radius 2px
            width 36px
            height: 18px
            margin-right 10px
            color #fff
            font-style normal
            position relative
            .mini-tag
              position absolute
              left 0
              top 0
              right -100%
              bottom -100%
              font-size 24px
              transform scale(.5)
              transform-origin 0 0
              display flex
              align-items center
              justify-content center
      .pic-wrapper
        width: 100%
        overflow: hidden
        white-space: nowrap
        margin-top 16px
        .pic-list
          font-size: 0
          .pic-item
            display: inline-block
            margin-right: 6px
            width: 120px
            height: 90px
            &:last-child
              margin: 0
      .detail
        margin-bottom -16px
        > li
          display flex
          justify-content space-between
          align-items center
          margin-right -10px
          padding 16px 12px 16px 0
          line-height 16px
          bottom-border-1px(#ddd)
          font-size 13px
          > .bold
            font-weight 700
            color #333
          &:last-child
            border-none()


    .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>
# search组件

<template>
<section class="search">

<HeaderTop title="搜索"/>
<form class="search_form" @submit.prevent="search">
  <input type="search" placeholder="请输入商家名称" class="search_input" v-model="keyword">
  <input type="submit" class="search_submit">
</form>
<section class="list" v-if="!noSearchShops">
  <ul class="list_container">
    <!--:to="'/shop?id='+item.id"-->
    <router-link :to="{path:'/shop', query:{id:item.id}}" tag="li"
                 v-for="item in searchShops" :key="item.id" class="list_li">
      <section class="item_left">
        <img :src="imgBaseUrl + item.image_path" class="restaurant_img">
      </section>
      <section class="item_right">
        <div class="item_right_text">
          <p>
            <span>{{item.name}}</span>
          </p>
          <p>月售 {{item.month_sales||item.recent_order_num}} 单</p>
          <p>{{item.delivery_fee||item.float_minimum_order_amount}} 元起送 / 距离{{item.distance}}</p>
        </div>
      </section>
    </router-link>
  </ul>
</section>

<div class="search_none" v-else>很抱歉!无搜索结果</div>

</section>
</template>

<script>
import {mapState} from 'vuex'
import HeaderTop from '../../components/HeaderTop/HeaderTop.vue'
export default {

data () {
  return {
    keyword: '',
    imgBaseUrl: 'http://cangdu.org:8001/img/',
    noSearchShops: false
  }
},

computed: {
  ...mapState(['searchShops'])
},

methods: {
  search () {
    // 得到搜索关键字
    const keyword = this.keyword.trim()
    // 进行搜索
    if(keyword) {
      this.$store.dispatch('searchShops', keyword)
    }
  }
},

watch: {
  searchShops (value) {
    if(!value.length) { // 没有数据
      this.noSearchShops = true
    } else {// 有数据
      this.noSearchShops = false
    }
  }
},

components: {
  HeaderTop
}

}
</script>

<style lang="stylus" rel="stylesheet/stylus" scoped>
@import "../../common/stylus/mixins.styl"
.search

width 100%
height 100%
overflow hidden
.search_form
  clearFix()
  margin-top 45px
  background-color #fff
  padding 12px 8px
  input
    height 35px
    padding 0 4px
    border-radius 2px
    font-weight bold
    outline none
    &.search_input
      float left
      width 79%
      border 4px solid #f2f2f2
      font-size 14px
      color #333
      background-color #f2f2f2
    &.search_submit
      float right
      width 18%
      border 4px solid #02a774
      font-size 16px
      color #fff
      background-color #02a774

.list
  .list_container
    background-color: #fff;
    .list_li
      display: flex;
      justify-content: center;
      padding: 10px
      border-bottom: 1px solid $bc;
      .item_left
        margin-right: 10px
        .restaurant_img
          width 50px
          height 50px
          display block
      .item_right
        font-size 12px
        flex 1
        .item_right_text
          p
            line-height 12px
            margin-bottom 6px
            &:last-child
              margin-bottom 0
.search_none
  margin: 0 auto
  color: #333
  background-color: #fff
  text-align: center
  margin-top: 0.125rem

</style>


HappyCodingTop
526 声望847 粉丝

Talk is cheap, show the code!!