组件结构快速搭建

image-20250327205426257.png

LayoutNav.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
<script setup>

</script>

<template>
<nav class="app-topnav">
<div class="container">
<ul>
<template v-if="true">
<li><a href="javascript:;""><i class="iconfont icon-user"></i>周杰伦</a></li>
<li>
<el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
<template #reference>
<a href="javascript:;">退出登录</a>
</template>
</el-popconfirm>
</li>
<li><a href="javascript:;">我的订单</a></li>
<li><a href="javascript:;">会员中心</a></li>
</template>
<template v-else>
<li><a href="javascript:;">请先登录</a></li>
<li><a href="javascript:;">帮助中心</a></li>
<li><a href="javascript:;">关于我们</a></li>
</template>
</ul>
</div>
</nav>
</template>


<style scoped lang="scss">
.app-topnav {
background: #333;
ul {
display: flex;
height: 53px;
justify-content: flex-end;
align-items: center;
li {
a {
padding: 0 15px;
color: #cdcdcd;
line-height: 1;
display: inline-block;

i {
font-size: 14px;
margin-right: 2px;
}

&:hover {
color: $xtxColor;
}
}

~li {
a {
border-left: 2px solid #666;
}
}
}
}
}
</style>

LayoutHeader.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
<script setup>

</script>

<template>
<header class='app-header'>
<div class="container">
<h1 class="logo">
<RouterLink to="/">小兔鲜</RouterLink>
</h1>
<ul class="app-header-nav">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li> <RouterLink to="/">居家</RouterLink> </li>
<li> <RouterLink to="/">美食</RouterLink> </li>
<li> <RouterLink to="/">服饰</RouterLink> </li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜">
</div>
<!-- 头部购物车 -->

</div>
</header>
</template>


