危险

为之则易,不为则难

0%

05_布局模块

今日目标

✔ 搭建顶部通栏模块静态结构。

✔ 掌握商品分类和吸顶模块的编写。

配置路由

目标

能够配置小兔鲜儿项目中的一级路由。

内容

  • 安装依赖包。
1
yarn add vue-router
  • 创建组件 views/layout/index.vue
1
2
3
4
5
6
7
<script lang="ts" setup name="Layout"></script>
<template>
<div>顶部</div>
<div>导航</div>
<div>路由出口</div>
<div>底部</div>
</template>
  • 创建组件 views/login/index.vue
1
2
3
4
<script lang="ts" setup name="Login"></script>
<template>
<div>login组件</div>
</template>
  • 创建文件 router/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/views/layout/index.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: Layout,
},
{
path: '/login',
component: () => import('@/views/login/index.vue'),
},
],
})
export default router
  • main.ts 中导入。
1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import 'normalize.css'
import '@/assets/styles/common.less'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
  • 修改 App.vue
1
2
3
<template>
<RouterView></RouterView>
</template>

script name

  • 了解第 1 种方法,src/views/layout/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
<script>
export default {
name: 'Layout',
}
</script>
<script lang="ts" setup name="Layout"></script>
<template>
<div>顶部</div>
<div>导航</div>
<div>路由出口</div>
<div>底部</div>
</template>
  • 使用第 2 种方法,链接
1
yarn add vite-plugin-vue-setup-extend -D
  • 配置 vite.config.js
1
2
3
4
5
import vueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
plugins: [vue(), vueSetupExtend()],
})
  • 注意 script 标签内部要写点内容(注释也行),name 才能生效。

顶部通栏

目标

能够完成 Layout 组件的顶部通栏布局。

image-20211229175807388

内容

  • index.html 引入字体图标文件。
1
2
3
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css" />
<title>小兔鲜儿</title>
  • 新建头部导航组件,layout/components/app-topnav.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
<script lang="ts" setup name="AppTopnav"></script>

<template>
<nav class="app-topnav">
<div class="container">
<ul>
<li>
<a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a>
</li>
<li><a href="javascript:;">退出登录</a></li>
<li><a href="javascript:;">请先登录</a></li>
<li><a href="javascript:;">免费注册</a></li>
<li><a href="javascript:;">我的订单</a></li>
<li><a href="javascript:;">会员中心</a></li>
<li><a href="javascript:;">帮助中心</a></li>
<li><a href="javascript:;">关于我们</a></li>
<li>
<a href="javascript:;"><i class="iconfont icon-phone"></i>手机版</a>
</li>
</ul>
</div>
</nav>
</template>

<style scoped lang="less">
.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>
  • src/views/layout/index.vue 中导入使用。
1
2
3
4
5
6
7
<script lang="ts" setup>
import AppTopnav from './components/app-topnav.vue'
</script>

<template>
<AppTopnav></AppTopnav>
</template>

头部布局

目标

能够完成 Layout 组件的头部布局。

image-20211229180517464

内容

  • 新建 src/views/layout/components/app-header.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
<script lang="ts" setup name="AppHeader">
//
</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><a href="#">美食</a></li>
<li><a href="#">餐厨</a></li>
<li><a href="#">艺术</a></li>
<li><a href="#">电器</a></li>
<li><a href="#">居家</a></li>
<li><a href="#">洗护</a></li>
<li><a href="#">孕婴</a></li>
<li><a href="#">服装</a></li>
<li><a href="#">杂货</a></li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜" />
</div>
<div class="cart">
<a class="curr" href="#"> <i class="iconfont icon-cart"></i><em>2</em> </a>
</div>
</div>
</header>
</template>

<style scoped lang="less">
.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;
}
}
}
}
.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>
  • src/views/layout.vue 中导入使用。
1
2
3
4
5
6
7
8
9
<script lang="ts" setup>
import AppTopnav from './components/app-topnav.vue'
import AppHeader from './components/app-header.vue'
</script>

<template>
<AppTopnav></AppTopnav>
<AppHeader></AppHeader>
</template>
  • 素材 assets/images/ 已经提供图片。

  • layout/compoennts/app-header-nav.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
<script lang="ts" setup name="AppHeaderNav"></script>

<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<li><a href="#">美食</a></li>
<li><a href="#">餐厨</a></li>
<li><a href="#">艺术</a></li>
<li><a href="#">电器</a></li>
<li><a href="#">居家</a></li>
<li><a href="#">洗护</a></li>
<li><a href="#">孕婴</a></li>
<li><a href="#">服装</a></li>
<li><a href="#">杂货</a></li>
</ul>
</template>

<style lang="less" scoped>
.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;
}
}
}
</style>
  • layout/components/app-header.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="ts" setup>
