Vue/H5 通用首页悬浮球实现:可拖动、全局常驻、遮罩层上方显示
在很多移动端项目里,都会有一个“回到首页”的悬浮球。它通常有几个要求:
- 全局都能看到
- 可以随手拖动,不挡内容
- 不会因为页面切换而消失
- 能稳定悬在普通内容和多数遮罩层之上
这类需求看起来简单,真做起来其实有几个关键点:挂载位置、拖拽逻辑、层级控制、点击与拖动的区分。
一、整体思路
一个通用的首页悬浮球,推荐放在应用的根布局层,而不是单页组件里。
核心原因很简单:
- 放在根组件里,路由切换时不会被销毁
- 可以统一控制显示隐藏
- 方便做全局层级管理
实现上一般分四步:
- 在全局布局中渲染悬浮球
- 使用
position: fixed固定到视口 - 用鼠标/触摸事件实现拖动
- 用
z-index保证它盖在大部分页面内容之上
二、为什么要放在根布局里
如果你把按钮写在某个页面里,那么跳转路由之后它很可能就没了。
所以更稳的做法是把它放到应用根部,比如:
<template> <div id="app"> <FloatingHomeButton v-if="showFloatingBtn" /> <router-view /> </div> </template>这样做的好处是:
- 只渲染一次
- 所有页面共享同一个悬浮球
- 页面切换不会影响位置和状态
如果还想按页面控制显示,可以监听路由:
watch:{$route(to){this.showFloatingBtn=!['login','register','error'].includes(to.name)}}三、拖动的核心实现
拖动本质上就是三件事:
- 记录按下时的位置
- 计算移动偏移
- 更新按钮坐标
1. 记录起点
onDragStart(e){this.dragging=truethis.moved=falsethis.startX=e.clientXthis.startY=e.clientYthis.originX=this.leftthis.originY=this.top document.addEventListener('mousemove',this.onDragging)document.addEventListener('mouseup',this.onDragEnd)}移动端则是:
onTouchStart(e){constt=e.touches[0]this.dragging=truethis.moved=falsethis.startX=t.clientXthis.startY=t.clientYthis.originX=this.leftthis.originY=this.top document.addEventListener('touchmove',this.onTouchMove,{passive:false})document.addEventListener('touchend',this.onTouchEnd)}这里的关键是:
按下时记录“起始坐标”和“按钮原始位置”,后面计算位移才有依据。
2. 计算拖动后的新位置
updatePosition(dx,dy){letnewLeft=this.originX+dxletnewTop=this.originY+dyconstmaxLeft=window.innerWidth-this.buttonWidthconstmaxTop=window.innerHeight-this.buttonHeightif(newLeft<0)newLeft=0if(newTop<0)newTop=0if(newLeft>maxLeft)newLeft=maxLeftif(newTop>maxTop)newTop=maxTopthis.left=newLeftthis.top=newTop}这段代码的重点是边界限制。
如果不限制,用户一拖就可能把悬浮球拖出屏幕,最后找都找不到。
所以一定要把坐标限制在:
left >= 0top >= 0left <= 屏幕宽度 - 按钮宽度top <= 屏幕高度 - 按钮高度
这一步是“能拖动”变成“好用”的关键。
3. 区分拖动和点击
这是很多人容易漏掉的点。
如果按钮既能拖动又能点击,那就必须区分:
- 轻点一下:执行“回到首页”
- 明显拖动:只移动位置,不触发跳转
一般通过阈值判断:
if(Math.abs(dx)>5||Math.abs(dy)>5){this.moved=true}结束时再判断:
onDragEnd(){this.dragging=falsedocument.removeEventListener('mousemove',this.onDragging)document.removeEventListener('mouseup',this.onDragEnd)if(!this.moved){this.goHome()}}这个设计很重要,因为它避免了一个常见问题:
- 用户想拖一下
- 松手却误触跳转
加上这个判断后,交互会稳定很多。
四、移动端为什么要preventDefault
移动端拖动时,建议在touchmove里阻止默认行为:
onTouchMove(e){if(!this.dragging)returnconstt=e.touches[0]constdx=t.clientX-this.startXconstdy=t.clientY-this.startYif(Math.abs(dx)>5||Math.abs(dy)>5){this.moved=true}this.updatePosition(dx,dy)e.preventDefault()}原因很简单:
- 防止页面跟着一起滚动
- 防止手势冲突
- 提升拖拽手感
如果你做的是 H5 或移动 Web,这一步基本是必做的。
五、为什么它能悬在所有页面上方
这个问题的答案其实就两个字:定位。
1.position: fixed
.floating-home-btn{position:fixed;right:16px;bottom:20%;}fixed的好处是:
- 相对浏览器视口定位
- 不受页面滚动影响
- 不参与普通文档流
所以它天然就是“浮起来”的。
2.z-index足够高
.floating-home-btn{z-index:3000;}z-index决定了它的层级顺序。
只要它比普通页面内容更高,就能显示在上面。
不过要注意:
- 有些弹窗、遮罩、loading 也会用很高的
z-index - 如果业务里层级很多,最好统一管理
比如:
.page-loading-mask{z-index:4000!important;}这类做法表示:
悬浮球不是绝对最高,而是通过层级体系去协调。
六、一个通用版示例
下面给一个可以直接参考的通用结构。
模板
<template> <div class="floating-home-btn" ref="btn" :style="{ left: left + 'px', top: top + 'px' }" @mousedown="onDragStart" @touchstart.prevent="onTouchStart" @click="goHome" > <span>首页</span> </div> </template>脚本
exportdefault{data(){return{left:300,top:400,dragging:false,moved:false,startX:0,startY:0,originX:0,originY:0,buttonWidth:50,buttonHeight:50}},methods:{onDragStart(e){this.dragging=truethis.moved=falsethis.startX=e.clientXthis.startY=e.clientYthis.originX=this.leftthis.originY=this.top document.addEventListener('mousemove',this.onDragging)document.addEventListener('mouseup',this.onDragEnd)},onDragging(e){if(!this.dragging)returnconstdx=e.clientX-this.startXconstdy=e.clientY-this.startYif(Math.abs(dx)>5||Math.abs(dy)>5)this.moved=truethis.updatePosition(dx,dy)},onDragEnd(){this.dragging=falsedocument.removeEventListener('mousemove',this.onDragging)document.removeEventListener('mouseup',this.onDragEnd)if(!this.moved)this.goHome()},onTouchStart(e){constt=e.touches[0]this.dragging=truethis.moved=falsethis.startX=t.clientXthis.startY=t.clientYthis.originX=this.leftthis.originY=this.top document.addEventListener('touchmove',this.onTouchMove,{passive:false})document.addEventListener('touchend',this.onTouchEnd)},onTouchMove(e){if(!this.dragging)returnconstt=e.touches[0]constdx=t.clientX-this.startXconstdy=t.clientY-this.startYif(Math.abs(dx)>5||Math.abs(dy)>5)this.moved=truethis.updatePosition(dx,dy)e.preventDefault()},onTouchEnd(){this.dragging=falsedocument.removeEventListener('touchmove',this.onTouchMove)document.removeEventListener('touchend',this.onTouchEnd)if(!this.moved)this.goHome()},updatePosition(dx,dy){letnewLeft=this.originX+dxletnewTop=this.originY+dyconstmaxLeft=window.innerWidth-this.buttonWidthconstmaxTop=window.innerHeight-this.buttonHeight newLeft=Math.max(0,Math.min(newLeft,maxLeft))newTop=Math.max(0,Math.min(newTop,maxTop))this.left=newLeftthis.top=newTop},goHome(){this.$router.push({name:'home'})}}}样式
.floating-home-btn{position:fixed;width:50px;height:50px;right:16px;bottom:20%;z-index:3000;border-radius:50%;background:#fff;box-shadow:0 0 8pxrgba(0,0,0,0.12);display:flex;align-items:center;justify-content:center;user-select:none;cursor:pointer;}七、这类方案的优点
总结一下,这种悬浮球实现方式有几个明显优势:
- 全局常驻,路由切换不丢失
- 交互自然,支持拖动和点击
- 可控性强,方便做显示隐藏
- 层级清晰,容易压住普通内容
- 对移动端友好,体验比较稳定
如果是 H5、Vue App、政务类门户、工具类应用,这套思路都很适合。
结语
首页悬浮球看似只是一个小按钮,其实涉及了布局、事件、手势、层级和交互细节。
真正好用的实现,不是“能显示就行”,而是要做到:
- 全局稳定出现
- 拖动顺手
- 点击不误触
- 层级不打架
如果你也在做类似功能,推荐直接按“根组件挂载 + fixed 定位 + 拖拽边界限制 + 点击/拖动区分”这条路线来实现,基本就能一次做对。
