黑马程序员前端 Vue3 小兔鲜电商项目——(四)Home 页面布局

Home 页面组件结构

组件结构拆分

image.png

按照结构新增五个组件:左侧分类、Banner、新鲜好物、人气推荐、产品列表。在 src/views/Home/components 路径下依次创建 HomeCategory.vueHomeBanner.vueHomeNew.vueHomeHot.vueHomeProduct.vue

image-20230621110557287

Home 模块中引入组件

在 Home 的 index.vue 模块入口组件中引入各个组件并渲染:

<script setup>
import HomeCategory from './components/HomeCategory.vue'
import HomeBanner from './components/HomeBanner.vue'
import HomeNew from './components/HomeNew.vue'
import HomeHot from './components/HomeHot.vue'
import homeProduct from './components/HomeProduct.vue'
</script>

<template>
  <div class="container">
    <HomeCategory />
    <HomeBanner />
  </div>
  <HomeNew />
  <HomeHot />
  <homeProduct />
</template>

分类实现

模板代码

HomeCategory.vue 文件中添加以下代码:

<script setup>

</script>

<template>
  <div class="home-category">
    <ul class="menu">
      <li v-for="item in 9" :key="item">
        <RouterLink to="/">居家</RouterLink>
        <RouterLink v-for="i in 2" :key="i" to="/">南北干货</RouterLink>
        <!-- 弹层layer位置 -->
        <div class="layer">
          <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
          <ul>
            <li v-for="i in 5" :key="i">
              <RouterLink to="/">
                <img alt="" />
                <div class="info">
                  <p class="name ellipsis-2">
                    男士外套
                  </p>
                  <p class="desc ellipsis">男士外套,冬季必选</p>
                  <p class="price"><i>¥</i>200.00</p>
                </div>
              </RouterLink>
            </li>
          </ul>
        </div>
      </li>
    </ul>
  </div>
</template>


<style scoped lang='scss'>
.home-category {
  width: 250px;
  height: 500px;
  background: rgba(0, 0, 0, 0.8);
  position: relative;
  z-index: 99;

  .menu {
    li {
      padding-left: 40px;
      height: 55px;
      line-height: 55px;

      &:hover {
        background: $xtxColor;
      }

      a {
        margin-right: 4px;
        color: #fff;

        &:first-child {
          font-size: 16px;
        }
      }

      .layer {
        width: 990px;
        height: 500px;
        background: rgba(255, 255, 255, 0.8);
        position: absolute;
        left: 250px;
        top: 0;
        display: none;
        padding: 0 15px;

        h4 {
          font-size: 20px;
          font-weight: normal;
          line-height: 80px;

          small {
            font-size: 16px;
            color: #666;
          }
        }

        ul {
          display: flex;
          flex-wrap: wrap;

          li {
            width: 310px;
            height: 120px;
            margin-right: 15px;
            margin-bottom: 15px;
            border: 1px solid #eee;
            border-radius: 4px;
            background: #fff;

            &:nth-child(3n) {
              margin-right: 0;
            }

            a {
              display: flex;
              width: 100%;
              height: 100%;
              align-items: center;
              padding: 10px;

              &:hover {
                background: #e3f9f4;
              }

              img {
                width: 95px;
                height: 95px;
              }

              .info {
                padding-left: 10px;
                line-height: 24px;
                overflow: hidden;

                .name {
                  font-size: 16px;
                  color: #666;
                }

                .desc {
                  color: #999;
                }

                .price {
                  font-size: 22px;
                  color: $priceColor;

                  i {
                    font-size: 16px;
                  }
                }
              }
            }
          }
        }
      }

      // 关键样式  hover状态下的layer盒子变成block
      &:hover {
        .layer {
          display: block;
        }
      }
    }
  }
}
</style>

渲染数据

后端接口返回的 JSON 数据格式如下:

image-20230621114021325

获取数据并进行遍历输出:

<script setup>
  
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()

</script>

