今日目标
✔ 搭建顶部通栏模块静态结构。
✔ 掌握商品分类和吸顶模块的编写。
配置路由
目标
能够配置小兔鲜儿项目中的一级路由。
内容
- 安装依赖包。
- 创建布局组件,
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 种方法,
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 标签内部要写点内容(注释也行),script name 才能生效。
顶部通栏
目标
能够完成 Layout 组件的顶部通栏布局。
内容
- 引入字体图标,
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>
|
- 头部导航组件,
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>
|
- 导入顶部通栏组件,
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 组件的头部导航。
内容
- 头部导航组件,
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>
|
- 导入头部导航组件,
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>
|
迁入图片素材,把 01-教学资料/images
目录拷贝到项目 assets
文件夹。
抽离导航部分为单独组件,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>
|
- 引入导航部分组件,
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 布局的底部布局效果。
内容
- 新建底部组件,
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>
|
- 导入使用,
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 环境,统一管理项目中的数据。
内容
- 安装 pinia。
- 注册 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')
|
- 创建 category 模块,
store/modules/category.ts
。
1 2 3 4 5 6
| import { defineStore } from 'pinia' export default defineStore('category', { state: () => ({ money: 100, }), })
|
- 创建 useStore 统一管理所有的模块,
store/index.ts
。
1 2 3 4 5 6 7
| import useCategoryStore from './modules/category'
export default function useStore() { return { category: useCategoryStore(), } }
|
- 测试,
views/layout/components/app-header-nav.vue
。
1 2 3
| import useStore from '@/store' const { category } = useStore() console.log(category.money)
|
头部分类导航
渲染一级分类
目标
能够请求分类导航的数据存储到 Pinia 并渲染。
内容
- 提供 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) }, }, })
|
- 发送请求,
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>
|
- 定义公共的数据类型,
types/data.d.ts
。
1 2 3 4 5 6
| export interface IApiRes<T> { code: string msg: string result: T }
|
- 定义分类导航的数据类型,
types/goods.d.ts
。
1 2 3 4 5 6
| export type CategoryItem = { id: string name: string picture: string }
|
- 给 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 }, }, })
|
- 渲染分类导航,
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>
|
二级分类渲染
- 修改 CategoryItem 的类型,
types/goods.d.ts
。
1 2 3 4 5 6 7
| export type CategoryItem = { id: string name: string picture: string children: CategoryItem[] }
|
- 动态渲染,
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 大分类),这样不请求也能展示一级分类,不至于白屏。
- 定义九个分类常量数据,
store/constants.ts
。
1 2
| export const topCategory = ['居家', '美食', '服饰', '母婴', '个护', '严选', '数码', '运动', '杂项']
|
- 在 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'
import { topCategory } from '../constants'
const defaultCategory = topCategory.map((item) => ({ name: item, }))
export default defineStore('category', { state: () => ({ list: defaultCategory as CategoryItem[], }), actions: { async getAllCategory() { const res = await request.get<IApiRes<CategoryItem[]>>('/home/category/head') this.list = res.data.result }, }, })
|
- 修改 key 的值为索引,
views/layout/components/app-header-nav.vue
。
1
| <li v-for="(item, index) in category.list" :key="index"></li>
|
配置二级路由
- 创建首页组件,
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>
|
- 配置路由规则,
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
|
- 指定二级路由入口(包括一级分类和二级分类),
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>
|
- 指定二级路由出口,
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,即隐藏二级类目。
- 修改 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 }
|
- 给一级分类加 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 }
|
- 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 },
|
- 实现显示和隐藏,
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> <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 @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; } }
} } .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 的时候添加类名。
代码
- 新建吸顶导航组件,
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>
|
- 引入吸顶导航组件,
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>
|
- 给 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>
|
- 控制吸顶组件的显示隐藏,
views/layout/components/app-header-sticky.vue
。
1 2
| <div class="app-header-sticky" :class="{show:y>=78}"></div>
|
- 修复 bug,划入一级分类导航时,吸顶的二级分类也进行了展示,
views/layout/components/app-header-sticky.vue
。
1
| <div class="container" v-show="y >= 78" />
|
吸顶重构
目标
使用 vueuse/core 重构吸顶功能。
内容
- 安装
@vueuse/core
。
- 在吸顶导航中使用,
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>
|