小兔鲜Vue3 项目学习笔记Day03

系列文章目录

小兔鲜Vue3 项目学习笔记 Day01

小兔鲜Vue3 项目学习笔记 Day02

小兔鲜Vue3 项目学习笔记 Day03

小兔鲜Vue3 项目学习笔记 Day04

小兔鲜Vue3 项目学习笔记 Day05

小兔鲜Vue3 项目学习笔记 Day06

Home

1.Home整体结构搭建和分类实现

在这里插入图片描述

Home文件夹创建components五个组件HomeBanner、HomeCategory、HomeHot、HomeNew、HomeProduct,写点代码。

<template>
    我是...
</template>

打开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-->
<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>
  • 使用pinia渲染出来
<script setup>
import { useCategoryStore } from "@/stores/category";
const categoryStore = useCategoryStore()   //实例有了
</script>

看一下数据结构
在这里插入图片描述

<!--将数据绑定给对应字段-->
 <li v-for="item in categoryStore.categoryList" :key="item">
                <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 alt="" :src="i.picture" />
                                <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>

在这里插入图片描述

2. banner轮播图功能

  • 准备模板
<script setup>

</script>


<!-- HomeBanner.vue -->
<template>
    <div class="home-banner">
        <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>
  • 熟悉elementPlus相关组件
  • 获取接口数据

封装接口

//获取轮播图数据              @/apis/home.js
export function getBannerAPI(params) {
    return httpInstance({
        url: '/home/banner',
        params
    })
}

获取数据

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

// 获取轮播图数据
const getBannerList = async () => {

    const res = await getBannerAPI()
    console.log(res)
}

onMounted(() => getBannerList())
</script>

在这里插入图片描述

  • 渲染组件
<script setup>
import { getBannerAPI } from '@/apis/home.js'
import { onMounted } from 'vue'
import { ref } from 'vue'

// 获取轮播图数据
const bannerList = ref([])   //存储轮播图数组
const getBannerList = async () => {

    const res = await getBannerAPI()
    // console.log(res)
    bannerList.value = res.result
    // console.log(bannerList)

}

onMounted(() => getBannerList())
</script>


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

效果:
在这里插入图片描述

3. Home 面板组件封装

组件封装能解决的问题:a. 复用问题 b. 业务维护问题

新鲜好物人气推荐 模块 结构非常相似,我们考虑进行组件封装,将组件进行复用
在这里插入图片描述
组件封装核心思路: 把可复用的结构只写一次,把可能发生变化的部分(比如说上图的 新鲜好物人气推荐文字就是不同点;图片下的文字也不同)抽象成组件参数(props/插槽)

实现步骤:

  • 准备静态模板 在Home文件夹的components创建一个HomePanel.vue
<!-- HomePanel -->
<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>
  • 抽象可变的部分:

    • 主标题副标题是纯文本,可以抽象为prop传入
    <!-- HomePanel 相关代码-->
    <script setup>
    //定义props
    defineProps({
        title: { type: String },   //主标题
        subTitile: { type: String }  //负标题
    })
    </script>
    
    
    <template>
        <div class="home-panel">
            <div class="container">
                <div class="head">
                    <!-- 主标题和副标题 -->
                    <h3>
                        {{ title }}<small>{{ subTitile }}</small>
                    </h3>
                </div>
                <!-- 主体内容区域 -->
                <div> 主体内容 </div>
            </div>
        </div>
    </template>
    
    • 主体内容是复杂模板,抽象成插槽传入
<!-- HomePanel 相关代码-->
		<!-- 主体内容区域 -->
        <!-- <div> 主体内容 </div> -->
        <slot />

导入到入口组件Home/index.vue中,测试一下

<!-- 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'
import HomePanel from './components/HomePanel.vue'
</script>

<template>
    <div class="container">
        <HomeCategory />
        <HomeBanner />
    </div>
    <HomeNew />
    <HomeHot />
    <HomeProduct />
    
    <!-- 测试,插槽要成对出现 -->
    <HomePanel title="新鲜好物" subTitle="新鲜好物 好多商品">
        <div>我是新鲜好物的插槽内容</div>
    </HomePanel>
    <HomePanel title="人气推荐" subTitle="人气推荐 好多商品">
        <div>我是人气推荐的插槽内容</div>
    </HomePanel>
