You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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事件。

实现步骤

  1. 引入Vue的ref、onMounted、onUnmountedAPI,用于存储滚动状态和管理事件生命周期
  2. 编写滚动处理逻辑,复用原生JS的判断逻辑,改为操作响应式状态
  3. 给导航栏根元素绑定动态类,通过状态控制显示/隐藏
  4. 组件挂载时添加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 21:39:18