import AppHeaderNavVue from './app-header-nav.vue'
</script>

<template>
<header class="app-header">
<div class="container">
<h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1>
<AppHeaderNavVue></AppHeaderNavVue>
// ...
</div>
</header>
</template>

底部内容

目标

能够完成 Layout 布局的底部布局效果。

image-20211229182504728

内容

  • src/views/layout/components/app-footer.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
<script lang="ts" setup name="AppFooter"></script>

<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="less">
.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>
  • src/views/layout.vue 中导入使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script lang="ts" setup>
import AppTopnav from './components/app-topnav.vue'
import AppHeader from './components/app-header.vue'
import AppFooter from './components/app-footer.vue'
</script>

<template>
<AppTopnav></AppTopnav>
<AppHeader></AppHeader>
<main class="app-body">
<!-- 路由出口 -->
</main>
<AppFooter></AppFooter>
</template>

<style lang="less" scoped>
.app-body {
min-height: 600px;
}
</style>

配置 Pinia

目标

能够集成 Pinia 环境,统一管理项目中的数据。

内容

  • 安装 pinia。
1
yarn add pinia
  • main.ts 中注册 pinia。
1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from 'vue'
import App from '@/App.vue'
// 引入通用样式,浏览器表现是一致的
import 'normalize.css'
// 引入项目中一些通用样式
import '@/assets/styles/common.less'
import router from '@/router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(router)

app.use(createPinia())
app.mount('#app')
  • store/modules/category.ts,用于管理 category 模块的数据。
1
2
3
4
5
6
import { defineStore } from 'pinia'
export default defineStore('category', {
state: () => ({
money: 100,
}),
})
  • store/index.ts 统一管理所有的模块。
1
2
3
4
5
6
7
import useCategoryStore from './modules/category'

export default function useStore() {
return {
category: useCategoryStore(),
}
}
  • layout/components/app-header-nav.vue 中测试。
1
2
3
import useStore from '@/store'
const { category } = useStore()
console.log(category.money)

头部分类导航

渲染一级分类

目标

能够发送请求完成分类导航的渲染。

内容

  • store/modules/category.ts 中提供 state 和 actions。
1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from 'pinia'
import request from '@/utils/request'
export default defineStore('category', {
state: () => ({
list: [],
}),
actions: {
async getAllCategory() {
const res = await request.get('/home/category/head')
console.log(res)
},
},
})
  • layout/components/app-header-nav.vue 中发送请求。
1
2
3
4
5
<script lang="ts" setup>
import useStore from '@/store'
const { category } = useStore()
category.getAllCategory()
</script>
  • types/data.d.ts 中定义数据类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 所有的接口的通用类型
export interface ApiRes<T> {
code: string
msg: string
result: T
}

// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
}
  • 修改 store/modules/category.ts,给 axios 请求增加泛型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from 'pinia'
import request from '@/utils/request'
import { ApiRes, CategoryItem } from '@/types/data'
export default defineStore('category', {
state: () => ({
list: [] as CategoryItem[],
}),
actions: {
async getAllCategory() {
const res = await request.get<ApiRes<CategoryItem[]>>('/home/category/head')
this.list = res.data.result
},
},
})
  • 渲染分类导航,在 layout/components/app-header-nav.vue 中。
1
2
3
4
5
6
7
8
<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<li v-for="item in category.list" :key="item.id">
<a href="#">{{ item.name }}</a>
</li>
</ul>
</template>

完善二级分类

src/views/layout/components/app-header-nav.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
<script lang="ts" setup>
import useStore from '@/store'
const { category } = useStore()
category.getAllCategory()
</script>

<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<li v-for="item in home.categoryList" :key="item.id">
<a href="#">{{ item.name }}</a>

<!-- 新增++++++++ -->
<div class="layer">
<ul>
<li v-for="i in 10" :key="i">
<a href="#">
<img src="https://yanxuan.nosdn.127.net/cc361cf40d4f81c7eccefed1ad18face.png?quality=95&imageView" alt="" />
<p>果干</p>
</a>
</li>
</ul>
</div>
<!-- 新增++++++++ -->
</li>
</ul>
</template>

<style scoped lang="less">
.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;
}
}
// 新增样式
&:hover {
> a {
color: @xtxColor;
border-bottom: 1px solid @xtxColor;
}
> .layer {
height: 132px;
opacity: 1;
}
}
}
}
// 新增样式
.layer {
width: 1240px;
background-color: #fff;
position: absolute;
left: -200px;
top: 56px;
height: 0;
overflow: hidden;
opacity: 0;
box-shadow: 0 0 5px #ccc;
transition: all 0.2s 0.1s;
ul {
display: flex;
flex-wrap: wrap;
padding: 0 70px;
align-items: center;
height: 132px;
li {
width: 110px;
text-align: center;
img {
width: 60px;
height: 60px;
}
p {
padding-top: 10px;
}
&:hover {
p {
color: @xtxColor;
}
}
}
}
}
</style>

二级分类渲染

根据数据渲染二级分类,修改 types/data.d.ts 的类型。

1
2
3
4
5
6
7
// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
children: CategoryItem[]
}

动态渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<li v-for="item in home.categoryList" :key="item.id">
<a href="#">{{ item.name }}</a>

<!-- 新增++++++++ -->
<div class="layer" v-if="item.children">
<ul>
<li v-for="sub in item.children" :key="sub.id">
<a href="#">
<img :src="sub.picture" alt="" />
<p>{{ sub.name }}</p>
</a>
</li>
</ul>
</div>
<!-- 新增++++++++ -->
</li>
</ul>
</template>

白屏问题优化

问题:刷新页面,会发现导航加载之前,就闪一下,用户体验不好。

思路:定义一个常量数据和后台保持一致(约定好 9 大分类),这样不请求后台就能展示一级分类,不至于白屏。

  • 定义九个分类常量数据 src/store/constants.ts
1
2
// 顶级分类
export const topCategory = ['居家', '美食', '服饰', '母婴', '个护', '严选', '数码', '运动', '杂项']
  • 在 pinia 中提供初始值,store/modules/category.ts
1
2
3
4
5
6
7
8
9
10
import { topCategory } from '../constants'
const defaultCategory = topCategory.map((item) => ({
name: item,
}))

const useHomeStore = defineStore('category', {
state: () => ({
categoryList: defaultCategory as CategoryItem[],
}),
})

配置二级路由

  • 创建首页组件 views/home/index.vue
1
2
3
4
<script lang="ts" setup name="Home"></script>
<template>
<div class="home">首页</div>
</template>
  • 创建分类组件 views/category/index.vue
1
2
3
4
<script lang="ts" name="TopCategory" setup></script>
<template>
<div class="category">分类组件</div>
</template>
  • 创建二级分类组件 views/category/sub.vue
1
2
3
4
<script lang="ts" setup name="SubCategory"></script>
<template>
<div class="category">二级分类组件</div>
</template>
  • 配置路由规则。
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
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/views/layout/index.vue'
import Home from '@/views/home/index.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: Layout,
children: [
{
path: '',
component: Home,
},
{
path: '/category/:id',
component: () => import('@/views/category/index.vue'),
},
{
path: '/category/sub/:id',
component: () => import('@/views/category/sub.vue'),
},
],
},
{
path: '/login',
component: () => import('@/views/login/index.vue'),
},
],
})

export default router
  • 点击分类进行路由跳转,components/app-header-nav.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<li v-for="(item, index) in list" :key="index">
<RouterLink :to="item.id ? `/category/${item.id}` : '/'">{{ item.name }}</RouterLink>
<div class="layer" v-if="item.children">
<ul>
<li v-for="sub in item.children" :key="sub.id">
<RouterLink :to="`/category/sub/${sub.id}`">
<img :src="sub.picture" alt="" />
<p>{{ sub.name }}</p>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</template>
  • 指定二级路由出口 src/views/layout/index.vue
1
2
3
4
5
6
7
8
9
<template>
<AppTopnav></AppTopnav>
<AppHeader></AppHeader>
<div class="app-body">
<!-- 路由出口 -->
<RouterView></RouterView>
</div>
<AppFooter></AppFooter>
</template>

分类导航优化

目标

点击分类跳转的同时并能关闭对应的二级分类弹窗。

内容

问题:由于是单页面路由跳转不会刷新页面,css 的 hover 一直触发无法关闭分类弹窗。

思路:Pinia 中准备控制二级类目显示/隐藏的变量;鼠标移入 li 时改变对应的变量为 true,即显示对应的二级类目,移出时隐藏二级类目;点击一级类目和二级类目时改变对应的变量为 false,即隐藏二级类目。

1. 在 pinia 给一级分类加 open 控制显示隐藏 src/store/modules/category.ts

1
2
3
4
5
6
7
8
9
10
async getAllCategory() {
const res = await request.get<ApiRes<CategoryItem[]>>(
'/home/category/head'
)
// 1. 给每一个一级分类添加一个open属性,用于控制显示隐藏
res.data.result.forEach((item) => {
item.open = false
})
this.list = res.data.result
},

需要给 types/data.d.ts 文件中新增一个属性。

1
2
3
4
5
6
7
8
// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
open: boolean
children: CategoryItem[]
}

2. action 中添加了 show hide 方法控制显示和隐藏,store/modules/category.ts

1
2
3
4
5
6
7
8
show(id: string) {
const category = this.list.find((item) => item.id === id)
category!.open = true
},
hide(id: string) {
const category = this.list.find((item) => item.id === id)
category!.open = false
},

