【饿了么】—— Vue2.0高仿饿了么核心模块&移动端Web App项目爬坑(三)

前言:接着上一篇项目总结,这一篇是学习过程记录的最后一篇,这里会梳理:评论组件、商家组件、优化、打包、相关资料链接。项目github地址:https://github.com/66Web/ljq_eleme,欢迎Star。


ratings seller
一、评论组件-ratings

       评论组件主要分为三块

  • 评分信息-overview
  • 评论选择-ratingselect
  • 评论详细信息

       评分信息部分

  • 左侧评分
  1. 布局Dom
    <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>
         </div>
         <split></split>
  2. CSS样式

    .overview
            display flex
            padding 18px 0 18px 18px
            .overview-left
              padding-bottom 6px 0
              flex 0 0 137px
              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 110px
                width 110px
              .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
    View Code
  3. seller数据:App.vue中的routerview进行传递,在rating组件中使用props进行接收,这样才可以在模板中直接使用seller.XXX数据
     props: {
        seller: {
          type: Object
        }
      }
  • 右侧star组件+商品评分+送达时间
  1. 布局Dom
    <div class="overview">
            <div class="overview-left">
              ...
            </div>
            <div class="overview-right">
              <div class="score-wrapper">
                <span class="title">服务态度</span>
                <star :size="36" :score="seller.serviceScore"></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="delivery">{{seller.deliveryTime}}分钟</span>
              </div>
            </div>
          </div>
  2. CSS样式:

            .overview-right
              flex 1
              padding 6px 0 6px 24px
              @media only screen and (max-width 320px)
                padding-left 6px
              .score-wrapper
                line-height 18px
                margin-top 8px
                font-size 0
                .title 
                  display inline-block
                  vertical-align top
                  line-height 18px
                  font-size 12px
                  color rgb(7, 17, 27)
                .star
                  display inline-block
                  vertical-align top
                  margin 0 12px
                .score
                  display inline-block
                  vertical-align top
                  line-height 18px
                  font-size 12px
                  color rgb(255, 153, 0)
              .delivery-wrapper
                font-size 0 
                .title  //span文字和文字之间默认是垂直居中的,可以不用加display vertical-align  
                  display inline-block
                  vertical-align top
                  line-height 18px
                  font-size 12px
                  color rgb(7, 17, 27)
                .delivery
                  display inline-block
                  margin-left 12px
                  vertical-align top
                  line-height 18px
                  font-size 12px
                  color rgb(147, 153, 159)
    View Code
  3. 坑:视口宽度不够宽时,右侧部分过长会出现折行。解决:添加一个mediea Query媒体查询

    .overview-left
         padding-bottom: 6px 0
         flex: 0 0 137px
          137px // 防止出现兼容性问题
         border-right: 1px solid rgba(7,17,27,0.1)
         text-align: center
         @media only screen and (max-width 320px)
             flex: 0 0 110px
              110px
    .overview-right
         flex 1
         padding: 6px 0 6px 24px
         @media only screen and (max-width 320px)
             padding-left: 6px
  • 页面很长,需要引用better-scroll
  1. 同时,已经做好的分割区split组件、星星star组件、评论选择ratingselect组件、时间戳转换等也都需要引用
    import star from '@/components/star/star'
    import BScroll from 'better-scroll';
    import split from '@/components/split/split'
    import ratingselect from '@/components/ratingselect/ratingselect'
    import {formatDate} from '@/common/js/date'
    <template>
      <div class="ratings" ref="ratings"> <!-- ratings-content大于ratings的时候出现滚动 -->
        <div class="ratings-content">
  2. 要实现滚动,像good组件一样,需要固定视口的高度,将其定位绝对定位,top为header组件的高度

    .ratings
          position: absolute 
          top: 174px
          bottom: 0
          left: 0
           100%
          overflow: hidden

       评论选择部分 

  • 使用引用并注册好的split组件和ratingselect组件
    <split></split>
    <ratingselect @increment="incrementTotal" 
                  :select-type="selectType" 
                  :only-content="onlyContent" 
                  :ratings="ratings">
    </ratingselect>

      评论详细信息

  • 同商品组件,在created()函数中拿到ratings的API数据,将得到的ratings传到ratings的组件中
    const ERR_OK = 0;
    created () {
        this.$http.get('/api/ratings')          
              .then((res) => { 
                 res = res.body;
                 if (res.errno === ERR_OK) {
                      this.ratings = res.data;
                      // console.log(this.ratings)
                      this.$nextTick(() => {
                         this.scroll = new BScroll(this.$refs.ratings, {
                             click: true
                         })
                      });
                 }               
              }
        )
  • 拿到数据之后在raring组件中填充html中的DOM数据

    <div class="rating-wrapper">
            <ul>
              <li v-for="rating in ratings" :key="rating.id" class="rating-item" v-show="needShow(rating.rateType, rating.text)">
                <div class="avatar">
                  <img :src="rating.avatar" width="28px" height="28px">
                </div>
                <div class="content">
                  <h1 class="name">{{rating.username}}</h1>
                  <div class="star-wrapper">
                    <star :size="24" :score="rating.score"></star>
                    <span class="delivery" v-show="rating.deliveryTime">
                      {{rating.deliveryTime}}
                    </span>
                  </div>
                  <p class="text">{{rating.text}}</p>
                  <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <!-- 赞或踩和相关推荐 -->
                    <i class="icon-thumb_up"></i>
                    <span class="item" v-for="item in rating.recommend" :key="item.id">{{item}}</span>
                  </div>
                  <div class="time">
                     {{rating.rateTime | formatDate}}
                  </div>
                </div>
              </li>
            </ul>
          </div>
    View Code
  • CSS样式

          .rating-wrapper
            padding 0 18px
            .rating-item
              display flex
              padding 18px 0
              border-1px(rgba(1, 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-weight 700
                  font-size 10px
                  color rgb(7, 17, 27)
                .star-wrapper
                  margin-bottom 6px
                  font-size 0
                  .star
                    display inline-block
                    margin-right 16px
                    vertical-align top
                  .delivery
                    display inline-block
                    vertical-align top
                    font-size 10px
                    line-height 12px
                    color rgb(147, 153, 159)
                .text
                  line-height 18px
                  color rgb(7, 17, 27)
                  font-size 12px
                  margin-bottom 8px
                .recommend
                  line-height 16px
                  font-size 0
                  .icon-thumb_up, .item
                    display inline-block
                    margin 0 8px 4px 0
                    font-size 9px
                  .icon-thumb_up
                    color rgb(0, 160, 220)
                  .item
                    padding 0 6px
                    border 1px solid rgba(7, 17, 27, 0.1)
                    border-radius 1px
                    color rgb(147, 153, 159)
                    background #fffff
                .time
                  position absolute
                  top 0
                  right 0
                  line-height 12px
                  font-size 10px
                  color rgb(147, 153, 159)
    View Code
  • 绑定better-scroll,使评论列表部分可以滚动

  1.   拿到DOM数据,ref="ratings",将better-scroll初始化时机写在created函数拿到api数据之后
二、商家组件-seller

       基础操作

  • 接收传递进来的seller数据
     props: {
        //APP.vue的routerview中已经将seller传进来了,这里只需要接收就好
        seller: {
          type: Object
        }
      }
  • 布局DOM

    <div class="overview">
            <h1 class="title">{{seller.name}}</h1>
            <div class="desc border-1px">
              <star :size="36" :score="seller.score"></star>
              <span class="text">({{seller.ratingCount}})</span>
              <span class="text">月售{{seller.sellCount}}单</span>
            </div>
            <ul class="remark">
              <li class="block">
                <h2>起送价</h2>
                <div class="content">
                  <span class="stress">{{seller.minPrice}}</span></div>
              </li>
              <li class="block">
                <h2>商家配送</h2>
                <div class="content">
                  <span class="stress">{{seller.deliveryPrice}}</span></div>
              </li>
              <li class="block">
                <h2>平均配送时间</h2>
                <div class="content">
                  <span class="stress">{{seller.deliveryTime}}</span></div>
              </li>
            </ul>
            <div class="favorite"  @click="toggleFavorite($event)">
              <i class="icon-favorite" 
                 :class="{'active':favorite}"></i> <!-- 对应是否收藏两种样式-->
              <span>{{favoriteText}}</span> <!-- 有没有选中对应不同的文本,所以这里要绑定一个变量,放到data中 -->
            </div>
          </div>
    View Code
  • CSS样式

    .seller
            position: absolute 
            top: 174px
            bottom: 0
            left: 0
             100%
            overflow: hidden
            .overview
              padding: 18px
              position: relative
              .title
                margin-bottom: 8px
                line-height: 14px
                color: rgb(7, 17, 27)
                font-size: 14px
              .desc
                padding-bottom: 18px
                font-size: 0
                border-1px(rgba(7, 17, 27, 0.1))
                &:before
                   display: none
                .star
                  display: inline-block
                  vertical-align: top
                  margin-right: 8px
                .text
                  display: inline-block
                  vertical-align: top
                  margin-right: 12px
                  line-height: 18px // 不能为父元素设置line-heigth,否则组件会被撑高
                  font-size: 10px
                  color: rgb(77, 85, 93)
              .remark
                display: flex
                padding-top: 18px
                .block
                  flex: 1
                  text-align: center
                  border-right: 1px solid rgba(7, 17, 27, 0.1)
                  &:last-child
                    border: none
                  h2
                    margin-bottom: 4px
                    line-height: 10px
                    font-size: 10px
                    color: rgb(147, 153, 149)
                  .content
                    line-height: 24px
                    font-size: 10px
                    color: rgb(7, 17, 27)
                    .stress
                      font-size: 24px
    View Code

       公告与活动部分

  • 先添加一个split组件,再添加内容,同时不要忘记把图片拷贝过来
  1. 布局DOM
    <div class="bulletin">
            <h1 class="title">公告与活动</h1>
            <div class="content-wrapper border-1px">
              <p class="content">{{seller.bulletin}}</p>
            </div>
            <ul v-if="seller.supports" class="supports">
                <li class="support-item border-1px" 
    v-for
    ="(item,index) in seller.supports"
    :key
    ="(item.id,index.id)"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split>

    其中:图标icon  动态绑定class时,使用classMap,在created()中定义,通过获取索引值一一对应,同header.vue组件中一样

     created() {
        this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
     }
  2. CSS样式
    .bulletin
                padding: 18px 18px 0 18px
                .title
                  margin-bottom: 8px
                  line-height: 14px
                  color: rgb(7, 17, 27)
                  font-size: 14px
                .content-wrapper
                  padding: 0 12px 16px 1px
                  border-1px(rgba(7, 17, 27, 0.1))
                  .content
                    line-height: 24px
                    font-size: 12px 
                    color: rgb(240, 20, 20)
                .supports
                  .support-item
                    padding: 16px 12px
                    border-1px(rgba(7, 17, 27, 0.1))
                    font-size 0
                    &:last-child
                      border-none()
                    .icon
                      display inline-block
                      width 16px
                      height 16px
                      vertical-align top
                      margin-right 6px
                      background-size 16px 16px
                      background-repeat no-repeat
                      &.decrease
                        bg-image('decrease_4')
                      &.discount
                        bg-image('discount_4')
                      &.guarantee
                        bg-image('guarantee_4')
                      &.invoice
                        bg-image('invoice_4')
                      &.special
                        bg-image('special_4')
                    .text
                      display inline-block
                      font-size 12px
                      line-height 16px
                      color rgb(7, 17, 27)
    View Code

       使用BScroll

  • 页面很长,需要引用BScroll
  1. 坑:初始化BScroll语句放在created()中,但是不起作用。
  2. 原因:seller是异步获取的,但是我们的内容都是靠seller里的数据撑开的,所以一开始内容肯定是小于我我们定义的wrapper的,所以没有被撑开
  3. 解决:将其放入watch:{} 中可以监测到seller的变化,将初始化语句写成一个方法,在watch中进行调用
     methods: {
         _initScroll() {
          this.$nextTick(() => {
              if (!this.scroll) {
                this.scroll = new BScroll(this.$refs.seller, {click: true});
              }else{
                this.scroll.refresh();
              }
          })
        }
     watch: {
        'seller'() {  //观测seller数据的更新,并且执行更新后的操作
          this._initScroll();
          this._initPics();
        }
      },
     created() {
        this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
    
        this._initScroll();
        this._initPics();
     }
  4. 坑:之前的情况是切换之后不能滚动,现在的新问题是一开始(没切换界面之前)就不能滚动了,切换之后就可以滚动了;
  5. 原因:created()的执行时机要先于watch中的seller,然后我们在执行seller中的initScroll的时候就会发现BScroll已经被初始化了,所以initScroll失效,即使在watch中观察到变化也只能什么都不做

  6. 解决:一定要为初始化函数_initScroll()和this._initPics()中的nextTick()下的添加if-else语句,对BScroll进行刷新,完成

  • 商家实景区块 -- 横向滚动
  1. 添加图片,设置样式,横向排列
    <div class="pics">
           <h1 class="title">商家实景</h1>
            <div class="pic-wrapper" ref="picWrapper">
              <ul class="pic-list" ref="picList">
                <li class="pic-item" v-for="pic in seller.pics" :key="pic.id">
                  <img :src="pic" width="120" height="90">
                </li>
              </ul>
            </div>
    </div>
    <split></split>

    CSS样式:

     .pics
           padding: 18px
           .title
                margin-bottom: 12px
                line-height: 14px
                color: rgb(7, 17, 27)
                font-size: 14px
                .pic-wrapper
                   100%
                  overflow: hidden
                  white-space: nowrap /*不产生折行*/
                  .pic-list
                    font-size: 0
                    .pic-item
                      display: inline-block
                      margin-right: 6px
                       120px
                      height: 90px
                      &:last-child
                        margin: 0
    View Code
  2. 原理: pic-wrapper是固定宽度的视口的大小,当里面的ul超过视口宽度的时候就会出现滚动
  3. 注意:ul是外层的宽度,并不是真实的li撑开的宽度
  4. 实现:使用BScroll实现滚动,添加_initPic()方法,并把它添加到watch和create()中
    _initPics() {
          if(this.seller.pics) {
             let picWidth = 120;
             let margin = 6;
             let width = (picWidth + margin)*this.seller.pics.length - margin;//计算ul的宽度
             this.$nextTick(() => {
                  this.$refs.picList.style.width = width + 'px';//设置ul宽度,不要忘记单位
                  if (!this.picScroll) {
                     this.picScroll = new BScroll(this.$refs.picWrapper, {
                          scrollX: true,//表示横向滚动
                          eventPassthrough:'vertical'//横向滚动图片的时候忽略纵向的滚动
                     });
                  }else{
                    this.scroll.refresh();
                  }
              })
            
          }
        }

       收藏商家

  • 收藏按钮:设置:active样式(红,白)和字体的变化(收藏和未收藏)
    <div class="favorite"  @click="toggleFavorite($event)">
         <i class="icon-favorite" :class="{'active':favorite}"></i> <!-- 对应是否收藏两种样式-->
         <span>{{favoriteText}}</span> <!-- 有没有选中对应不同的文本,所以这里要绑定一个变量,放到data中 -->
    </div>
  • favorite是一个变量,在data里观测,使用computed定义favoriteText()改变并返回变量
    data() {
        return {
         // favorite: false, //默认没有被收藏,从localStorge中取读取,不是一个默认值了
          favorite: (() => {
           return loadFromlLocal(this.seller.id, 'favorite', false);
         })()
        };
    },
    computed: {
        favoriteText() {
           return this.favorite ? '已收藏' : '收藏'; 
        }
    }
  • CSS样式

    .favorite
         position: absolute
         right: 11px
         top: 18px
          50px
         text-align: center
         .icon-favorite
             display: block
             margin-bottom: 4px
             line-height: 24px
             font-size: 24px
              50px
             color: #d4d6d9
             &.active
                   color: rgb(240,20,20)
             .text
                    line-height: 10px
                    font-size: 10px
                    color: rgb(77,85,93)       
    View Code
  • 添加点击事件,methods中定义toggleFavorite()方法

     toggleFavorite(event) {
            if (!event._constructed) {
              return;
            }
            this.favorite = !this.favorite;
            //这样写取法区分商家id,不同商家的状态一样
            //localStorage.favorite = this.favorite;
            saveToLocal(this.seller.id, 'favorite', this.favorite);
     },
  • 保存收藏状态

  1. 解析url中商家id数据为Object对象:每一个商家都有一个唯一的id,这个id存在url中,所以创建util.js,封装一个函数,将url解析成对象的模式
    /**
     * 解析url参数
     * Created by yi on 2016-12-28.
     * @return Object {id:12334}
     */
    export function urlParse() {
      let url = window.location.search;
      let obj = {};
      let reg = /[?&][^?&]+=[^?&]]+/g;
      let arr = url.match(reg);
      // ['?id=123454','&a=b']
     
      if (arr) {
        arr.forEach((item) => {
          let tempArr = item.substring(1).split('=');// 先分割取到id=123454,之后用=号分开
          let key = tempArr[0];
          let val = tempArr[1];
          obj[key] = val;
        });
      }
      // return obj;
      return {id: 123123};
    };
  2. 在App.vue组件中引入urlParse,并在data中获取data,通过扩展对象在data.json文件中存入data
    import {urlParse} from './common/js/util.js'
    
    data() {
         return {
           seller:{
             id: (() => {
               let queryParam = urlParse();
              //  console.log(queryParam)
               return queryParam.id;
             })()
           }
         }
       },
       created: function() {
          this.$http.get('/api/seller?id=' + this.seller.id)          
              .then((res) => { 
                 res = res.body;
                 if (res.errno === ERR_OK) {
                      this.seller = res.data;
                      // console.log(this.seller)
                      this.seller = Object.assign({}, this.seller, res.data);//扩展对象 添加其它属性--id
                 }               
              }, (err) => { 
    
              })
       }
  3. 刷新之后,收藏样式就会消失:创建store.js实现数据的存取,专门存取不同商家的id,通过唯一id,将收藏的信息添加到localStorge中

    //savaToLocal(this.seller.id, 'favorite', this.favorite);存取
    export function saveToLocal(id, key, value) { //存储到localStorge
      let seller = window.localStorage.__seller__;
      if (!seller) { //没有seller的时候,初始化,定义一个seller对象,并给他设定一个id
        seller = {};
        seller[id] = {}; // 每个id下都是一个单独的obj
      } else {
        seller = JSON.parse(seller); // JSON 字符串转换为对象
        if (!seller[id]) { //判断是否有当前这个商家
          seller[id] = {};
        }
      }
      seller[id][key] = value; // 将key和value存到id这个对象的下边
      //将一个JavaScript值(对象或者数组)转换为一个 JSON字符串
      window.localStorage.__seller__ = JSON.stringify(seller);
    }
    //loadFromlLocal(this.seller.id, 'favorite', false);读取
    export function loadFromlLocal(id, key, def) { //读取,读不到的时候传入一个default变量
      let seller = window.localStorage.__seller__;
      if (!seller) {
        return def;
      }
      seller = JSON.parse(seller)[id]; // 取到这个商家下所有的对象
      if (!seller) {
        return def;
      }
      let ret = seller[key];
      return ret || def;
    }
  4. seller.vue中引入,并在data和toggleFavorite()中使用这两个方法:

    import {saveToLocal, loadFromlLocal} from 'common/js/store.js';
    data() {
        return {
         // favorite: false, //默认没有被收藏,从localStorge中取读取,不是一个默认值了
          favorite: (() => {
           return loadFromlLocal(this.seller.id, 'favorite', false);
         })()
        };
      }
    toggleFavorite(event) {
       if (!event._constructed) {
                  return;
       }
       this.favorite = !this.favorite;
       //这样写取法区分商家id,不同商家的状态一样
       //localStorage.favorite = this.favorite;
       saveToLocal(this.seller.id, 'favorite', this.favorite);
    },
三、优化&打包

       优化

  • 问题:切换界面时会闪现
  • 原因:界面被重新渲染了,生命周期函数被重新执行了一遍
  • 优化:切换组件的时候,组件之前的状态也能被保留
  • 解决:vue中提供 vue-router切换组件保留的功能内置组件<keepalive>,在App.vue中更改为
    <keep-alive>
          <router-view :seller="seller"></router-view>
    </keep-alive>

       打包

  • vue-cli 项目打包构建的结果就是根目录下会多出一个dist文件夹:存储编译后的文件
    npm run build
四、相关资料链接

       Vue.js官网https://vuejs.org.cn/

       Vue-cli: https://github.com/vuejs/vue-cli

       Vue-resource: https://github.com/vuejs/vue-resource

       Vue-router: https://github.com/vuejs/vue-router

       better-scrollhttp://npm.taobao.org/package/better-scroll

       webpack官网https://www.webpackjs.com/

       Stylus中文文档https://www.zhangxinxu.com/jq/stylus/

       es6入门学习http://es6.ruanyifeng.com/

       eslint规则http://eslint.org/docs/rules/

       设备像素比https://www.zhangxinxu.com/wordpress/2012/08/window-devicepixelratio/

       Flex布局http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

       贝塞尔曲线测试http://cubic-bezier.com/#.17,.67,.83,.67


注:项目来自慕课网

原文地址:https://www.cnblogs.com/ljq66/p/10012878.html