如何在Vue3中让Header组件监听Scroll事件?
Vue项目实现滚动显示/隐藏固定导航栏
问题说明
我已经用原生JS实现了固定导航栏的滚动交互:向上滚动时显示,向下滚动时隐藏,代码如下:
const body = document.body; let lastScroll = 0; window.addEventListener('scroll', () => { const currentScroll = window.scrollY; if (currentScroll <= 0) { body.classList.remove('scroll-up'); } if (currentScroll > lastScroll && !body.classList.contains('scroll-down')) { body.classList.remove('scroll-up'); body.classList.add('scroll-down'); } if (currentScroll < lastScroll && body.classList.contains('scroll-down')) { body.classList.remove('scroll-down'); body.classList.add('scroll-up'); } lastScroll = currentScroll; })
但在Vue项目的Header组件(使用Composition API)里尝试给nav添加@scroll="handleScroll"却没有效果,需要实现相同功能。以下是当前Header组件代码:
<template> <Disclosure as="nav" class="bg-white w-full border-b-2 border-disabled" v-slot="{ open }" > <div class="max-w-screen-2xl mx-auto w-full"> <div class="flex justify-between w-full h-20"> <div class="flex py-3 mx-auto justify-around w-90"> <div class="flex py-3 w-full justify-between"> <div class="flex flex-row"> <router-link to="/"> <img class="w-20 mr-5" :src="require('../assets/img/Frame@2x.png')" alt="logo" /></router-link> <div class="hidden sm:mt-2 sm:ml-6 sm:flex sm:items-center sm:space-x-8" > <router-link v-for="link in navigation" :key="link.name" v-slot="{ isExactActive }" :to="link.href" > <div class="flex flex-col items-center gap-1 capitalize cursor-pointer" :class="[ isExactActive ? 'text-primary font-semibold' : 'text-secondary text-opacity-50' ]" > {{ t(link.name) }} <span :class="[ 'w-2 h-2 rounded-full', isExactActive ? 'bg-pink' : 'bg-transparent' ]" /> </div> </router-link> </div> </div> </div> <div class="flex my-auto ml-auto flex-row gap-4 sm:mr-4"> <button v-for="lang in Language" :key="lang" @click="setLanguage(lang)" class="p-0.5 w-10 h-10" :class="{ ' border-2 rounded-full border-pink': selected === lang }" > <img class="w-8 h-8" :src="require(`../assets/img/${lang}.png`)" alt="lang" /> </button> </div> <div class="hidden md:flex my-auto flex-row gap-4 buttons-container"> <a class="block w-full px-8 py-2 text-md font-medium bg-transparent border-2 border-pink rounded-xl text-pink sm:w-auto hover:text-red-accent hover:border-red-accent focus:outline-none focus:ring" :href="`${appDomain}/login`" > {{ t('navbar.buttons.signIn') }} </a> <a class="block w-full px-8 py-2 text-md font-normal text-white rounded-xl border-2 border-transparent shadow bg-accent sm:w-auto focus:outline-none" :href="`${appDomain}`" > {{ t('navbar.buttons.join') }} </a> </div> </div> <div class="-mr-2 flex items-center sm:hidden"> <!-- Mobile menu button --> <DisclosureButton class="bg-white inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > <MenuIcon v-if="!open" class="block h-6 w-6" aria-hidden="true" /> <XIcon v-else class="block h-6 w-6" aria-hidden="true" /> </DisclosureButton> </div> </div> </div> <DisclosurePanel class="sm:hidden"> <div class="pt-2 pb-3 space-y-1 ring-none outline-none"> <a v-for="link in navigation" :key="link.name" :href="link.href" class="block px-4 py-2 text-base font-medium hover:text-primary" > <router-link :to="link.href" v-slot="{ isExactActive }"> <span v-if="isExactActive" class="text-center mt-7 bg-accent transform rounded-full z-40" style="height: 3px" /> <component :is="link.name" class="w-6 h-6 text-secondary hover:text-primary" :class="{ 'text-accent indicator': isExactActive }" > {{ t(link.name) }}</component > </router-link> </a> </div> </DisclosurePanel> </Disclosure> </template> <script lang="ts"> import { computed, defineComponent, inject } from 'vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { MenuIcon, XIcon } from '@heroicons/vue/outline' import Button from '../../../ui/src/components/Button.vue' import { useI18n } from 'vue-i18n' import { Language } from '@package/lib/types/Language' import { useStore } from 'vuex' export default defineComponent({ name: 'Header', components: { Disclosure, DisclosureButton, DisclosurePanel, MenuIcon, XIcon, Button }, setup() { const { t, locale } = useI18n({ useScope: 'global' }) const store = useStore() const navigation = [ { name: 'navbar.home', href: '/', current: true }, { name: 'navbar.about', href: '/about', current: false }, { name: 'navbar.FAQ', href: '/FAQ', current: false }, { name: 'navbar.contact', href: '/contact', current: false } ] const selected = computed((): Language => store.state.language) const setLanguage = (lang: Language): void => { locale.value = lang.toLowerCase() store.commit('setLanguage', lang) } const appDomain = inject('appDomain') return { t, navigation, setLanguage, selected, Language, appDomain } } }) </script> <style> @media (max-width: 910px) { .buttons-container { display: none; } } </style>
解决方案
核心问题
@scroll绑定在nav元素上无效,因为滚动事件是发生在window对象上(页面整体滚动),而非nav自身(除非nav有独立滚动条)。需要监听window的scroll事件。
实现步骤
- 引入Vue的
ref、onMounted、onUnmountedAPI,用于存储滚动状态和管理事件生命周期 - 编写滚动处理逻辑,复用原生JS的判断逻辑,改为操作响应式状态
- 给导航栏根元素绑定动态类,通过状态控制显示/隐藏
- 组件挂载时添加scroll监听,卸载时移除,避免内存泄漏
修改后的完整代码
Template部分(更新Disclosure的class绑定)
<template> <Disclosure as="nav" class="bg-white w-full border-b-2 border-disabled fixed top-0 left-0 z-50 transition-transform duration-300" :class="{ 'scroll-up': isScrollUp, 'scroll-down': isScrollDown }" v-slot="{ open }" > <!-- 原有模板内容不变 --> </Disclosure> </template>
Script部分(添加滚动逻辑)
<script lang="ts"> import { computed, defineComponent, inject, ref, onMounted, onUnmounted } from 'vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { MenuIcon, XIcon } from '@heroicons/vue/outline' import Button from '../../../ui/src/components/Button.vue' import { useI18n } from 'vue-i18n' import { Language } from '@package/lib/types/Language' import { useStore } from 'vuex' export default defineComponent({ name: 'Header', components: { Disclosure, DisclosureButton, DisclosurePanel, MenuIcon, XIcon, Button }, setup() { const { t, locale } = useI18n({ useScope: 'global' }) const store = useStore() // 滚动状态管理 const lastScroll = ref(0) const isScrollUp = ref(false) const isScrollDown = ref(false) const handleScroll = () => { const currentScroll = window.scrollY if (currentScroll <= 0) { isScrollUp.value = false isScrollDown.value = false return } if (currentScroll > lastScroll.value && !isScrollDown.value) { isScrollUp.value = false isScrollDown.value = true } else if (currentScroll < lastScroll.value && isScrollDown.value) { isScrollDown.value = false isScrollUp.value = true } lastScroll.value = currentScroll } // 绑定/移除滚动事件 onMounted(() => { window.addEventListener('scroll', handleScroll) }) onUnmounted(() => { window.removeEventListener('scroll', handleScroll) }) const navigation = [ { name: 'navbar.home', href: '/', current: true }, { name: 'navbar.about', href: '/about', current: false }, { name: 'navbar.FAQ', href: '/FAQ', current: false }, { name: 'navbar.contact', href: '/contact', current: false } ] const selected = computed((): Language => store.state.language) const setLanguage = (lang: Language): void => { locale.value = lang.toLowerCase() store.commit('setLanguage', lang) } const appDomain = inject('appDomain') return { t, navigation, setLanguage, selected, Language, appDomain, isScrollUp, isScrollDown } } }) </script>
补充样式(控制导航栏显示/隐藏)
<style> @media (max-width: 910px) { .buttons-container { display: none; } } /* 滚动向下时隐藏导航栏 */ .scroll-down { transform: translateY(-100%); } /* 滚动向上时显示导航栏 */ .scroll-up { transform: translateY(0); } </style>
关键说明
- 给导航栏添加
fixed top-0 left-0 z-50确保固定定位,这是滚动交互的前提 - 用
transition-transform duration-300添加平滑过渡效果,提升体验 - 通过响应式状态
isScrollUp/isScrollDown控制类名,符合Vue响应式思想 - 生命周期钩子中管理事件监听,避免内存泄漏
内容的提问来源于stack exchange,提问作者Tomas Gil Amoedo
相关产品推荐
相关产品推荐