</template>

在这里插入图片描述

纯展示类组件通用封装思路总结:

- 搭建纯静态的部分,不管可变的部分

- 抽象可变的部分为组件参数(非复杂的模板抽象为props,复杂的结构模板抽象为插槽)

4.新鲜好物和人气推荐实现

在这里插入图片描述

步骤:

  • 准备模板
<!-- 新鲜好物 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>
  • 定制props

引入HomePanel,写入props

<!-- 新鲜好物 HomeNew.vue -->
<script setup>
import HomePanel from './HomePanel.vue'

</script>

<template>
    <HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱"></HomePanel>
    <div></div>
    ...
  • 定制插槽内容(接口+渲染模板)
//获取新鲜好物内容
export function getFreshAndGoodAPI(params) {
    return httpInstance({
        url: '/home/new',
        params
    })
}
<!-- 新鲜好物 HomeNew.vue 相关代码 -->
<script setup>
import HomePanel from './HomePanel.vue'
import { getFreshAndGoodAPI } from '@/apis/home.js'
import { onMounted } from 'vue'
import { ref } from 'vue'

const freshAndGoodList = ref([])  //装新鲜好物数据的数组
const getFreshAndGood = async () => {

    const res = await getFreshAndGoodAPI()
    // console.log(res)
    freshAndGoodList.value = res.result
}

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

<template>
    <HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱">
        <div>
            <ul class="goods-list">
                <li v-for="item in freshAndGoodList" :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>
        </div>
    </HomePanel>
</template>

在这里插入图片描述
人气推荐 类似写法,在此贴上相关代码

//获取 人气推荐 内容
export function getHotAPI() {
    return httpInstance({
        url: '/home/hot'
    })
}
<script setup>
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/apis/home'
import { ref } from 'vue'
const hotList = ref([])
const getHotList = async () => {
    const res = await getHotAPI()
    hotList.value = res.result
}
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="">
                    <p class="name">{{ item.title }}</p>
                    <p class="desc">{{ item.alt }}</p>
                </RouterLink>
            </li>
        </ul>
    </HomePanel>
</template>

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

    li {
        width: 306px;
        height: 406px;
        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;
        }

        .desc {
            color: #999;
            font-size: 18px;
        }
    }
}
</style>

在这里插入图片描述

5. 图片懒加载指令实现

进入视口区域才发送图片请求。

步骤:

  • 熟悉指令语法

自定义指令:https://cn.vuejs.org/guide/reusability/custom-directives.html,记得选 组合式
在这里插入图片描述
定义指令

//定义全局指令         main.js
app.directive('img-lazy', {
    mounted(el, binding) {
        //el:指令绑定的那个元素 img
        //binding:binding》value 指令等于号后面绑定的表达式的值 图片url
        console.log(el, binding.value)
    }
})
  • 判断图片是否进入视口(vueUse

使用useIntersectionObserver

//main.js 
//定义全局指令
app.directive('img-lazy', {
    mounted(el, binding) {
        //el:指令绑定的那个元素 img
        //binding:binding》value 指令等于号后面绑定的表达式的值 图片url
        console.log(el, binding.value)
        useIntersectionObserver(
            el,  //监听的元素el
            ([{ isIntersecting }]) => {   //isIntersecting 布尔值
                console.log(isIntersecting)  //图片进入视口区域,那么isIntersecting为true,否则false
            }
        )
    }
})
  • 如果图片进入视口,发送图片资源请求(img.src = url
//定义全局指令
app.directive('img-lazy', {
    mounted(el, binding) {
        //el:指令绑定的那个元素 img
        //binding:binding》value 指令等于号后面绑定的表达式的值 图片url
        //console.log(el, binding.value)
        useIntersectionObserver(
            el,  //监听的元素el
            ([{ isIntersecting }]) => {   //isIntersecting 布尔值
                //console.log(isIntersecting)  //图片进入视口区域,那么isIntersecting为true,否则false
                if (isIntersecting) {
                    //进入视口区域
                    el.src = binding.value
                }
            }
        )
    }
})

<!--HomeHot.vue-->
<img v-img-lazy="item.picture" alt="">

懒加载指令优化

  • a. 书写位置不合理。我们将懒加载指令封装为一个插件main.js入口文件只需要负责注册插件即可
//directinve新建index.js

//定义懒加载插件
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)
                useIntersectionObserver(
                    el,  //监听的元素el
                    ([{ isIntersecting }]) => {   //isIntersecting 布尔值
                        console.log(isIntersecting)  //图片进入视口区域,那么isIntersecting为true,否则false
                        if (isIntersecting) {
                            //进入视口区域
                            el.src = binding.value
                        }
                    }
                )
            }
        })
    }
}