<style scoped lang='scss'>
.app-header {
background: #fff;

.container {
display: flex;
align-items: center;
}

.logo {
width: 200px;

a {
display: block;
height: 132px;
width: 100%;
text-indent: -9999px;
background: url('@/assets/images/logo.png') no-repeat center 18px / contain;
}
}

.app-header-nav {
width: 820px;
display: flex;
padding-left: 40px;
position: relative;
z-index: 998;

li {
margin-right: 40px;
width: 38px;
text-align: center;

a {
font-size: 16px;
line-height: 32px;
height: 32px;
display: inline-block;

&:hover {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}

.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
}

.search {
width: 170px;
height: 32px;
position: relative;
border-bottom: 1px solid #e7e7e7;
line-height: 32px;

.icon-search {
font-size: 18px;
margin-left: 5px;
}

input {
width: 140px;
padding-left: 5px;
color: #666;
}
}

.cart {
width: 50px;

.curr {
height: 32px;
line-height: 32px;
text-align: center;
position: relative;
display: block;

.icon-cart {
font-size: 22px;
}

em {
font-style: normal;
position: absolute;
right: 0;
top: 0;
padding: 1px 6px;
line-height: 1;
background: $helpColor;
color: #fff;
font-size: 12px;
border-radius: 10px;
font-family: Arial;
}
}
}
}
</style>

LayoutFooter.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<template>
<footer class="app_footer">
<!-- 联系我们 -->
<div class="contact">
<div class="container">
<dl>
<dt>客户服务</dt>
<dd><i class="iconfont icon-kefu"></i> 在线客服</dd>
<dd><i class="iconfont icon-question"></i> 问题反馈</dd>
</dl>
<dl>
<dt>关注我们</dt>
<dd><i class="iconfont icon-weixin"></i> 公众号</dd>
<dd><i class="iconfont icon-weibo"></i> 微博</dd>
</dl>
<dl>
<dt>下载APP</dt>
<dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd>
<dd class="download">
<span>扫描二维码</span>
<span>立马下载APP</span>
<a href="javascript:;">下载页面</a>
</dd>
</dl>
<dl>
<dt>服务热线</dt>
<dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd>
</dl>
</div>
</div>
<!-- 其它 -->
<div class="extra">
<div class="container">
<div class="slogan">
<a href="javascript:;">
<i class="iconfont icon-footer01"></i>
<span>价格亲民</span>
</a>
<a href="javascript:;">
<i class="iconfont icon-footer02"></i>
<span>物流快捷</span>
</a>
<a href="javascript:;">
<i class="iconfont icon-footer03"></i>
<span>品质新鲜</span>
</a>
</div>
<!-- 版权信息 -->
<div class="copyright">
<p>
<a href="javascript:;">关于我们</a>
<a href="javascript:;">帮助中心</a>
<a href="javascript:;">售后服务</a>
<a href="javascript:;">配送与验收</a>
<a href="javascript:;">商务合作</a>
<a href="javascript:;">搜索推荐</a>
<a href="javascript:;">友情链接</a>
</p>
<p>CopyRight © 小兔鲜儿</p>
</div>
</div>
</div>
</footer>
</template>

<style scoped lang='scss'>
.app_footer {
overflow: hidden;
background-color: #f5f5f5;
padding-top: 20px;

.contact {
background: #fff;

.container {
padding: 60px 0 40px 25px;
display: flex;
}

dl {
height: 190px;
text-align: center;
padding: 0 72px;
border-right: 1px solid #f2f2f2;
color: #999;

&:first-child {
padding-left: 0;
}

&:last-child {
border-right: none;
padding-right: 0;
}
}

dt {
line-height: 1;
font-size: 18px;
}

dd {
margin: 36px 12px 0 0;
float: left;
width: 92px;
height: 92px;
padding-top: 10px;
border: 1px solid #ededed;

.iconfont {
font-size: 36px;
display: block;
color: #666;
}

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

&:last-child {
margin-right: 0;
}
}

.qrcode {
width: 92px;
height: 92px;
padding: 7px;
border: 1px solid #ededed;
}

.download {
padding-top: 5px;
font-size: 14px;
width: auto;
height: auto;
border: none;

span {
display: block;
}

a {
display: block;
line-height: 1;
padding: 10px 25px;
margin-top: 5px;
color: #fff;
border-radius: 2px;
background-color: $xtxColor;
}
}

.hotline {
padding-top: 20px;
font-size: 22px;
color: #666;
width: auto;
height: auto;
border: none;

small {
display: block;
font-size: 15px;
color: #999;
}
}
}

.extra {
background-color: #333;
}

.slogan {
height: 178px;
line-height: 58px;
padding: 60px 100px;
border-bottom: 1px solid #434343;
display: flex;
justify-content: space-between;

a {
height: 58px;
line-height: 58px;
color: #fff;
font-size: 28px;

i {
font-size: 50px;
vertical-align: middle;
margin-right: 10px;
font-weight: 100;
}

span {
vertical-align: middle;
text-shadow: 0 0 1px #333;
}
}
}

.copyright {
height: 170px;
padding-top: 40px;
text-align: center;
color: #999;
font-size: 15px;

p {
line-height: 1;
margin-bottom: 20px;
}

a {
color: #999;
line-height: 1;
padding: 0 10px;
border-right: 1px solid #999;

&:last-child {
border-right: none;
}
}
}
}
</style>

Layout下的index.vue 也就是首页

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
</script>

<template>
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
</template>

image-20250327211434833.png

字体图标渲染

如果不这样做 前面引入的首页会有很多空白

字体图标采用的是阿里的字体图标库,样式文件已经准备好,在 index.html文件中引入即可

font-class 方式

1
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">

一级导航渲染

image.png
实现步骤

  1. 封装接口函数
  2. 调用接口函数
  3. v-for渲染模版

代码落地

1
2
3
4
5
6
7
8

import httpInstance from '@/utils/http'

export function getCategoryAPI () {
return httpInstance({
url: '/home/category/head'
})
}
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>
import { getCategoryAPI } from '@/apis/layout'
import { onMounted, ref } from 'vue'

const categoryList = ref([])
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
}

onMounted(() => getCategory())

</script>

<template>
<header class='app-header'>
<div class="container">
<h1 class="logo">
<RouterLink to="/">小兔鲜</RouterLink>
</h1>
<ul class="app-header-nav">
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜">
</div>
<!-- 头部购物车 -->
</div>
</header>
</template>

吸顶导航交互实现

image-20250327213309861.png

image-20250327213328589.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
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
<script setup>

</script>

<template>
<div class="app-header-sticky"> //这里添加一个show 类名即可显示
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav ">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li>
<RouterLink to="/">居家</RouterLink>
</li>
<li>
<RouterLink to="/">美食</RouterLink>
</li>
<li>
<RouterLink to="/">服饰</RouterLink>
</li>
<li>
<RouterLink to="/">母婴</RouterLink>
</li>
<li>
<RouterLink to="/">个护</RouterLink>
</li>
<li>
<RouterLink to="/">严选</RouterLink>
</li>
<li>
<RouterLink to="/">数码</RouterLink>
</li>
<li>
<RouterLink to="/">运动</RouterLink>
</li>
<li>
<RouterLink to="/">杂项</RouterLink>
</li>
</ul>

<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</div>
</div>
</div>
</template>


<style scoped lang='scss'>
.app-header-sticky {
width: 100%;
height: 80px;
position: fixed;
left: 0;
top: 0;
z-index: 999;
background-color: #fff;
border-bottom: 1px solid #e4e4e4;
// 此处为关键样式!!!
// 状态一:往上平移自身高度 + 完全透明
transform: translateY(-100%);
opacity: 0;

// 状态二:移除平移 + 完全不透明
&.show {
transition: all 0.3s linear;
transform: none;
opacity: 1;
}

.container {
display: flex;
align-items: center;
}

.logo {
width: 200px;
height: 80px;
background: url("@/assets/images/logo.png") no-repeat right 2px;
background-size: 160px auto;
}

.right {
width: 220px;
display: flex;
text-align: center;
padding-left: 40px;
border-left: 2px solid $xtxColor;

a {
width: 38px;
margin-right: 40px;
font-size: 16px;
line-height: 1;

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

.app-header-nav {
width: 820px;
display: flex;
padding-left: 40px;
position: relative;
z-index: 998;

li {
margin-right: 40px;
width: 38px;
text-align: center;

a {
font-size: 16px;
line-height: 32px;
height: 32px;
display: inline-block;

&:hover {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}

.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
}
</style>

2. 渲染基础数据

3. 实现吸顶交互

核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示

vueUse解决获取滚动距离的问题

下载VueUse npm i @vueuse/core

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import LayoutHeaderUl from './LayoutHeaderUl.vue'
// vueUse
import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
</script>

<template>
<div class="app-header-sticky" :class="{ show: y > 78 }">
<!-- 省略部分代码 -->
</div>
</template>

Pinia优化重复请求

image-20250327214408984.png

image-20250327214648126.png

新增一个store category.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
// 导航列表的数据管理
// state 导航列表数据
const categoryList = ref([])

// action 获取导航数据的方法
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
}

return {
categoryList,
getCategory
}
})

接着在 父组件中 :

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


import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
import LayoutFixed from './components/LayoutFixed.vue'

// 触发获取导航列表的action
// pinia 里面的action需要被调用 所以在父组件index中调用 这样只需要发送一次请求
import { useCategoryStore } from '@/stores/category'
import { onMounted } from 'vue'
const categoryStore = useCategoryStore()

onMounted(() => categoryStore.getCategory())
// 接着去两个子组件中调用数据

</script>

<template>
<LayoutFixed />
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
</template>

然后分别在两个子组件中引入:

1
2
3
4
5
//使用pinia中的数据
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()

然后再template中的categoryList 遍历哪个地方前面加上categoryStore.

这样就实现发送一次请求了!