文章目录
- Home 页面组件结构
- 组件结构拆分
- Home 模块中引入组件
- 分类实现
- 模板代码
- 渲染数据
- banner 轮播图实现
- 模板代码
- 封装接口
- 渲染数据
- 面板组件封装
- 创建公共组件复用
- 抽取主题和副主题
- 新鲜好物实现
- 模版代码
- 封装接口
- 渲染数据
- 人气推荐实现
- 模板代码
- 封装接口
- 渲染数据
- 懒加载指令实现
- 封装全局指令
- 注册全局指令
- 图片懒加载
- Product 产品列表实现
- 模板代码
- 封装接口
- 渲染数据
- 图片懒加载
- GoodsItem组件封装
- 封装组件
- 使用组件
Home 页面组件结构
组件结构拆分
按照结构新增五个组件:左侧分类、Banner、新鲜好物、人气推荐、产品列表。在 src/views/Home/components 路径下依次创建 HomeCategory.vue
、HomeBanner.vue
、HomeNew.vue
、HomeHot.vue
、HomeProduct.vue
:
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 数据格式如下:
获取数据并进行遍历输出:
<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 传入
- 主体内容是复杂的模版,抽象成插槽传入
创建公共组件复用
在 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>
新鲜好物实现
模版代码
在 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">¥{{ 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">¥{{ item.price }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>
人气推荐实现
模板代码
在 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 产品列表实现
因为商品列表页面也是有很多模块代码可以复用,只是数据不同,因此我们依然可以继续使用封装好的面板组件。
模板代码
在之前创建好的 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">¥{{ 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组件封装
在小兔鲜项目的很多个业务模块中都需要用到同样的商品展示模块,没必要重复定义,封装起来,方便复用。
原代码:
<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">¥{{ 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">¥{{ 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>