在main.js中注册插件

import { lazyPlugin } from './directives'

app.use(lazyPlugin)
  • b. useIntersectionObserver对应元素的监听一直存在,除非手动停止监听,存在内存浪费

图片加载完成就不需要监听了,也就是完成第一次加载就停止监听

const { stop } = useIntersectionObserver(
                    el,  //监听的元素el
                    ([{ isIntersecting }]) => {   //isIntersecting 布尔值
                        //console.log(isIntersecting)  //图片进入视口区域,那么isIntersecting为true,否则false
                        if (isIntersecting) {
                            //进入视口区域
                            el.src = binding.value
                            stop()   //结构出的停止监听的方法
                        }
                    }
                )

6. Home- product产品列表实现

重复几个下图结构组成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 准备静态模板

    <!--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>封装接口
    
  • 封装接口

/**
 *获取商品模块Product
 */
export const getGoodsAPI = () => {
    return httpInstance({
        url: '/home/goods'
    })
}
  • 获取数据渲染模板
<!--HomeProduct.vue-->
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'

const goodsProduct = ref([])
const getGoods = async () => {
    const res = await getGoodsAPI()
    goodsProduct.value = res.result
}
onMounted(() => {
    getGoods()
})
</script>

在这里插入图片描述

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

const goodsProduct = ref([])
const getGoods = async () => {
    const res = await getGoodsAPI()
    goodsProduct.value = res.result
}
onMounted(() => {
    getGoods()
})
</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>

在这里插入图片描述

  • 图片懒加载

在这里插入图片描述

查看是否成功:向下滚动看看是否发送请求获取图片:
在这里插入图片描述

7. Home-GoodsItem 组件封装

结构类似,没必要重复定义,封装起来,方便复用
在这里插入图片描述
核心思想:将要显示的数据对象设计为props参数,传入什么对象,显示什么数据

新增GoodItem.vue, 将代码抽离出来。定义props。把相关样式抽离出来。

<script setup>
defineProps({
    goods: {
        type: Object,
        default: () => { }
    }
})
</script>

<template>
    <RouterLink to="/" class="goods-item">
        <img v-img-lazy="goods.picture" alt="" />
        <p class="name ellipsis">{{ goods.name }}</p>
        <p class="desc ellipsis">{{ goods.desc }}</p>
        <p class="price">&yen;{{ goods.price }}</p>
    </RouterLink>
</template>
<style scoped 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>

使用:

<!--HomeProduct.vue-->
<ul class="goods-list">
                    <li v-for="goods in cate.goods" :key="goods.id">
                        <GoodItem :goods="goods" />
                    </li>
                </ul>

一级路由

1. 整体认识和路由配置

配置路由,点击哪个就跳转到哪个页面,地址还要有参数
在这里插入图片描述
找到LayoutHeader组件,拼接字符串
在这里插入图片描述
效果,鼠标放到category上,看左下角:
在这里插入图片描述
吸顶也要配置一下,一样的方式,打开LayoutFixed

 <!-- 导航区域 -->
            <ul class="app-header-nav">
                <li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
                    <RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
                </li>
            </ul>

2. 面包屑导航