<template>
  <div class="home-category">
    <ul class="menu">
      <li v-for="item in categoryStore.categoryList" :key="item.id">
        <RouterLink to="/">{
   
   { item.name }}</RouterLink>
        <RouterLink v-for="i in item.children.slice(0, 2)" :key="i" to="/">{
   
   { i.name }}</RouterLink>
        <!-- 弹层layer位置 -->
        <div class="layer">
          <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
          <ul>
            <li v-for="i in item.goods" :key="i.id">
              <RouterLink to="/">
                <img :src="i.picture" alt="" />
                <div class="info">
                  <p class="name ellipsis-2">
                    {
   
   { i.name }}
                  </p>
                  <p class="desc ellipsis">{
   
   { i.desc }}</p>
                  <p class="price"><i>¥</i>{
   
   { i.price }}</p>
                </div>
              </RouterLink>
            </li>
          </ul>
        </div>
      </li>
    </ul>
  </div>
</template>

banner 轮播图实现

模板代码

<script setup>

</script>

<template>
  <div class="home-banner">
    <!--使用 ElementPlus 的轮播图组件-->
    <el-carousel height="500px">
      <el-carousel-item v-for="item in 4" :key="item">
        <img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg" alt="">
      </el-carousel-item>
    </el-carousel>
  </div>
</template>



<style scoped lang='scss'>
.home-banner {
  width: 1240px;
  height: 500px;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 98;

  img {
    width: 100%;
    height: 500px;
  }
}
</style>

封装接口

创建 src/apis/home.js 文件,编写方法用于获取 Banner 图的 API:

/**
 * @description: 获取banner图
 */
import http from '@/utils/http'
export function getBannerAPI() {
    
    
    return http({
    
    
        url: 'home/banner'
    })
}

渲染数据

获取数据并进行遍历输出:

