静态结构搭建和分类实现

1. 整体结构创建

image.png

1- 按照结构新增五个组件,准备最简单的模版,分别在Home模块的入口组件中引入

  • HomeCategory
  • HomeBanner
  • HomeNew
  • HomeHot
  • HomeProduct
1
2
3
4
5
6
<script setup>
</script>

<template>
<div> HomeCategory </div>
</template>

2- Home模块入口组件中引入并渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>

2. 分类实现

image-20250327221055896.png

1- 准备详细模版

HomeCategory.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<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>

image-20250327221217102.png

2- 完成代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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轮播图实现

1. 熟悉组件

image-20250327221742047.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>

</script>



<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>

2. 获取数据渲染组件

1- 封装接口

1
2
3
4
5
6
7
8
9
10
11
/**
* @description: 获取banner图
* @param {*}
* @return {*}
*/
import { httpInstance } from '@/utils/http'
export function getBannerAPI() {
return httpInstance({
url: 'home/banner'
})
}

2- 获取数据渲染模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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>

面板组件封装

场景说明

问: 组件封装解决了什么问题?

1.复用问题 2.业务维护问题

新鲜好物和人气推荐模块,在结构上非常相似,只是内容不同,可以通过组件封装可以实现复用结构的效果

image-20250328084519296.png

1. 纯静态结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<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>

2. 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script setup>

defineProps({
title: {
type: String,
default: ''
},
subTitle: {
type: String,
default: ''
}
})

</script>


<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{{ title }}<small>{{ subTitle }}</small>
</h3>
</div>
<!-- 主体内容区域 -->
<slot name="main" />
</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>

测试代码:

在Home下的index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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="新鲜好物" sub-title="新鲜毫无 好多商品">
<div>
我是新鲜好物的插槽内容
</div>
</HomePanel>
<HomePanel title="人气推荐" sub-title="人气推荐 好多商品">
<div>
我是新鲜好物的插槽内容
</div>
</HomePanel>
</template>

新鲜好物实现

image-20250328085819225.png

1. 准备模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<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>

2. 封装接口

1
2
3
4
5
6
7
8
9
10
/**
* @description: 获取新鲜好物
* @param {*}
* @return {*}
*/
export const findNewAPI = () => {
return httpInstance({
url:'/home/new'
})
}

3. 获取数据渲染模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup>
import HomePanel from './HomePanel.vue'
import { getNewAPI } from '@/apis/home'
import { ref } from 'vue'
const newList = ref([])
const getNewList = async () => {
const res = await getNewAPI()
newList.value = res.result
}

getNewList()
</script>

<template>
<HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">
<template #main>
<ul class="goods-list">
<li v-for="item in newList" :key="item.id">
<RouterLink :to="`/detail/${item.id}`">
<img :src="item.picture" alt="" />
<p class="name">{{ item.name }}</p>
<p class="price">&yen;{{ item.price }}</p>
</RouterLink>
</li>
</ul>
</template>
</HomePanel>
</template>

人气推荐实现

1. 封装接口

1
2
3
4
5
6
7
8
9
10
/**
* @description: 获取人气推荐
* @param {*}
* @return {*}
*/
export const getHotAPI = () => {
return httpInstance({
url: '/home/hot'
})
}

2. 获取数据渲染模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<script setup>
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'
const hotList = ref([])
const getHotList = async () => {
const res = await getHotAPI()
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="">
<p class="name">{{ item.title }}</p>
<p class="desc">{{ item.desc }}</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>

懒加载指令实现

image-20250328130426602.png

image-20250328130526169.png

1. 封装全局指令

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义懒加载插件
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()
}
},
)
}
})
}
}

更改掉HomeHot.vue中的img标签的src

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
<ul class="goods-list">
<li v-for="item in hotList" :key="item.id">
<RouterLink to="/">
<img v-img-lazy="item.picture" alt="">
<p class="name">{{ item.title }}</p>
<p class="desc">{{ item.desc }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>

image-20250328141115694.png

image-20250328141203214.png

懒加载指令优化:

image-20250328141907025.png

在directives 文件夹下创建一个index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//定义懒加载插件
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'


//引入初始化的样式文件
import '@/styles/common.scss'

//引入懒加载指令插件并且注册
import { lazyPlugin } from '@/directives'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(lazyPlugin)
app.mount('#app')

image-20250328142413390.png

2. 注册全局指令

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

Product产品列表实现

image-20250328142531405.png

image-20250328142539172.png

image-20250328142610702.png

1. 基础数据渲染

1- 准备静态模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<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>

2- 封装接口

1
2
3
4
5
6
7
8
9
10
/**
* @description: 获取所有商品模块
* @param {*}
* @return {*}
*/
export const getGoodsAPI = () => {
return httpInstance({
url: '/home/goods'
})
}

3- 获取并渲染数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home'
import { ref } from 'vue'
const goodsProduct = ref([])
const getGoods = async () => {
const { result } = await getGoodsAPI()
goodsProduct.value = 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="goods in cate.goods" :key="good.id">
<RouterLink to="/" class="goods-item">
<img :src="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>
</li>
</ul>
</div>
</HomePanel>
</div>
</template>

2. 图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="home-product">
<HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
<div class="box">
<RouterLink class="cover" to="/">
<!-- 指令替换 -->
<img v-img-lazy="cate.picture" />
</RouterLink>
<ul class="goods-list">
<li v-for="goods in cate.goods" :key="goods.id">
<RouterLink to="/" class="goods-item">
<!-- 指令替换 -->
<img v-img-lazy="goods.picture" alt="" />
</RouterLink>
</li>
</ul>
</div>
</HomePanel>
</div>

GoodsItem组件封装

image-20250328143325395.png

image-20250328143409038.png

1. 封装组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

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

<template>
<RouterLink to="/" class="goods-item">
<img :src="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>

2. 使用组件

1
2
3
4
5
<ul class="goods-list">
<li v-for="goods in cate.goods" :key="item.id">
<GoodsItem :goods="goods" />
</li>
</ul>

image-20250328145159426.png