在这里插入图片描述

  • 准备模板
<!--Category/index.vue--> 
<script setup>

</script>

<template>
  <div class="top-category">
    <div class="container m-top-20">
      <!-- 面包屑 -->
      <div class="bread-container">
        <el-breadcrumb separator=">">
          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
          <el-breadcrumb-item>居家</el-breadcrumb-item>
        </el-breadcrumb>
      </div>
    </div>
  </div>
</template>


<style scoped lang="scss">
.top-category {
  h3 {
    font-size: 28px;
    color: #666;
    font-weight: normal;
    text-align: center;
    line-height: 100px;
  }

  .sub-list {
    margin-top: 20px;
    background-color: #fff;

    ul {
      display: flex;
      padding: 0 32px;
      flex-wrap: wrap;

      li {
        width: 168px;
        height: 160px;


        a {
          text-align: center;
          display: block;
          font-size: 16px;

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

          p {
            line-height: 40px;
          }

          &:hover {
            color: $xtxColor;
          }
        }
      }
    }
  }

  .ref-goods {
    background-color: #fff;
    margin-top: 20px;
    position: relative;

    .head {
      .xtx-more {
        position: absolute;
        top: 20px;
        right: 20px;
      }

      .tag {
        text-align: center;
        color: #999;
        font-size: 20px;
        position: relative;
        top: -20px;
      }
    }

    .body {
      display: flex;
      justify-content: space-around;
      padding: 0 40px 30px;
    }
  }

  .bread-container {
    padding: 25px 0;
  }
}
</style>
  • 封装接口 这个接口有一个参数,和之前的不一样
//  apis/category.js
//获取二级分类列表
export function getCategoryAPI(id) {
    return httpInstance({
        url: '/category',
        params: {
            id
        }
    })
}
  • 调用接口获取数据
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'

const categoryData = ref({})
const getCategoryData = anysc() => {
    const route = useRoute()
    const res = await getCategoryAPI(route.params.id)
    categoryData.value = res.result
}
onMounted(() => {
    getCategoryData()
})
</script>
  • 渲染模板
<!--Category/index.vue--> 
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>

在这里插入图片描述

3. 一级分类 - 轮播图的实现

轮播图的结构很相似,我们改造一下接口,再复用一下首页轮播图的逻辑

  • 改造之前的接口(适配参数)
//获取轮播图banner数据
export function getBannerAPI(params = {}) {
    // 默认为1 商品为2
    const { distributionSite = '1' } = params
    return httpInstance({
        url: '/home/banner',
        params: {
            distributionSite
        }
    })
}
  • 迁移首页轮播图的逻辑,样式也要记得迁移
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'

//获取数据
const categoryData = ref({})
const route = useRoute()
const getCategoryData = async () => {
    const res = await getCategoryAPI(route.params.id)
    categoryData.value = res.result
}

// 获取轮播图数据
const bannerList = ref([])   //存储轮播图数组
const getBannerList = async () => {

    const res = await getBannerAPI({
        // 默认为1 商品为2
        distributionSite: '2'
    })
    // console.log(res)
    bannerList.value = res.result
    // console.log(bannerList)

}

onMounted(() => {
    getCategoryData()
    getBannerList()
})

</script>

<template>
    <div class="top-category">
        <div class="container m-top-20">
            <!-- 面包屑 -->
            <div class="bread-container">
                <el-breadcrumb separator=">">
                    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
                </el-breadcrumb>
            </div>
            <!-- 轮播图 -->
            <div class="home-banner">
                <el-carousel height="500px">
                    <el-carousel-item v-for="item in bannerList" :key="item">
                        <img :src="item.imgUrl" alt="">
                    </el-carousel-item>
                </el-carousel>
            </div>
        </div>
    </div>
</template>


