用quasar+vue3+组合式api 实现小米商城标题栏动画
先来看一下小米商城标题栏动画:![](https://img2023.cnblogs.com/blog/2194212/202304/2194212-20230411113512630-65681366.gif)
小米商城标题栏动画主要特点:
- 移入时二级菜单缓慢出现;
- 移出时二级菜单缓慢消失;
- 在一级菜单之间移动时,二级菜单内容直接切换,没有过渡效果。
实现思路
一、纯css实现(❌)
首先肯定是考虑 :hover,但是经过试验发现,:hover 可以实现鼠标移入移出时的过渡效果,但在一级菜单之间移动时,二级菜单总是有过渡效果。
纯css代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <script setup> 2 import { ref } from "vue"; 3 4 const titles = ref([ 5 { 6 name: "小米商城", path: "", children: [] 7 }, 8 { 9 name: "Xiaomi手机", path: "", children: [ 10 { name: "小米13", path: "" }, 11 { name: "小米13Pro", path: "" }, 12 { name: "小米11 青春活力版", path: "" }, 13 ] 14 }, { 15 name: "Redmi手机", path: "", children: [ 16 { name: "红米 K60", path: "" }, 17 { name: "红米 12C", path: "" }, 18 { name: "红米 Note 12", path: "" }, 19 ] 20 }, 21 { 22 name: "电视", path: "", children: [ 23 { name: "智能电视X65", path: "" }, 24 { name: "小米透明电视", path: "" }, 25 { name: "小米电视 大师 77", path: "" }, 26 { name: "小米电视 大师 65英寸", path: "" }, 27 ] 28 }, 29 { 30 name: "笔记本", path: "", children: [ 31 { name: "Xiaomi BookAir 13", path: "" }, 32 { name: "Xiaomi Book Pro14", path: "" }, 33 ] 34 } 35 ]) 36 37 </script> 38 39 <template> 40 <q-page> 41 <header class="row q-pa-lg bg-grey justify-center"> 42 <div class="container row justify-center bg-yellow"> 43 <!-- 一级标题 --> 44 <div class="menu text-h5 col text-center" v-for="menu in titles" :key="menu.name"> 45 <span>{{ menu.name }}</span> 46 <!-- 二级标题 --> 47 <ul class="sub-menu row justify-center bg-green"> 48 <li class="q-ma-lg" v-for="submenu in menu.children" :key="submenu.name" @click="clickSubmenu(submenu)"> 49 {{ submenu.name }} 50 </li> 51 </ul> 52 </div> 53 </div> 54 </header> 55 56 <div> 57 <h4 class="text-center">模拟小米官网titlebar动画</h4> 58 <ul class="text-body1">动画特点如下: 59 <li>鼠标从container外部移入任意一级菜单(有children)时,显示二级菜单(有过渡效果)</li> 60 <li>鼠标从一级菜单(有children)移出container时,二级菜单消失(有过渡效果)</li> 61 <li>鼠标从一级菜单(有children)移入一级菜单(无children)等同于移出container:二级菜单消失(有过渡效果)</li> 62 <li>--------------- 以上可以用 纯css 实现 👆--------------</li> 63 <li>--------------- 以下要用 js 实现 👇--------------</li> 64 <li>鼠标在一级菜单(有children)之间移动时,二级菜单内容切换(没有过渡效果)</li> 65 <li>鼠标在一级菜单(有children)之间移动、然后移出container,二级菜单消失(有过渡效果)</li> 66 </ul> 67 </div> 68 </q-page> 69 </template> 70 71 <style scoped> 72 .container ul { 73 padding: 0; 74 margin: 0; 75 list-style: none; 76 } 77 78 /* 包裹所有标题tab的容器 */ 79 .container { 80 position: relative; 81 width: 100%; 82 } 83 84 /* 一级标题样式 */ 85 .menu { 86 border: 1px solid blue; 87 } 88 89 /* 二级标题样式 */ 90 .sub-menu { 91 position: absolute; 92 top: 100%; 93 left: 0; 94 right: 0; 95 } 96 97 .sub-menu { 98 /* 这里不需要设置 transition */ 99 /* 初始化,必需 */ 100 max-height: 0; 101 overflow: hidden; 102 transition: all 1s; 103 } 104 105 .menu:nth-child(n):hover .sub-menu { 106 max-height: 200px; 107 transition: all 1s; 108 } 109 </style>
二、css+js实现(✅ )
既然纯css实现不了,那么就要考虑js。主要思路是用css设置了基本样式之后:
- 用js监听:当鼠标在一级菜单之间移入移出时,如果是一级菜单之间的切换(没有移出包裹一级菜单的容器),就设置 transition:all 0s; 反之则设置 transition:all 1s;
- 选择js监听事件时,有mouseenter、mouseleave、mouseover、mouseout可选择。经测试,使用mouseover/ mouseout有闪烁问题(https://blog.csdn.net/tianjiliuhen/article/details/106340534),因此选择 mouseenter+ mouseleave。
- mouseenter:当一个定点设备(通常指鼠标)第一次移动到触发事件元素中的激活区域时触发;
- mouseleave:事件在定点设备(通常是鼠标)的指针移出某个元素时被触发。
- mouseover:当鼠标移动到一个元素上时,会在这个元素上触发 mouseover 事件。
- mouseout:当移动指针设备(通常是鼠标),使指针不再包含在这个元素或其子元素中时,mouseout 事件被触发。
html部分
<template> <q-page> <header class="row q-pa-lg bg-grey justify-center"> <div class="container row justify-center bg-yellow"> <!-- 一级标题 --> <div class="menu text-h5 col text-center" v-for="menu in titles" :key="menu.name"> <span>{{ menu.name }}</span> <!-- 二级标题 --> <ul class="sub-menu row justify-center bg-green"> <li class="q-ma-lg" v-for="submenu in menu.children" :key="submenu.name" @click="clickSubmenu(submenu)"> {{ submenu.name }} </li> </ul> </div> </div> </header> <div> <h4 class="text-center">模拟小米官网titlebar动画</h4> <ul class="text-body1">动画特点如下: <li>鼠标从container外部移入任意一级菜单(有children)时,显示二级菜单(有过渡效果)</li> <li>鼠标从一级菜单(有children)移出container时,二级菜单消失(有过渡效果)</li> <li>鼠标从一级菜单(有children)移入一级菜单(无children)等同于移出container:二级菜单消失(有过渡效果)</li> <li>--------------- 以上可以用 纯css 实现 👆--------------</li> <li>--------------- 以下要用 js 实现 👇--------------</li> <li>鼠标在一级菜单(有children)之间移动时,二级菜单内容切换(没有过渡效果)</li> <li>鼠标在一级菜单(有children)之间移动、然后移出container,二级菜单消失(有过渡效果)</li> </ul> </div> </q-page> </template>
css部分
<style scoped> .container ul { padding: 0; margin: 0; list-style: none; } /* 包裹所有标题tab的容器 */ .container { position: relative; width: 100%; } /* 一级标题样式 */ .menu { border: 1px solid blue; } /* 二级标题样式 */ .sub-menu { position: absolute; top: 100%; left: 0; right: 0; } .sub-menu { /* 这里不需要设置 transition */ /* 初始化,必需 */ max-height: 0; overflow: hidden; } </style>
js部分(以下“一级菜单”用menus、menu表示,包裹所有一级菜单的容器用 container 表示,“二级菜单”用 subMenus、submenu表示):
首先找到dom元素,并新建一个数组变量记录已经被 hover 过的一级元素 menu:
let container = document.querySelector(".container"); let menus = document.querySelectorAll(".menu"); let subMenus = document.querySelectorAll(".sub-menu"); let checkedMenus = ref([false]); initCheckMenu(); /* 初始化 checkedMenus ,全都设为 false ,即此时所有 menu 都没有被hover */ function initCheckMenu() { // index = 0 始终是false,因为它没有 children checkedMenus = ref([false]); for (let i = 1; i < menus.length; i++) { checkedMenus.value.push(false); } }
因为默认情况下鼠标肯定是从 container 外移入 menu 的,所以刚开始就要设定过度时间为 0.5s:
setSubMenuLeaveTrans(); /* 设置所有 submenu 的过渡效果 */ function setSubMenuLeaveTrans() { menus.forEach((menu, index) => { // 设置 css 的代码顺序不能变,否则没有过渡效果 subMenus[index].style = "max-height: 0px;overflow: hidden;" subMenus[index].style.transition = "all .5s"; }) }
然后可以先考虑鼠标移出 container 的情况,此时肯定有过渡效果。而且鼠标离开 container 后,记录有几个 menu 被hover过的变量 checkedMenus 要恢复默认值全false。所以先监听 container 的鼠标离开事件:
// 离开 container-nav(一定离开了 menu),离开需要动画 container.addEventListener("mouseleave", () => { setSubMenuLeaveTrans(); initCheckMenu(); }) /* 初始化 checkedMenus ,全都设为 false ,即此时所有 menu 都没有被hover */ function initCheckMenu() { // index = 0 始终是false,因为它没有 children checkedMenus = ref([false]); for (let i = 1; i < menus.length; i++) { checkedMenus.value.push(false); } } /* 设置所有 submenu 的过渡效果 */ function setSubMenuLeaveTrans() { menus.forEach((menu, index) => { // 设置 css 的代码顺序不能变,否则没有过渡效果 subMenus[index].style = "max-height: 0px;overflow: hidden;" subMenus[index].style.transition = "all .5s"; }) }
下一步考虑鼠标进入和离开 menu 的事件:
menus.forEach((menu, index) => { // 离开 menus[index] 但没有离开 container-nav menu.addEventListener("mouseleave", () => { subMenus[index].style = "max-height: 0px;overflow: hidden;" if (index == 1) { /* 如果离开的是第2个menu(第一个menu没有子菜单): -- 移出时menu,但没有移出container: 需要动画 -- 移出时menu + 移出container: 需要动画 */ subMenus[index].style.transition = "all .5s"; } else { /* 如果离开的其其她menu: -- 移出时menu,但没有移出container:必然是在 tab 之间移动,不需要动画 -- 移出时menu + 移出container: 被 container.mouseleave 的动画覆盖 */ subMenus[index].style.transition = "all 0s"; } }) menu.addEventListener("mouseenter", () => { if (index != 0) { checkedMenus.value[index] = true; subMenus[index].style = "max-height: 200px;"; } else { initCheckMenu(); } // console.log('checkedMenus', checkedMenus.value); if (countCheckedMenu(checkedMenus.value) > 1) { // console.log('已经有被选中的 menu,hover不需要动画'); subMenus[index].style.transition = "all 0s"; } else { // console.log('没有被选中的 menu,hover需要动画'); subMenus[index].style.transition = "all .5s"; } }) })
css+js 全部代码如下:
<script setup> import { ref } from "vue"; import { onMounted } from "vue"; const titles = ref([ { name: "小米商城", path: "", children: [] }, { name: "Xiaomi手机", path: "", children: [ { name: "小米13", path: "" }, { name: "小米13Pro", path: "" }, { name: "小米11 青春活力版", path: "" }, ] }, { name: "Redmi手机", path: "", children: [ { name: "红米 K60", path: "" }, { name: "红米 12C", path: "" }, { name: "红米 Note 12", path: "" }, ] }, { name: "电视", path: "", children: [ { name: "智能电视X65", path: "" }, { name: "小米透明电视", path: "" }, { name: "小米电视 大师 77", path: "" }, { name: "小米电视 大师 65英寸", path: "" }, ] }, { name: "笔记本", path: "", children: [ { name: "Xiaomi BookAir 13", path: "" }, { name: "Xiaomi Book Pro14", path: "" }, ] } ]) onMounted(() => { let container = document.querySelector(".container"); let menus = document.querySelectorAll(".menu"); let subMenus = document.querySelectorAll(".sub-menu"); let checkedMenus = ref([false]); initCheckMenu(); setSubMenuLeaveTrans(); // 离开 container-nav(一定离开了 menu),离开需要动画 container.addEventListener("mouseleave", () => { setSubMenuLeaveTrans(); initCheckMenu(); }) menus.forEach((menu, index) => { // 离开 menus[index] 但没有离开 container-nav menu.addEventListener("mouseleave", () => { subMenus[index].style = "max-height: 0px;overflow: hidden;" if (index == 1) { /* 如果离开的是第2个menu(第一个menu没有子菜单): -- 移出时menu,但没有移出container: 需要动画 -- 移出时menu + 移出container: 需要动画 */ subMenus[index].style.transition = "all .5s"; } else { /* 如果离开的其其她menu: -- 移出时menu,但没有移出container:必然是在 tab 之间移动,不需要动画 -- 移出时menu + 移出container: 被 container.mouseleave 的动画覆盖 */ subMenus[index].style.transition = "all 0s"; } }) menu.addEventListener("mouseenter", () => { if (index != 0) { checkedMenus.value[index] = true; subMenus[index].style = "max-height: 200px;"; } else { initCheckMenu(); } // console.log('checkedMenus', checkedMenus.value); if (countCheckedMenu(checkedMenus.value) > 1) { // console.log('已经有被选中的 menu,hover不需要动画'); subMenus[index].style.transition = "all 0s"; } else { // console.log('没有被选中的 menu,hover需要动画'); subMenus[index].style.transition = "all .5s"; } }) }) /* 初始化 checkedMenus ,全都设为 false ,即此时所有 menu 都没有被hover */ function initCheckMenu() { // index = 0 始终是false,因为它没有 children checkedMenus = ref([false]); for (let i = 1; i < menus.length; i++) { checkedMenus.value.push(false); } } /* 设置所有 submenu 的过渡效果 */ function setSubMenuLeaveTrans() { menus.forEach((menu, index) => { // 设置 css 的代码顺序不能变,否则没有过渡效果 subMenus[index].style = "max-height: 0px;overflow: hidden;" subMenus[index].style.transition = "all .5s"; }) } /* 计算共有几个menu被hover过 */ function countCheckedMenu(array) { return array.filter((e) => e == true).length; } }) function clickSubmenu(submenu) { console.log(submenu.name); } </script> <template> <q-page> <header class="row q-pa-lg bg-grey justify-center"> <div class="container row justify-center bg-yellow"> <!-- 一级标题 --> <div class="menu text-h5 col text-center" v-for="menu in titles" :key="menu.name"> <span>{{ menu.name }}</span> <!-- 二级标题 --> <ul class="sub-menu row justify-center bg-green"> <li class="q-ma-lg" v-for="submenu in menu.children" :key="submenu.name" @click="clickSubmenu(submenu)"> {{ submenu.name }} </li> </ul> </div> </div> </header> <div> <h4 class="text-center">模拟小米官网titlebar动画</h4> <ul class="text-body1">动画特点如下: <li>鼠标从container外部移入任意一级菜单(有children)时,显示二级菜单(有过渡效果)</li> <li>鼠标从一级菜单(有children)移出container时,二级菜单消失(有过渡效果)</li> <li>鼠标从一级菜单(有children)移入一级菜单(无children)等同于移出container:二级菜单消失(有过渡效果)</li> <li>--------------- 以上可以用 纯css 实现 👆--------------</li> <li>--------------- 以下要用 js 实现 👇--------------</li> <li>鼠标在一级菜单(有children)之间移动时,二级菜单内容切换(没有过渡效果)</li> <li>鼠标在一级菜单(有children)之间移动、然后移出container,二级菜单消失(有过渡效果)</li> </ul> </div> </q-page> </template> <style scoped> .container ul { padding: 0; margin: 0; list-style: none; } /* 包裹所有标题tab的容器 */ .container { position: relative; width: 100%; } /* 一级标题样式 */ .menu { border: 1px solid blue; } /* 二级标题样式 */ .sub-menu { position: absolute; top: 100%; left: 0; right: 0; } .sub-menu { /* 这里不需要设置 transition */ /* 初始化,必需 */ max-height: 0; overflow: hidden; } </style>
注意事项:
- 项目用的quasar搭建,所以有些css样式和往常不同,属性直接写在class中,不用quasar的话可以自行转化为普通css样式;
-
script部分用的是vue3组合式api,同样可以自行转化为选项式api;
- 用js操作dom设置css样式时, subMenus[index].style = "max-height: 0px;overflow: hidden;" subMenus[index].style.transition = "all .5s"; 的顺序不要变,否则没有过渡效果;
- 上一条原因参见:https://segmentfault.com/q/1010000011829015 、 https://juejin.cn/post/7062507282046648357
css+js 实现效果: