危险

为之则易,不为则难

0%

05_布局模块

今日目标

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

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

配置路由

目标

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

内容

  1. 安装依赖包。
1
yarn add vue-router
  1. 创建布局组件,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>
  1. 创建登录组件,views/login/index.vue
1
2
3
4
<script lang="ts" setup name="Login"></script>
<template>
<div>login组件</div>
</template>
  1. 创建路由文件,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
  1. 使用路由,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')
  1. 指定一级路由出口,App.vue
1
2
3
<template>
<RouterView></RouterView>
</template>

script name

目标:通过调试工具能看到组件的名字。

  1. 了解第 1 种方法,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>
  1. 使用第 2 种方法,链接
1
yarn add vite-plugin-vue-setup-extend -D
  1. 配置 vite.config.js
1
2
3
4
5
import vueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
plugins: [vue(), vueSetupExtend()],
})
  1. 注意:作为路由组件适应时,script 标签内部要写点内容(注释也行),script name 才能生效。

顶部通栏

目标

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

image-20211229175807388

内容

  1. 引入字体图标,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>
  1. 头部导航组件,views/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>
  1. 导入顶部通栏组件,views/layout/index.vue
1
2
3
4
5
6
7
8
<script lang="ts" setup>
import AppTopnav from './components/app-topnav.vue'
</script>

<template>
<!-- 调试工具显示的名字,其实是和组件的文件名保持一致的 -->
<AppTopnav />
</template>

头部导航

目标

能够完成 Layout 组件的头部导航。

image-20211229180517464

内容

  1. 头部导航组件,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>
  1. 导入头部导航组件,views/layout/index.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 />
<AppHeader />
</template>
  1. 迁入图片素材,把 01-教学资料/images 目录拷贝到项目 assets 文件夹。

  2. 抽离导航部分为单独组件,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
<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>
  1. 引入导航部分组件,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 />
// ...
</div>
</header>
</template>

底部内容

目标

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

image-20211229182504728

内容

  1. 新建底部组件,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>
  1. 导入使用,views/layout/index.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 />
<AppHeader />
<main class="app-body">
<!-- 路由出口 -->
</main>
<AppFooter />
</template>

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

配置 Pinia

目标

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

内容

  1. 安装 pinia。
1
yarn add pinia
  1. 注册 pinia,main.ts
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')
  1. 创建 category 模块,store/modules/category.ts
1
2
3
4
5
6
import { defineStore } from 'pinia'
export default defineStore('category', {
state: () => ({
money: 100,
}),
})
  1. 创建 useStore 统一管理所有的模块,store/index.ts
1
2
3
4
5
6
7
import useCategoryStore from './modules/category'

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

头部分类导航

渲染一级分类

目标

能够请求分类导航的数据存储到 Pinia 并渲染。

内容

  1. 提供 state 和 actions,store/modules/category.ts
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)
},
},
})
  1. 发送请求,views/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>
  1. 定义公共的数据类型,types/data.d.ts
1
2
3
4
5
6
// 所有的接口的通用类型
export interface IApiRes<T> {
code: string
msg: string
result: T
}
  1. 定义分类导航的数据类型,types/goods.d.ts
1
2
3
4
5
6
// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
}
  1. 给 axios 请求增加泛型,store/modules/category.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import request from '@/utils/request'
import { defineStore } from 'pinia'
import { IApiRes } from '@/types/data'
import { CategoryItem } from '@/types/category'

export default defineStore('category', {
state: () => ({
list: [] as CategoryItem[],
}),
actions: {
async getAllCategory() {
const res = await request.get<IApiRes<CategoryItem[]>>('/home/category/head')
this.list = res.data.result
},
},
})
  1. 渲染分类导航,views/layout/components/app-header-nav.vue
1
2
3
4
5
6
7
8
9
<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>

完善二级分类

添加结构和样式,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 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>
<li v-for="item in category.list" :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 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;
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. 修改 CategoryItem 的类型,types/goods.d.ts
1
2
3
4
5
6
7
// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
children: CategoryItem[]
}
  1. 动态渲染,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
<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>
<!-- [[[开始 -->
<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 大分类),这样不请求也能展示一级分类,不至于白屏。

  1. 定义九个分类常量数据,store/constants.ts
1
2
// 顶级分类
export const topCategory = ['居家', '美食', '服饰', '母婴', '个护', '严选', '数码', '运动', '杂项']
  1. 在 pinia 中提供初始值,store/modules/category.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import request from '@/utils/request'
import { defineStore } from 'pinia'
import { IApiRes } from '@/types/data'
import { CategoryItem } from '@/types/category'
// #1
import { topCategory } from '../constants'

// #2
const defaultCategory = topCategory.map((item) => ({
name: item,
}))

export default defineStore('category', {
state: () => ({
// #3
list: defaultCategory as CategoryItem[],
}),
actions: {
async getAllCategory() {
const res = await request.get<IApiRes<CategoryItem[]>>('/home/category/head')
this.list = res.data.result
},
},
})
  1. 修改 key 的值为索引,views/layout/components/app-header-nav.vue
1
<li v-for="(item, index) in category.list" :key="index"></li>

配置二级路由

  1. 创建首页组件,views/home/index.vue
1
2
3
4
<script lang="ts" setup name="Home"></script>
<template>
<div class="home">首页</div>
</template>
  1. 创建分类组件,views/category/index.vue
1
2
3
4
<script lang="ts" name="TopCategory" setup></script>
<template>
<div class="category">分类组件</div>
</template>
  1. 创建二级分类组件,views/category/sub.vue
1
2
3
4
<script lang="ts" setup name="SubCategory"></script>
<template>
<div class="category">二级分类组件</div>
</template>
  1. 配置路由规则,router/index.ts
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
  1. 指定二级路由入口(包括一级分类和二级分类),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
<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>
  1. 指定二级路由出口,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 一直在触发着。

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

  1. 修改 CategoryItem 类型,types/goods.d.ts
1
2
3
4
5
6
7
8
// 单个分类的类型
export type CategoryItem = {
id: string
name: string
picture: string
children: CategoryItem[]
open: boolean
}
  1. 给一级分类加 open 属性用于控制子分类的显示隐藏,store/modules/category.ts
1
2
3
4
5
6
7
8
9
async getAllCategory() {
const res = await request.get<ApiRes<CategoryItem[]>>(
'/home/category/head'
)
res.data.result.forEach((item) => {
item.open = false
})
this.list = res.data.result
}
  1. 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
},
  1. 实现显示和隐藏,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
106
107
108
<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>
<!-- #2 -->
<li v-for="(item, index) in category.list" :key="index" @mouseenter="category.show(item.id)" @mouseleave="category.hide(item.id)">
<!-- #3: 绑定点击 -->
<RouterLink :to="item.id ? `/category/${item.id}` : '/'" @click="category.hide(item.id)">{{ item.name }}</RouterLink>
<!-- [[[开始 -->
<!-- #1: 绑定点击 -->
<div class="layer" v-if="item.children" :class="{ open: item.open }">
<ul>
<!-- #4 -->
<li @click="category.hide(item.id)">
<RouterLink :to="`/category/sub/${sub.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>

头部分类导航

吸顶功能

目标

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

内容

交互要求:默认吸顶组件消失隐藏,当滚动距离大于等于 78 的时候,组件固定定位在顶部。

实现思路:准备一个吸顶组件和控制显示隐藏的类名;监听页面滚动,记录滚动距离,大于 78 的时候添加类名。

代码

  1. 新建吸顶导航组件,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>
  1. 引入吸顶导航组件,views/layout/index.vue
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>
  1. 给 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>
  1. 控制吸顶组件的显示隐藏,views/layout/components/app-header-sticky.vue
1
2
<!-- 这儿是通过位置控制的显示隐藏 -->
<div class="app-header-sticky" :class="{show:y>=78}"></div>
  1. 修复 bug,划入一级分类导航时,吸顶的二级分类也进行了展示,views/layout/components/app-header-sticky.vue
1
<div class="container" v-show="y >= 78" />

吸顶重构

目标

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

内容

  1. 安装 @vueuse/core
1
yarn add @vueuse/core
  1. 在吸顶导航中使用,views/layout/components/app-header-sticky.vue
1
2
3
4
5
6
<script lang="ts" setup name="AppHeaderSticky">
import { useWindowScroll } from '@vueuse/core'
import AppHeaderNav from './app-header-nav.vue'

const { y } = useWindowScroll()
</script>