Vue3 + Element Plus 实现点击导航平滑滚动到页面指定位置

在现代 Web 应用开发中,单页应用(SPA)越来越流行。为了提升用户体验,我们常常需要实现"点击导航菜单 → 平滑滚动到页面对应区块"的功能。本文将基于 Vue 3 + Composition API + Element Plus,结合你提供的代码,深入剖析如何通过自定义事件总线(Event Bus)与 DOM 查询,实现跨组件的精准、平滑滚动,并兼顾 PC 端与移动端的差异化体验。


一、需求场景

index.vue(联系页面)中,顶部有三个 Tab:

  • 联系总部
  • 办公地址
  • 在线留言

用户点击任一 Tab,页面应平滑滚动 至下方对应的 .headquarters.address.message 区块。

而在 App.vue(根布局组件)中,已经封装了全局的滚动容器(.layout)和返回顶部逻辑。

关键挑战

  1. 两个组件无直接父子关系,如何通信?
  2. 滚动容器不是 window,而是自定义的 .layout 元素。
  3. 需要处理不同设备下的偏移量(如移动端需更大顶部留白)。

二、技术方案:事件总线 + DOM 定位 + 平滑滚动

1. 使用 mitt 创建轻量级事件总线

注:虽然 Vue 官方不推荐在大型项目中使用 Event Bus,但对于简单跨组件通信(如本例),它简洁高效。

复制代码
// utils/eventbus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter

2. 发送滚动指令(index.vue

当用户点击 Tab 时,通过 emitter.emit 发送一个包含目标标识的事件:

复制代码
<!-- index.vue -->
<script setup>
import emitter from "@/utils/eventbus.js"

const handleChose = (val) => {
  currentTab.value = val
  // 发送滚动事件,val 为 1/2/3
  emitter.emit("scroll", currentTab.value)
}
</script>

3. 监听并执行滚动(App.vue

App.vueonMounted 中监听 "scroll" 事件,并根据传入的 val 映射到具体的 DOM 选择器:

复制代码
// App.vue
emitter.on("scroll", (val) => {
  let targetSelector
  switch (val) {
    case 1: targetSelector = '.headquarters'; break
    case 2: targetSelector = '.address'; break
    case 3: targetSelector = '.message'; break
    default: return
  }

  const targetElement = document.querySelector(targetSelector)
  if (targetElement && layout.value) {
    // 获取目标元素相对于视口的位置
    const rect = targetElement.getBoundingClientRect()
    // 获取滚动容器(.layout)相对于视口的位置
    const layoutRect = layout.value.getBoundingClientRect()
    
    // 计算目标元素相对于滚动容器的偏移量
    const relativeTop = rect.top - layoutRect.top
    
    // 当前已滚动的距离
    const currentScrollTop = layout.value.scrollTop
    
    // 根据设备类型设置顶部偏移(避免被固定头部遮挡)
    const offset = isMobile.value ? 100 : 50
    
    // 最终滚动位置 = 当前滚动位置 + 相对偏移 - 顶部留白
    const targetPosition = currentScrollTop + relativeTop - offset

    // 使用原生 smooth 滚动
    layout.value.scrollTo({
      top: targetPosition,
      behavior: "smooth"
    })
  }
})

关键点解析

  • getBoundingClientRect() 返回元素相对于视口的位置。
  • 由于滚动发生在 .layout 内部,而非整个窗口,因此必须计算 目标元素相对于滚动容器的位置
  • offset 是为了防止内容被 Header 遮挡,在移动端预留更多空间(100px vs 50px)。

三、为什么不用 window.scrollTo

因为你的页面结构是:

复制代码
<div class="layout" style="overflow-y: auto; height: 100vh;">
  <Header />
  <div class="content"><RouterView /></div>
  <Footer />
</div>

真正的滚动条属于 .layout 元素 ,而不是 bodywindow。所以必须操作 layout.value.scrollTop 或调用其 scrollTo 方法。


四、移动端适配处理

通过 useIsMobile Hook 判断设备类型:

复制代码
// hooks/useIsMobile.js
import { ref, onMounted } from 'vue'

export function useIsMobile() {
  const isMobile = ref(false)
  onMounted(() => {
    isMobile.value = window.innerWidth <= 767
  })
  return { isMobile }
}

然后在计算 offset 时动态调整:

复制代码
const offset = isMobile.value ? 100 : 50

这样在手机上滚动后,内容不会紧贴顶部,留出足够空间供用户查看。


五、完整流程图

复制代码
[用户点击 Tab]
        ↓
(index.vue) emit("scroll", 1)
        ↓
(App.vue) 监听到事件
        ↓
根据 val=1 → 选择器 '.headquarters'
        ↓
querySelector 获取 DOM 元素
        ↓
计算该元素在 .layout 中的相对位置
        ↓
加上设备适配偏移量(PC:50px, Mobile:100px)
        ↓
调用 layout.scrollTo({ top: ..., behavior: 'smooth' })
        ↓
页面平滑滚动到目标区域

六、总结

本文通过分析真实项目代码,展示了如何在 Vue 3 项目中实现跨组件、容器内、响应式的平滑滚动导航。核心在于:

  • 事件解耦 :使用 mitt 实现松耦合通信;
  • 精准定位 :利用 getBoundingClientRect 计算相对位置;
  • 体验优先:区分 PC/Mobile 设置不同偏移量;
  • 原生能力 :直接使用 element.scrollTo({ behavior: 'smooth' }),无需第三方库。

这种模式适用于任何需要"锚点跳转"但又不在 window 滚动的 SPA 场景,值得收藏复用!

Vue 3 实战:路由跳转后自动滚动到顶部或指定位置的完整解决方案

相关推荐
小王努力学编程2 小时前
LangChain——AI应用开发框架(核心组件1)
linux·服务器·前端·数据库·c++·人工智能·langchain
pas1362 小时前
35-mini-vue 实现组件更新功能
前端·javascript·vue.js
前端达人2 小时前
为什么聪明的工程师都在用TypeScript写AI辅助代码?
前端·javascript·人工智能·typescript·ecmascript
快乐点吧2 小时前
使用 data-属性和 CSS 属性选择器实现状态样式控制
前端·css
EndingCoder3 小时前
属性和参数装饰器
java·linux·前端·ubuntu·typescript
小二·3 小时前
Python Web 开发进阶实战:量子机器学习实验平台 —— 在 Flask + Vue 中集成 Qiskit 构建混合量子-经典 AI 应用
前端·人工智能·python
TTGGGFF3 小时前
控制系统建模仿真(十):实战篇——从工具掌握到工程化落地
前端·javascript·ajax
郝学胜-神的一滴3 小时前
深入解析C/S架构与B/S架构:技术选型与应用实践
c语言·开发语言·前端·javascript·程序人生·架构
s19134838482d4 小时前
javascript练习题
开发语言·javascript·ecmascript