用quasar+vue3+组合式api 实现小米商城标题栏动画

发布时间 2023-04-11 12:09:56作者: sunshine233

先来看一下小米商城标题栏动画:

 

 

小米商城标题栏动画主要特点:

  1. 移入时二级菜单缓慢出现;
  2. 移出时二级菜单缓慢消失;
  3. 在一级菜单之间移动时,二级菜单内容直接切换,没有过渡效果。

实现思路

一、纯css实现(❌)

首先肯定是考虑 :hover,但是经过试验发现,:hover 可以实现鼠标移入移出时的过渡效果,但在一级菜单之间移动时,二级菜单总是有过渡效果。

纯css代码:

  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代码,展开查看

二、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>

 注意事项:

  1. 项目用的quasar搭建,所以有些css样式和往常不同,属性直接写在class中,不用quasar的话可以自行转化为普通css样式;
  2. script部分用的是vue3组合式api,同样可以自行转化为选项式api;
  3. 用js操作dom设置css样式时, subMenus[index].style = "max-height: 0px;overflow: hidden;" subMenus[index].style.transition = "all .5s"; 的顺序不要变,否则没有过渡效果;
  4. 上一条原因参见:https://segmentfault.com/q/1010000011829015 、 https://juejin.cn/post/7062507282046648357

css+js 实现效果: