Vue.js仿饿了么外卖App--(2)头部相关的组件的实现

本篇文章主要实现的是App.vue组件、头部组件header、蒙层组件detal和Modal以及路由切换router

文章目录

    • 一、App.vue组件
        • 1、重点说明
        • 2、具体实现
            • (1)、整体布局
            • (2)、数据获取
        • 3、源码
    • 二、header.vue组件
        • 1、重点说明
        • 2、具体实现
            • (1)、整体布局
            • (2)、父子组件传值
            • (3)、商家头像展示
            • (4)、商家内容展示
            • (5)、字体图标的使用
        • 3、源码
    • 三、蒙层组件
        • 1、detal组件
        • 2、Modal组件
            • (1)、整体布局
        • 3、蒙层的展示和隐藏
        • 4、源码
    • 四、路由切换

一、App.vue组件

1、重点说明

App组件是整个项目的大的组件,其中可以划分为头部组件、导航栏切换、以及对应的导航栏内容显示。

2、具体实现

(1)、整体布局
<template>
  <div id="app">
    <Header :seller="seller"></Header>
    <div class="tab">
      <div class="tab-item">
        <router-link :to="{name:'goods'}">商品</router-link>
      </div>
      <div class="tab-item">
        <router-link :to="{name:'ratings'}">评价</router-link>
      </div>
      <div class="tab-item">
        <router-link :to="{name:'seller'}">商家</router-link>
      </div>
    </div>
    <keep-alive>
    <router-view :seller="seller"></router-view></keep-alive>
  </div>
</template>
(2)、数据获取

本次项目中主要使用的是axios实现与后端的数据交互
引入axios

import axios from 'axios'

数据获取 获取的数据使用seller接收

<script>
// eslint-disable-next-line no-unused-vars
import Header from '@/components/header/header'
import axios from 'axios'
const ERR_OK = 0
export default {
  data () {
    return {
      seller: {
      }
    }
  },

  created () {
    axios.get('/api/seller').then((response) => {
      response = response.data
      if (response.errno === ERR_OK) {
        this.seller = response.data
      }
    })
  },
  components: {
    Header
  },
  name: 'App'
}
</script>

3、源码

<template>
  <div id="app">
    <Header :seller="seller"></Header>
    <div class="tab">
      <div class="tab-item">
        <router-link :to="{name:'goods'}">商品</router-link>
      </div>
      <div class="tab-item">
        <router-link :to="{name:'ratings'}">评价</router-link>
      </div>
      <div class="tab-item">
        <router-link :to="{name:'seller'}">商家</router-link>
      </div>
    </div>
    <keep-alive>
    <router-view :seller="seller"></router-view></keep-alive>
  </div>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import Header from '@/components/header/header'
import axios from 'axios'
const ERR_OK = 0
export default {
  data () {
    return {
      seller: {
      }
    }
  },

  created () {
    axios.get('/api/seller').then((response) => {
      response = response.data
      if (response.errno === ERR_OK) {
        this.seller = response.data
      }
    })
  },
  components: {
    Header
  },
  name: 'App'
}
</script>
<style scoped>
/* 评论商家商品使用flex布局 */
.tab{
  display: flex;
  width: 100%;
  /* 展示的高度是height的两倍 */
  height: 40px;
  line-height: 40px;
}
.tab .tab-item{
  flex: 1;
  text-align: center;
}
.tab .tab-item ::after{
  display: block;
  content: ' ';
  border-top: 1px solid #ddd ;
  left: 0;
  right: 0;
  width: 100%;
}
.tab .tab-item router-link{
  display: block;
  font-size:14px;
  color: rgb(77,85,93);
}
.tab .tab-item .router-link-exact-active{
  /*  选中时样式 */
  color: rgb(240,20,20);
}
</style>>

二、header.vue组件

1、重点说明

header组件主要实现的是商家的名称和公告的展示,并且商家的公告在展示过程中要做到单行展示,超出部分缩略的效果。通过文字超出部分隐藏三要素实现

  // 文字超出部分隐藏三要素
  white-space: nowrap;
  // // 消除子元素之间的空白间隙 或者可以删掉span标签之间的空格
  // font-size:0;
  overflow: hidden;
  text-overflow: ellipsis;

背景图片模糊效果 使用通过设置背景图片撑满整个header,将背景图片的z-index设置为-1,使用CSS3中的filter: blur(10px);实现

2、具体实现

(1)、整体布局
<template>
  <div class="header">
    <div class="content-wrapper">
      <!-- 头像 -->
      <div class="avatar">
        <img :src="seller.avatar" width="64" height="64" alt="">
      </div>
      <!-- 内容 -->
      <div class="content">
        <div class="title">
          <span class="brand"></span>
          <span class="name">{{ seller.name }}</span>
        </div>
        <div class="description">{{ seller.description }}/{{ seller.deliveryTime }}分钟送达</div>
        <div v-if="seller.supports" class="support">
          <span class="icon" :class="classMap[seller.supports[0].type]"></span>
          <span class="text">{{seller.supports[0].description}}</span>
        </div>
      <div v-if="seller.supports" class="support-count" @click="handleClick">
        <span class="count">{{seller.supports.length}}</span>
        <i class="iconfont icon-moreinfo-copy"></i>
      </div>
      </div>
    </div>
    <!-- 详情简介 -->
    <div class="bulletin-wrapper" @click="handleClick">
      <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span>
      <i class="iconfont icon-moreinfo-copy"></i>
    </div>
    <!-- 背景 -->
    <div class="background">
      <img :src="seller.avatar" width="100%" height="100%" alt="">
    </div>
    <!-- 弹层 -->
    <Detal ref="child" :seller="seller" :classMap="classMap"/>
  </div>
</template>
(2)、父子组件传值

在父组件中通过

将数据传递给header组件,在header组件中通过prop属性接收seller数据

 props: {
    seller: {
      type: Object
    }
  },
(3)、商家头像展示

商家头像展示通过动态绑定img的src属性获取商家的图片

<div class="avatar">
   <img :src="seller.avatar" width="64" height="64" alt="">
</div>
(4)、商家内容展示

在商家内容展示中,难点就是商家会有不同的折扣方式,不同的折扣方式对应着不同的折扣图片和文字信息。定义一个classMap用来存储折扣方式

  created () {
    // 定义一个数组用来存储折扣的图标
    this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
  },

通过:class="classMap[seller.supports[0].type]"绑定对应的折扣图标通过{{seller.supports[0].description}}展示文字信息

(5)、字体图标的使用
<div v-if="seller.supports" class="support-count" @click="handleClick">
   <span class="count">{{seller.supports.length}}</span>
   <i class="iconfont icon-moreinfo-copy"></i>
</div>
 <div class="bulletin-wrapper" @click="handleClick">
   <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span>
   <i class="iconfont icon-moreinfo-copy"></i>
 </div>

3、源码

<template>
  <div class="header">
    <div class="content-wrapper">
      <!-- 头像 -->
      <div class="avatar">
        <img :src="seller.avatar" width="64" height="64" alt="">
      </div>
      <!-- 内容 -->
      <div class="content">
        <div class="title">
          <span class="brand"></span>
          <span class="name">{{ seller.name }}</span>
        </div>
        <div class="description">{{ seller.description }}/{{ seller.deliveryTime }}分钟送达</div>
        <div v-if="seller.supports" class="support">
          <span class="icon" :class="classMap[seller.supports[0].type]"></span>
          <span class="text">{{seller.supports[0].description}}</span>
        </div>
      <div v-if="seller.supports" class="support-count" @click="handleClick">
        <span class="count">{{seller.supports.length}}</span>
        <i class="iconfont icon-moreinfo-copy"></i>
      </div>
      </div>
    </div>
    <!-- 详情简介 -->
    <div class="bulletin-wrapper" @click="handleClick">
      <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span>
      <i class="iconfont icon-moreinfo-copy"></i>
    </div>
    <!-- 背景 -->
    <div class="background">
      <img :src="seller.avatar" width="100%" height="100%" alt="">
    </div>
    <!-- 弹层 -->
    <Detal ref="child" :seller="seller" :classMap="classMap"/>
  </div>
</template>
<script>
import '../../common/stylus/mixin.styl'
// import Modal from '@/components/Modal/Modal.js'
import Detal from '@/components/detal/detal'
export default {
  // 父组件使用:seller='seller'将数据传给子组件,子组件使用props接收
  props: {
    seller: {
      type: Object
    }
  },
  created () {
    // 定义一个数组用来存储折扣的图标
    this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
  },
  components: {
    Detal
  },
  methods: {
    handleClick () {
      // 父组件修改子组件数据
      this.$refs.child.detailShow = true
    }
  }
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
.header{
  color: #fff;
  background: rgba(7,17,27,0.5);
  position: relative;
  overflow: hidden;
}
.header .content-wrapper{
  padding: 24px 12px 18px 24px;
  font-size: 0;
  position: relative;
}
.header .content-wrapper .avatar{
  display: inline-block;
  // 顶部对齐
  vertical-align: top;
}
.header .content-wrapper .avatar img{
  border-radius:2px;
}
.header .content-wrapper .content{
  display: inline-block;
  margin-left: 16px;
  // font-size: 14px;
}
.header .content-wrapper .content .title{
  margin: 2px 0 8px 0;
}
.header .content-wrapper .content .title .brand{
  width: 30px;
  height: 18px;
  vertical-align: top;
  display: inline-block;
  // bg-image('brand');
  background-image :url('./[email protected]')
  background-size :30px 18px;
  background-repeat : no-repeat;
}
.header .content-wrapper .content .title .name{
  margin-left: 6px;
  font-size: 16px;
  line-height: 18px;
  font-weight: bold;
}
.header .content-wrapper .content .description{
  margin-bottom: 10px;
  line-height: 12px;
  font-size: 12px;
}
.header .content-wrapper .content .support .icon{
  display: inline-block;
  width: 12px;
  height: 12px;
  margin-right: 4px;
  background-size: 12px 12px;
  background-repeat: no-repeat;
}
.header .content-wrapper .content .support .decrease{
  background-image: url('./[email protected]')
}
.header .content-wrapper .content .support .discount{
  background-image: url('./[email protected]')
}
.header .content-wrapper .content .support .guarantee{
  background-image: url('./[email protected]')
}
.header .content-wrapper .content .support .invoice{
  background-image: url('./[email protected]')
}
.header .content-wrapper .content .support .special{
  background-image: url('./[email protected]')
}
.header .content-wrapper .content .support .text{
  line-height: 12px;
  font-size: 12px;
  vertical-align: top;
}
.header .content-wrapper .support-count{
  position: absolute;
  right: 12px;
  bottom: 18px;
  padding: 0 8px;
  height: 24px;
  line-height: 24px;
  border-radius: 14px;
  background: rgba(0,0,0,0.2);
  text-align: center;
}
.header .content-wrapper .support-count .count,
.header .content-wrapper .support-count .iconfont{
  vertical-align: top;
  font-size: 10px;
}
.header .bulletin-wrapper{
  height: 28px;
  line-height: 28px;
  position: relative;
  padding: 0 22px 0 12px;
  // 文字超出部分隐藏三要素
  white-space: nowrap;
  // // 消除子元素之间的空白间隙 或者可以删掉span标签之间的空格
  // font-size:0;
  overflow: hidden;
  text-overflow: ellipsis;
  background: rgba(7,17,27,0.2);
}
.header .bulletin-wrapper .bulletin-title{
  display: inline-block;
  width: 22px;
  height: 12px;
  background-image: url('./[email protected]');
  background-size: 22px 12px;
  background-repeat: no-repeat;
  vertical-align: top;
  margin-top: 7px;
}
.header .bulletin-wrapper .bulletin-text{
  font-size: 10px;
  margin: 0 4px;
  vertical-align: top;
}
.header .bulletin-wrapper .iconfont{
  position: absolute;
  font-size: 10px;
  right: 12px;
}
.header .background{
  //背景图片
  position: absolute;
  top:0;
  left:0;
  width:100%;
  height:100%;
  z-index:-1;
  // 模糊效果
  filter: blur(10px);
}
</style>

三、蒙层组件

1、detal组件

<template>
  <transition name="slide-fade">
    <div class="detail" v-if="detailShow">
      <Modal :seller="seller" :classMap="classMap" @hide-detail="changeDetail">
      </Modal>
    </div>
  </transition>
</template>

默认detal组件是不显示的,因此在data中定义detailShow,通过改变detal的值来控制蒙层。

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

类似的子组件通过定义prop属性来接收父组件传过来的值

   props: {
    seller: {
      type: Object
    },
    classMap: {
      type: Array
    }
  },

2、Modal组件

Modal组件主要实现蒙层的内容展示

(1)、整体布局
<template>
  <div class="modal">
    <div class = "detail-wrapper  clearfix">
      <div class = "detail-main">
        <div class="detail-content">
          <h1 class="name">{{seller.name}}</h1>
          <div class="star-wrapper">
            <Star :score="seller.score"></Star>
          </div>
          <div class="title">
            <div class="line"></div>
            <div class="text">优惠信息</div>
            <div class="line"></div>
          </div>
          <ul v-if="seller.supports" class="supports">
            <li class="supports-item" v-for="(item,index) in seller.supports" :key="index">
              <span class="icon" :class="classMap[seller.supports[index].type]"></span>
              <span class="text">{{seller.supports[index].description}}</span>
            </li>
          </ul>
          <div class="title">
            <div class="line"></div>
            <div class="text">商家公告</div>
            <div class="line"></div>
          </div>
          <div class="bulletin">
            <p class="content">{{seller.bulletin}}</p>
          </div>
        </div>
      </div>
      <div class = "detail-close" @click="hideDetail">
        <i class="iconfont icon-quxiao"></i>
    </div>
    </div>
  </div>
</template>

3、蒙层的展示和隐藏

展示
在header组件中通过给

<div v-if="seller.supports" class="support-count" @click="handleClick">
   <span class="count">{{seller.supports.length}}</span>
   <i class="iconfont icon-moreinfo-copy"></i>
</div>
 <div class="bulletin-wrapper" @click="handleClick">
   <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span>
   <i class="iconfont icon-moreinfo-copy"></i>
 </div>

绑定点击事件handleClick事件来控制蒙层的展示
由于控制蒙层的展示数据在detal组件中,detal组件是header的子组件,父组件想要修改子组件的数据可以通过

  methods: {
    handleClick () {
      // 父组件修改子组件数据
      this.$refs.child.detailShow = true
    }
  }

修改。
隐藏
在Modal组件中通过点击

<div class = "detail-close" @click="hideDetail">
   <i class="iconfont icon-quxiao"></i>
</div>

可以实现隐藏,由于控制蒙层的隐藏数据在detal组件中,Modal组件是detal组件的子组件,子组件无法直接修改父组件的元素,因此子组件可以抛出一个emit事件(触发事件)

  methods: {
    hideDetail () {
      this.$emit('hide-detail')
    }
  }

父组件detal为子组件设置事件处理程序(注册事件)

 <Modal :seller="seller" :classMap="classMap" @hide-detail="changeDetail">
      </Modal>
  methods: {
    changeDetail () {
      this.detailShow = false
    }
  }

4、源码

detal.vue

<template>
  <transition name="slide-fade">
    <div class="detail" v-if="detailShow">
      <Modal :seller="seller" :classMap="classMap" @hide-detail="changeDetail">
      </Modal>
    </div>
  </transition>
</template>
<script>
import Modal from '@/components/Modal/Modal.vue'
export default {
   props: {
    seller: {
      type: Object
    },
    classMap: {
      type: Array
    }
  },
  components: {
    Modal
  },
  data () {
    return {
      detailShow: false
    }
  },
  methods: {
    changeDetail () {
      this.detailShow = false
    }
  }
}
</script>

<style >
.detail{
  position: fixed;
  z-index: 100;
  top:0;
  left:0;
  width: 100%;
  height: 100%;
  /* overflow: auto 可以当内容超过屏幕高度时可以实现屏幕滚动*/
  overflow: auto;
  background: rgba(7, 17, 27, 0.8);
}
.slide-fade-enter-active {
  transition: all .3s ease;
}
.slide-fade-leave-active {
  transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
  transform: translateX(10px);
  opacity: 0;
}
</style>

Modal.vue

<template>
  <div class="modal">
    <div class = "detail-wrapper  clearfix">
      <div class = "detail-main">
        <div class="detail-content">
          <h1 class="name">{{seller.name}}</h1>
          <div class="star-wrapper">
            <Star :score="seller.score"></Star>
          </div>
          <div class="title">
            <div class="line"></div>
            <div class="text">优惠信息</div>
            <div class="line"></div>
          </div>
          <ul v-if="seller.supports" class="supports">
            <li class="supports-item" v-for="(item,index) in seller.supports" :key="index">
              <span class="icon" :class="classMap[seller.supports[index].type]"></span>
              <span class="text">{{seller.supports[index].description}}</span>
            </li>
          </ul>
          <div class="title">
            <div class="line"></div>
            <div class="text">商家公告</div>
            <div class="line"></div>
          </div>
          <div class="bulletin">
            <p class="content">{{seller.bulletin}}</p>
          </div>
        </div>
      </div>
      <div class = "detail-close" @click="hideDetail">
        <i class="iconfont icon-quxiao"></i>
    </div>
    </div>
  </div>
</template>

<script>
import Star from '@/components/Star/star'
export default {
  props: {
    seller: {
      type: Object
    },
    classMap: {
      type: Array
    }
  },
  components: {
    Star
  },
  methods: {
    hideDetail () {
      this.$emit('hide-detail')
    }
  }
}
</script>

<style scoped>
/* .clearfix{
  display: inline-block;
} */
.clearfix ::after{
  display: table;
  content: ' ';
  clear: both;
}
.detail-wrapper{
  position: fixed;
  width: 100%;
  height: 100%;
  z-index: 100;
  left: 0;
  top: 0;
  overflow: auto;
}
.detail-wrapper .detail-main{
  display: inline-block;
  min-height: 100%;
  width: 100%;
}
.detail-wrapper .detail-main .detail-content{
  margin-top: 64px;
  padding-bottom: 64px;/**撑开高度 给按钮留出位置 */
}
.detail-wrapper .detail-main .detail-content .name{
  line-height: 16px;
  text-align: center;
  font-size: 16px;
  font-weight: 700;
}
.detail-wrapper .detail-main .detail-content .star-wrapper{
  margin-top:18px;
  padding: 2px 0;
  text-align: center;
}
.detail-wrapper .detail-main .detail-content .title{
  display: flex;
  width: 80%;
  margin: 28px auto 24px;
}
.detail-wrapper .detail-main .detail-content .title .line{
  flex: 1;
  position: relative;
  top: -6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.detail-wrapper .detail-main .detail-content .title .text{
  padding: 0 12px;
  font-size: 14px;
  font-weight: 700;
}
.detail-wrapper .detail-main .detail-content .supports{
  width: 80%;
  margin: 0 auto;
}
.detail-wrapper .detail-main .detail-content .supports .supports-item{
  padding: 0 12px;
  margin-bottom: 12px;
  font-size: 0;
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .icon{
  display: inline-block;
  width: 16px;
  height: 16px;
  vertical-align: top;
  margin-right: 6px;
  background-size: 16px 16px;
  background-repeat: no-repeat;
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .decrease{
  background-image: url('./[email protected]')
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .discount{
  background-image: url('./[email protected]')
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .guarantee{
  background-image: url('./[email protected]')
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .invoice{
  background-image: url('./[email protected]')
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .special{
  background-image: url('./[email protected]')
}
.detail-wrapper .detail-main .detail-content .supports .supports-item .text{
  line-height: 16px;
  font-size: 12px;
}
.detail-wrapper .detail-main .detail-content .bulletin{
  width: 80%;
  margin: 0 auto;
}
.detail-wrapper .detail-main .detail-content .bulletin .content{
  padding: 0 12px;
  line-height: 24px;
  font-size: 12px;
}
.detail-close{
  position: relative;
  width: 32px;
  height: 32px;
  margin: -64px auto 0 auto;
  clear: both;
  font-size: 32px;
}
</style>

四、路由切换

在这里使用了页面懒加载,当我们执行这个函数的时候再加载页面

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
  mode: 'history',
  routes: [
    { // 定义首页
      path: '',
      redirect: {name: 'goods'}
    },
    {
      path: '/goods',
      name: 'goods',
      // 这里使用了Webpack的页面懒加载,当我们运行到这个函数的时候再加载这个页面
      component: () => import('@/components/goods/goods')
    },
    {
      path: '/ratings',
      name: 'ratings',
      component: () => import('@/components/ratings/ratings')
    },
    {
      path: '/seller',
      name: 'seller',
      component: () => import('@/components/seller/seller')
    }
  ]
})

你可能感兴趣的:(Vue仿饿了么项目)