<style scoped lang="scss">
.top-category {
    h3 {
        font-size: 28px;
        color: #666;
        font-weight: normal;
        text-align: center;
        line-height: 100px;
    }

    .sub-list {
        margin-top: 20px;
        background-color: #fff;

        ul {
            display: flex;
            padding: 0 32px;
            flex-wrap: wrap;

            li {
                width: 168px;
                height: 160px;


                a {
                    text-align: center;
                    display: block;
                    font-size: 16px;

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

                    p {
                        line-height: 40px;
                    }

                    &:hover {
                        color: $xtxColor;
                    }
                }
            }
        }
    }

    .ref-goods {
        background-color: #fff;
        margin-top: 20px;
        position: relative;

        .head {
            .xtx-more {
                position: absolute;
                top: 20px;
                right: 20px;
            }

            .tag {
                text-align: center;
                color: #999;
                font-size: 20px;
                position: relative;
                top: -20px;
            }
        }

        .body {
            display: flex;
            justify-content: space-around;
            padding: 0 40px 30px;
        }
    }

    .bread-container {
        padding: 25px 0;
    }
}

.home-banner {
    width: 1240px;
    height: 500px;
    // position: absolute;
    margin: 0 auto; //居中
    left: 0;
    top: 0;
    z-index: 98;

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

4. 激活状态控制

点击后 样式发生如下改变
在这里插入图片描述
添加active-class属性

<!--LayoutHeader.vue-->

<ul class="app-header-nav">
                <li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
                    <RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
                </li>
            </ul>


在这里插入图片描述

5.分类列表的渲染

  • 模板

    <!--category/index.vue-->
    
    <div class="sub-list">
      <h3>全部分类</h3>
      <ul>
        <li v-for="i in categoryData.children" :key="i.id">
          <RouterLink to="/">
            <img :src="i.picture" />
            <p>{{ i.name }}</p>
          </RouterLink>
        </li>
      </ul>
    </div>
    <div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
      <div class="head">
        <h3>- {{ item.name }}-</h3>
      </div>
      <div class="body">
        <GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" />
      </div>
    </div>
    

    全部代码:

    <!--Category/index.vue-->
    <script setup>
    import { getCategoryAPI } from '@/apis/category.js'
    import { getBannerAPI } from '@/apis/home.js'
    import { ref, onMounted } from 'vue'
    import { useRoute } from 'vue-router'
    import GoodsItem from '../Home/components/GoodItem.vue'
    
    //获取数据
    const categoryData = ref({})
    const route = useRoute()
    const getCategoryData = async () => {
        const res = await getCategoryAPI(route.params.id)
        categoryData.value = res.result
    }
    
    // 获取轮播图数据
    const bannerList = ref([])   //存储轮播图数组
    const getBannerList = async () => {
    
        const res = await getBannerAPI({
            // 默认为1 商品为2
            distributionSite: '2'
        })
        // console.log(res)
        bannerList.value = res.result
        // console.log(bannerList)
    
    }
    
    onMounted(() => {
        getCategoryData()
        getBannerList()
    })
    
    </script>
    
    <template>
        <div class="top-category">
            <div class="container m-top-20">
    
                <!-- 面包屑 -->
                <div class="bread-container">
                    <el-breadcrumb separator=">">
                        <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                        <el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
                    </el-breadcrumb>
                </div>
                <!-- 轮播图 -->
                <div class="home-banner">
                    <el-carousel height="500px">
                        <el-carousel-item v-for="item in bannerList" :key="item">
                            <img :src="item.imgUrl" alt="">
                        </el-carousel-item>
                    </el-carousel>
                </div>
                <!-- 分类列表 -->
                <div class="sub-list">
                    <h3>全部分类</h3>
                    <ul>
                        <li v-for="i in categoryData.children" :key="i.id">
                            <RouterLink to="/">
                                <img :src="i.picture" />
                                <p>{{ i.name }}</p>
                            </RouterLink>
                        </li>
                    </ul>
                </div>
                <div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
                    <div class="head">
                        <h3>- {{ item.name }}-</h3>
                    </div>
                    <div class="body">
                        <GoodsItem v-for="goods in item.goods" :goods="goods" :key="goods.id" />
                    </div>
                </div>
            </div>
        </div>
    </template>
    
    
    <style scoped lang="scss">
    .top-category {
        h3 {
            font-size: 28px;
            color: #666;
            font-weight: normal;
            text-align: center;
            line-height: 100px;
        }
    
        .sub-list {
            margin-top: 20px;
            background-color: #fff;
    
            ul {
                display: flex;
                padding: 0 32px;
                flex-wrap: wrap;
    
                li {
                    width: 168px;
                    height: 160px;
    
    
                    a {
                        text-align: center;
                        display: block;
                        font-size: 16px;
    
                        img {
                            width: 100px;
                            height: 100px;
                        }
    
                        p {
                            line-height: 40px;
                        }
    
                        &:hover {
                            color: $xtxColor;
                        }
                    }
                }
            }
        }
    
        .ref-goods {
            background-color: #fff;
            margin-top: 20px;
            position: relative;
    
            .head {
                .xtx-more {
                    position: absolute;
                    top: 20px;
                    right: 20px;
                }
    
                .tag {
                    text-align: center;
                    color: #999;
                    font-size: 20px;
                    position: relative;
                    top: -20px;
                }
            }
    
            .body {
                display: flex;
                justify-content: space-around;
                padding: 0 40px 30px;
            }
        }
    
        .bread-container {
            padding: 25px 0;
        }
    }
    
    .home-banner {
        width: 1240px;
        height: 500px;
        // position: absolute;
        margin: 0 auto; //居中
        left: 0;
        top: 0;
        z-index: 98;
    
        img {
            width: 100%;
            height: 500px;
        }
    }
    </style>
    

6.一级分类-解决路由缓存问题

官方说明:
在这里插入图片描述
一级分类切换满足这个条件,组件实例复用,导致分类数据无法进行更新。

点击美食居家,参数会发生变化但是页面不会变化。

解决问题思路:

  • 办法1:不复用组件实例,强制销毁

以当前路由完整路径为key的值,给router-view组件绑定

<RouterView :key="$route.fullPath"/>

在这里插入图片描述

  • 办法2:监听路由变化,变化之后执行 数据更新 操作

使用beforeRouteUpdate导航钩子,这个函数可以再每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可。

//	Category/index.vue
<script setup>
import { getCategoryAPI } from '@/apis/category.js'
import { getBannerAPI } from '@/apis/home.js'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../Home/components/GoodItem.vue'
import { onBeforeRouteUpdate } from 'vue-router'
//获取数据
const categoryData = ref({})
const route = useRoute()
const getCategoryData = async () => {
    const res = await getCategoryAPI(route.params.id)
    categoryData.value = res.result
}

// 获取轮播图数据
const bannerList = ref([])   //存储轮播图数组
const getBannerList = async (id = route.params.id) => {

    const res = await getBannerAPI(id)
    // console.log(res)
    bannerList.value = res.result
    // console.log(bannerList)

}

onMounted(() => {
    getCategoryData()
    getBannerList()
})

//期望在路由参数变化时,分类数据接口重新发送
onBeforeRouteUpdate((to) => {
    getCategoryData(to.params.id)
})
</script>

在这里插入图片描述

7. 使用 逻辑函数 拆分业务

指的是,把同一个组件中独立的业务代码通过函数作封装处理,提高代码的可维护性

实现步骤:

  • 安装业务声明以use打头的逻辑函数
  • 独立的业务逻辑封装到各个函数内部
  • 函数内部把组件中需要用到的数据或者方法return出去
  • 组件中调用函数,把数据或者方法组合回来使用

image.png

新建两个js文件
在这里插入图片描述

// 封装banner轮播图相关的业务代码  useBanner.js
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/apis/home'

export function useBanner() {
    const bannerList = ref([])

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

    onMounted(() => getBanner())

    return {
        bannerList
    }
}
//useCategory.js
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { getCategoryAPI } from '@/apis/category'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'

export function useCategory() {
    // 获取分类数据
    const categoryData = ref({})
    const route = useRoute()
    const getCategory = async (id = route.params.id) => {
        const res = await getCategoryAPI(id)
        categoryData.value = res.result
    }
    onMounted(() => getCategory())

    // 目标:路由参数变化的时候 可以把分类数据接口重新发送
    onBeforeRouteUpdate((to) => {
        // 存在问题:使用最新的路由参数请求最新的分类数据
        getCategory(to.params.id)
    })
    return {
        categoryData
    }
}
<!--Category/index.vue-->
<script setup>
import GoodItem from '../Home/components/GoodItem.vue'
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()
</script>

<template>
    <div class="top-category">
        <div class="container m-top-20">

            <!-- 面包屑 -->
            <div class="bread-container">
                <el-breadcrumb separator=">">
                    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
                </el-breadcrumb>
            </div>
            <!-- 轮播图 -->
            <div class="home-banner">
                <el-carousel height="500px">
                    <el-carousel-item v-for="item in bannerList" :key="item">
                        <img :src="item.imgUrl" alt="">
                    </el-carousel-item>
                </el-carousel>
            </div>
            <!-- 分类列表 -->
            <div class="sub-list">
                <h3>全部分类</h3>
                <ul>
                    <li v-for="i in categoryData.children" :key="i.id">
                        <RouterLink to="/">
                            <img :src="i.picture" />
                            <p>{{ i.name }}</p>
                        </RouterLink>
                    </li>
                </ul>
            </div>
            <div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
                <div class="head">
                    <h3>- {{ item.name }}-</h3>
                </div>
                <div class="body">
                    <GoodsItem v-for="goods in item.goods" :goods="goods" :key="goods.id" />
                </div>
            </div>
        </div>
    </div>
</template>


<style scoped lang="scss">
.top-category {
    h3 {
        font-size: 28px;
        color: #666;
        font-weight: normal;
        text-align: center;
        line-height: 100px;
    }

    .sub-list {
        margin-top: 20px;
        background-color: #fff;

        ul {
            display: flex;
            padding: 0 32px;
            flex-wrap: wrap;

            li {
                width: 168px;
                height: 160px;


                a {
                    text-align: center;
                    display: block;
                    font-size: 16px;

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

                    p {
                        line-height: 40px;
                    }

                    &:hover {
                        color: $xtxColor;
                    }
                }
            }
        }
    }

    .ref-goods {
        background-color: #fff;
        margin-top: 20px;
        position: relative;

        .head {
            .xtx-more {
                position: absolute;
                top: 20px;
                right: 20px;
            }

            .tag {
                text-align: center;
                color: #999;
                font-size: 20px;
                position: relative;
                top: -20px;
            }
        }

        .body {
            display: flex;
            justify-content: space-around;
            padding: 0 40px 30px;
        }
    }

    .bread-container {
        padding: 25px 0;
    }
}

.home-banner {
    width: 1240px;
    height: 500px;
    // position: absolute;
    margin: 0 auto; //居中
    left: 0;
    top: 0;
    z-index: 98;

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

在这里插入图片描述

小结

本章对于Home和一级路由功能进行了实现
组件的静态结构也很有价值去研究和书写
love and peace
祝大家学习顺利
在这里插入图片描述

  • 35
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
小兔项目是一个使用Vue3.0开发的项目。它采用了vuex来管理购物车和用户数据,使得数据的管理更加方便。项目中会使用到vue3.0的新特性,因此会体验到更加嫩、酸爽的开发过程。然而,由于目前大多数第三方UI组件库不支持vue3.0,所以在项目中会大量自己封装和布局组件,并预制一些基本样式。在解决问题时,小兔项目主要使用ref函数和组合式API - computed,这样可以更加灵活地处理项目中的数据。关键文件包括vite.config.js(项目的配置文件,基于vite的配置)、package.json(项目包文件,核心依赖项变成了Vue3.x和vite)、main.js(入口文件,使用createApp函数创建应用实例)和app.vue(根组件,使用SFC单文件组件,包含script、template和style)。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [小兔Vue3.0前端电商项目实战](https://blog.csdn.net/qzc2017/article/details/120634549)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [小兔项目----vue3入门](https://blog.csdn.net/qq_63358859/article/details/130704712)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值