今日目标
✔ 搭建顶部通栏模块静态结构。
✔ 掌握商品分类和吸顶模块的编写。
配置路由
目标
能够配置小兔鲜儿项目中的一级路由。
内容
- 创建组件
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>
|
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 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 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>
|
1
| yarn add vite-plugin-vue-setup-extend -D
|
1 2 3 4 5
| import vueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({ plugins: [vue(), vueSetupExtend()], })
|
- 注意 script 标签内部要写点内容(注释也行),name 才能生效。
顶部通栏
目标
能够完成 Layout 组件的顶部通栏布局。

内容
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 组件的头部布局。

内容
- 新建
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>
|
因为在后面的吸顶交互里,我们需要复用导航部分,所以这里我们先直接把他拆分出来,拆分成一个单独的组件。
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 布局的底部布局效果。

内容
-
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 环境,统一管理项目中的数据。
内容
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' ) 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> <li v-for="(item, index) in category.list" :key="index" @mouseenter="category.show(item.id)" @mouseleave="category.hide(item.id)"> <RouterLink :to="item.id ? `/category/${item.id}` : '/'" @click="category.hide(item.id)">{{ item.name }}</RouterLink> <div class="layer" v-if="item.children" :class="{ open: item.open }"> <ul> <li v-for="sub in item.children" :key="sub.id"> <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 { 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>
|
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>
|
1
| <div class="app-header-sticky" :class="{show:y>=78}"></div>
|
- 修复 bug:划入正常的分类导航时,吸顶的二级分类也进行了展示。
1
| <div class="container" v-show="y>=78" />
|
吸顶重构
目标
使用 vueuse/core 重构吸顶功能。
内容
- 安装
@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>
|