<script setup>
import { getBannerAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'

const bannerList = ref([])

const getBanner = async () => {
  const res = await getBannerAPI()
  console.log(res)
  bannerList.value = res.result
}

onMounted(() => getBanner())

</script>

<template>
  <div class="home-banner">
    <el-carousel height="500px">
      <el-carousel-item v-for="item in bannerList" :key="item.id">
        <img :src="item.imgUrl" alt="">
      </el-carousel-item>
    </el-carousel>
  </div>
</template>

面板组件封装

在页面中显示的【新鲜好物】和【人气推荐】两个模块是一样的分布,代码也相同,只是显示的数据不同,因此可以抽取公共部分进行复用。

核心思路:把可复用的结构只写一次,把可能发生变化的部分抽象成组件参数(popS/插槽)。抽象可变的部分:

  • 主标题和副标题是纯文本,可以抽象成 props 传入
  • 主体内容是复杂的模版,抽象成插槽传入

image-20230621144521559

创建公共组件复用

在 src/views/Home/components 路径下创建 HomePanel.vue 文件,代码如下:

<script setup>

</script>


<template>
  <div class="home-panel">
    <div class="container">
      <div class="head">
         <!-- 主标题和副标题 -->
        <h3>
          新鲜好物<small>新鲜出炉 品质靠谱</small>
        </h3>
      </div>
      <!-- 主体内容区域 -->
      <div> 主体内容 </div>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.home-panel {
  background-color: #fff;

  .head {
    padding: 40px 0;
    display: flex;
    align-items: flex-end;

    h3 {
      flex: 1;
      font-size: 32px;
      font-weight: normal;
      margin-left: 6px;
      height: 35px;
      line-height: 35px;

      small {
        font-size: 16px;
        color: #999;
        margin-left: 20px;
      }
    }
  }
}
</style>

抽取主题和副主题

因为主标题和副标题是纯文本,可以抽象成 props 传入,代码如下:

<script setup>
//定义 Props,主标题和副标题
defineProps({
    
    
    title: {
    
    
        type: String
    },
    subTitle: {
    
    
        type: String
    }
})
</script>

在主代码中使用参数:

<div class="container">
  <div class="head">
    <!-- 主标题和副标题 -->
    <h3>
      {
   
   {title}}<small>{
   
   { subTitle }}</small>
    </h3>
  </div>
  <!-- 主体内容区域 插槽-->
  <slot/>
</div>

新鲜好物实现

image-20230621142447186

模版代码

在 src/views/Home/components 路径下有之前创建好的 HomeNew.vue 文件,添加以下代码:

<script setup>

</script>

<template>
  <div></div>
  <!-- 下面是插槽主体内容模版
  <ul class="goods-list">
    <li v-for="item in newList" :key="item.id">
      <RouterLink to="/">
        <img :src="item.picture" alt="" />
        <p class="name">{
   
   { item.name }}</p>
        <p class="price">&yen;{
   
   { item.price }}</p>
      </RouterLink>
    </li>
  </ul>
  -->
</template>


<style scoped lang='scss'>
.goods-list {
  display: flex;
  justify-content: space-between;
  height: 406px;

  li {
    width: 306px;
    height: 406px;

    background: #f0f9f4;
    transition: all .5s;

    &:hover {
      transform: translate3d(0, -3px, 0);
      box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    }

    img {
      width: 306px;
      height: 306px;
    }

    p {
      font-size: 22px;
      padding-top: 12px;
      text-align: center;
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
    }

    .price {
      color: $priceColor;
    }
  }
}
</style>

封装接口

在 src/apis/home.js 文件中,编写方法用于获取 【新鲜好物】 的 API:

/**
 * @description: 获取新鲜好物
 * @param {*}
 * @return {*}
 */
export function findNewAPI(){
    
    
    return http({
    
    
        url: '/home/new'
    })
}

渲染数据

获取数据并进行遍历输出:

<script setup>
import HomePanel from './HomePanel.vue'
import { findNewAPI } from '@/apis/home'
import { ref, onMounted } from 'vue'

const newList = ref([])
const getNewList = async () => {
    const res = await findNewAPI()
    console.log(res)
    newList.value = res.result
}

onMounted(() => {
    getNewList()
})
</script>

<template>
    <HomePanel title="新鲜好物" sub-title="新鲜出炉 品质好物">
        <!-- 下面是插槽主体内容模版 -->
        <ul class="goods-list">
            <li v-for="item in newList" :key="item.id">
                <RouterLink to="/">
                    <img :src="item.picture" alt="" />
                    <p class="name">{
   
   { item.name }}</p>
                    <p class="price">&yen;{
   
   { item.price }}</p>
                </RouterLink>
            </li>
        </ul>
    </HomePanel>
</template>

人气推荐实现

image-20230621144221266

模板代码

在 src/views/Home/components 路径下有之前创建好的 HomeHot.vue 文件,添加以下代码:

<script setup>

</script>

<template>
  <div></div>
  <!-- 下面是插槽主体内容模版
  <ul class="goods-list">
    <li v-for="item in newList" :key="item.id">
      <RouterLink to="/">
        <img :src="item.picture" :alt="item.alt" />
        <p class="name">{
   
   { item.title }}</p>
      </RouterLink>
    </li>
  </ul>
  -->
</template>

<style scoped lang='scss'>
.goods-list {
  display: flex;
  justify-content: space-between;
  height: 406px;

  li {
    width: 306px;
    height: 406px;

    background: #f0f9f4;
    transition: all .5s;

    &:hover {
      transform: translate3d(0, -3px, 0);
      box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    }

    img {
      width: 306px;
      height: 306px;
    }

    p {
      font-size: 22px;
      padding-top: 12px;
      text-align: center;
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
    }

    .price {
      color: $priceColor;
    }
  }
}
</style>

封装接口

在 src/apis/home.js 文件中,编写方法用于获取 【人气推荐】 的 API:

/**
 * @description: 获取人气推荐
 * @param {*}
 * @return {*}
 */
export function findHotAPI(){
    
    
    return http({
    
    
        url:'/home/hot'
    })
}

渲染数据

获取数据并进行遍历输出:

<script setup>
import HomePanel from './HomePanel.vue';
import { ref, onMounted } from 'vue'
import { findHotAPI } from '@/apis/home'

const hotList = ref([])
const getHotList = async() => {
    const res = await findHotAPI()
    console.log(res)
    hotList.value = res.result
}

onMounted(() => {
    getHotList()
})
</script>

<template>
    <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
        <!-- 下面是插槽主体内容模版 -->
        <ul class="goods-list">
            <li v-for="item in hotList" :key="item.id">
                <RouterLink to="/">
                    <img :src="item.picture" :alt="item.alt" />
                    <p class="name">{
   
   { item.title }}</p>
                </RouterLink>
            </li>
        </ul>
    </HomePanel>
</template>

懒加载指令实现

main.js 入口文件通常只做一些初始化的事情,不应该包含太多的逻辑代码,可以通过插件的方法把懒加载指令封装为插件,main.js 入口文件只需要负责注册插件即可。

封装全局指令

在 src/directives/ 目录下创建 index.js 文件,在其中自定义指令 v-img-lazy,实现当图片进入视口区域时,才对其进行加载:

// 定义懒加载插件
import {
    
     useIntersectionObserver } from '@vueuse/core'

export const lazyPlugin = {
    
    
  install (app) {
    
    
    // 懒加载指令逻辑
    app.directive('img-lazy', {
    
    
      mounted (el, binding) {
    
    
        // el: 指令绑定的那个元素 img
        // binding: binding.value  指令等于号后面绑定的表达式的值  图片url
        console.log(el, binding.value)
        const {
    
     stop } = useIntersectionObserver(
          el,
          ([{
     
      isIntersecting }]) => {
    
    
            console.log(isIntersecting)
            if (isIntersecting) {
    
    
              // 进入视口区域
              el.src = binding.value
              stop()
            }
          },
        )
      }
    })
  }
}

注册全局指令

在 main.js 中自定义指令注册为全局指令:

// 全局指令注册
import {
    
     directivePlugin } from '@/directives'
app.use(directivePlugin)

图片懒加载

在 HomeHot.vue 和 HomeNew.vue 文件的代码上使用自定义的懒加载指令进行修改。修改之后如下:

<img v-img-lazy="item.picture" alt="" />

这样当图片进入视口区域后,才会将图片的地址赋给 img 的 src 属性。

Product 产品列表实现

因为商品列表页面也是有很多模块代码可以复用,只是数据不同,因此我们依然可以继续使用封装好的面板组件。

image-20230621154128794

模板代码

在之前创建好的 HomeProduct.vue 文件中添加如下代码:

<script setup>
import HomePanel from './HomePanel.vue'

</script>

<template>
  <div class="home-product">
    <!-- <HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
      <div class="box">
        <RouterLink class="cover" to="/">
          <img :src="cate.picture" />
          <strong class="label">
            <span>{
   
   { cate.name }}馆</span>
            <span>{
   
   { cate.saleInfo }}</span>
          </strong>
        </RouterLink>
        <ul class="goods-list">
          <li v-for="good in cate.goods" :key="good.id">
            <RouterLink to="/" class="goods-item">
              <img :src="good.picture" alt="" />
              <p class="name ellipsis">{
   
   { good.name }}</p>
              <p class="desc ellipsis">{
   
   { good.desc }}</p>
              <p class="price">&yen;{
   
   { good.price }}</p>
            </RouterLink>
          </li>
        </ul>
      </div>
    </HomePanel> -->
  </div>
</template>

<style scoped lang='scss'>
.home-product {
  background: #fff;
  margin-top: 20px;
  .sub {
    margin-bottom: 2px;

    a {
      padding: 2px 12px;
      font-size: 16px;
      border-radius: 4px;

      &:hover {
        background: $xtxColor;
        color: #fff;
      }

      &:last-child {
        margin-right: 80px;
      }
    }
  }

  .box {
    display: flex;

    .cover {
      width: 240px;
      height: 610px;
      margin-right: 10px;
      position: relative;

      img {
        width: 100%;
        height: 100%;
      }

      .label {
        width: 188px;
        height: 66px;
        display: flex;
        font-size: 18px;
        color: #fff;
        line-height: 66px;
        font-weight: normal;
        position: absolute;
        left: 0;
        top: 50%;
        transform: translate3d(0, -50%, 0);

        span {
          text-align: center;

          &:first-child {
            width: 76px;
            background: rgba(0, 0, 0, 0.9);
          }

          &:last-child {
            flex: 1;
            background: rgba(0, 0, 0, 0.7);
          }
        }
      }
    }

    .goods-list {
      width: 990px;
      display: flex;
      flex-wrap: wrap;

      li {
        width: 240px;
        height: 300px;
        margin-right: 10px;
        margin-bottom: 10px;

        &:nth-last-child(-n + 4) {
          margin-bottom: 0;
        }

        &:nth-child(4n) {
          margin-right: 0;
        }
      }
    }

    .goods-item {
      display: block;
      width: 220px;
      padding: 20px 30px;
      text-align: center;
      transition: all .5s;

      &:hover {
        transform: translate3d(0, -3px, 0);
        box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
      }

      img {
        width: 160px;
        height: 160px;
      }

      p {
        padding-top: 10px;
      }

      .name {
        font-size: 16px;
      }

      .desc {
        color: #999;
        height: 29px;
      }

      .price {
        color: $priceColor;
        font-size: 20px;
      }
    }
  }
}
</style>

封装接口

在 src/apis/home.js 文件中,封装获取商品列表信息的接口:

/**
 * @description: 获取所有商品模块
 * @param {*}
 * @return {*}
 */
export function getGoodsAPI() {
    
    
    return http({
    
    
        url: '/home/goods'
    })
}

渲染数据

获取数据并进行遍历输出:

<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home';
import { ref,onMounted } from 'vue';

const goodsProduct = ref([])
const getGoodList = async()=>{
    const res = await getGoodsAPI()
    goodsProduct.value = res.result
}

onMounted(()=>{
    getGoodList()
})

</script>

图片懒加载

使用 v-img-lazy 懒加载指令替换 img 标签的原始 src:

<!-- 指令替换 -->
<img v-img-lazy="cate.picture" />

<!-- 指令替换 -->
<img v-img-lazy="goods.picture" alt="" />

GoodsItem组件封装

image-20230621154957748

在小兔鲜项目的很多个业务模块中都需要用到同样的商品展示模块,没必要重复定义,封装起来,方便复用。

原代码:

<ul class="goods-list">
  <li v-for="good in cate.goods" :key="good.id">
    <RouterLink to="/" class="goods-item">
      <img v-img-lazy="good.picture" alt="" />
      <p class="name ellipsis">{
   
   { good.name }}</p>
      <p class="desc ellipsis">{
   
   { good.desc }}</p>
      <p class="price">&yen;{
   
   { good.price }}</p>
    </RouterLink>
  </li>
</ul>

修改之后的代码:

<ul class="goods-list">
  <li v-for="goods in cate.goods" :key="item.id">
    <GoodsItem :good="good" />
  </li>
</ul>

封装组件

创建 src\views\Home\components\GoodsItem.vue 文件,抽取可复用的代码,并将商品信息抽象成 props 参数传入:

<script setup>
defineProps({
    good:{
        type:Object,
        default:()=>({})
    }
})
</script>
<template>
    <RouterLink to="/" class="goods-item">
        <img v-img-lazy="good.picture" alt="" />
        <p class="name ellipsis">{
   
   { good.name }}</p>
        <p class="desc ellipsis">{
   
   { good.desc }}</p>
        <p class="price">&yen;{
   
   { good.price }}</p>
    </RouterLink>
</template>
<style lang="scss">
.goods-item {
    display: block;
    width: 220px;
    padding: 20px 30px;
    text-align: center;
    transition: all .5s;

    &:hover {
        transform: translate3d(0, -3px, 0);
        box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    }

    img {
        width: 160px;
        height: 160px;
    }

    p {
        padding-top: 10px;
    }

    .name {
        font-size: 16px;
    }

    .desc {
        color: #999;
        height: 29px;
    }

    .price {
        color: $priceColor;
        font-size: 20px;
    }
}
</style>

使用组件

修改 src\views\Home\components\HomeProduct.vue 中商品信息的代码:

<ul class="goods-list">
  <li v-for="good in cate.goods" :key="good.id">
    <GoodsItem :good="good" />
  </li>
</ul>

猜你喜欢

转载自blog.csdn.net/qq_20185737/article/details/131329916
今日推荐