3. 实现显示和隐藏 src/layout/components/app-header-nav.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
<script lang="ts" setup name="AppHeaderNav">
import useStore from '@/store'
const { category } = useStore()
category.getAllCategory()
</script>

<template>
<ul class="app-header-nav">
<li class="home"><RouterLink to="/">首页</RouterLink></li>
<!-- #1 -->
<li v-for="(item, index) in category.list" :key="index" @mouseenter="category.show(item.id)" @mouseleave="category.hide(item.id)">
<!-- #2 -->
<RouterLink :to="item.id ? `/category/${item.id}` : '/'" @click="category.hide(item.id)">{{ item.name }}</RouterLink>
<!-- #3 -->
<div class="layer" v-if="item.children" :class="{ open: item.open }">
<ul>
<li v-for="sub in item.children" :key="sub.id">
<!-- #4 -->
<RouterLink :to="`/category/sub/${sub.id}`" @click="category.hide(item.id)">
<img :src="sub.picture" alt="" />
<p>{{ sub.name }}</p>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</template>

<style lang="less" scoped>
.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;
}
}
// 新增样式
&:hover {
> a {
color: @xtxColor;
border-bottom: 1px solid @xtxColor;
}
// -
/* > .layer {
height: 132px;
opacity: 1;
} */
}
}
}
.layer {
width: 1240px;
background-color: #fff;
position: absolute;
left: -200px;
top: 56px;
height: 0;
overflow: hidden;
opacity: 0;
box-shadow: 0 0 5px #ccc;
transition: all 0.2s 0.1s;
// +
&.open {
height: 132px;
opacity: 1;
}
ul {
display: flex;
flex-wrap: wrap;
padding: 0 70px;
align-items: center;
height: 132px;
li {
width: 110px;
text-align: center;
img {
width: 60px;
height: 60px;
}
p {
padding-top: 10px;
}
&:hover {
p {
color: @xtxColor;
}
}
}
}
}
</style>

头部分类导航

吸顶功能

目标

完成头部组件吸顶效果的实现。

内容

交互要求

1. 滚动距离大于等于 78 个 px 的时候,组件会在顶部固定定位。

2. 滚动距离小于 78 个 px 的时候,组件消失隐藏。

实现思路

1. 准备一个吸顶组件,准备一个类名,控制显示隐藏。

2. 监听页面滚动,判断滚动距离,距离大于 78px 添加类名。

代码

  • 新建吸顶导航组件 src/Layout/components/app-header-sticky.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
<script lang="ts" setup name="AppHeaderSticky">
import AppHeaderNav from './app-header-nav.vue'
</script>

<template>
<div class="app-header-sticky">
<div class="container">
<RouterLink class="logo" to="/" />
<AppHeaderNav />
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</div>
</div>
</div>
</template>

<style scoped lang="less">
.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%);
&.show {
transition: all 0.3s linear;
transform: translateY(0%);
}
.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;
}
}
}
}
</style>
  • Layout 首页引入吸顶导航组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script lang="ts" setup>
import AppTopnav from './components/app-topnav.vue'
import AppHeader from './components/app-header.vue'
import AppFooter from './components/app-footer.vue'
import AppHeaderSticky from './components/app-header-sticky.vue'
</script>
<template>
<AppTopnav></AppTopnav>
<AppHeader></AppHeader>
<AppHeaderSticky></AppHeaderSticky>
<div class="app-body">
<!-- 路由出口 -->
<RouterView></RouterView>
</div>
<AppFooter></AppFooter>
</template>

<style lang="less" scoped>
.app-body {
min-height: 600px;
}
</style>
  • 给 window 注册 scroll 事件,获取滚动距离,views/layout/components/app-header-sticky.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script lang="ts" setup name="AppHeaderSticky">
import AppHeaderNav from './app-header-nav.vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const y = ref(0)
const onScroll = () => {
y.value = document.documentElement.scrollTop
}
onMounted(() => {
window.addEventListener('scroll', onScroll)
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', onScroll)
})
</script>
  • 控制 sticky 的显示和隐藏。
1
<div class="app-header-sticky" :class="{show:y>=78}"></div>
  • 修复 bug:划入正常的分类导航时,吸顶的二级分类也进行了展示。
1
<div class="container" v-show="y>=78" />

吸顶重构

目标

使用 vueuse/core 重构吸顶功能。

内容

  • 安装 @vueuse/core 包,它封装了常见的一些交互逻辑。
1
yarn add @vueuse/core
  • 在吸顶导航中使用。

src/components/app-header-sticky.vue

1
2
3
4
5
6
<script lang="ts" setup>
import AppHeaderNav from './app-header-nav.vue'
import { useWindowScroll } from '@vueuse/core'
// 控制是否显示吸顶组件
const { y } = useWindowScroll()
</script>