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 = true
this.moved = false
this.startX = e.clientX
this.startY = e.clientY
this.originX = this.left
this.originY = this.top
document.addEventListener('mousemove', this.onDragging)
document.addEventListener('mouseup', this.onDragEnd)
}
移动端则是:
onTouchStart(e) {
const t = e.touches[0]
this.dragging = true
this.moved = false
this.startX = t.clientX
this.startY = t.clientY
this.originX = this.left
this.originY = this.top
document.addEventListener('touchmove', this.onTouchMove, { passive: false })
document.addEventListener('touchend', this.onTouchEnd)
}
这里的关键是:
按下时记录“起始坐标”和“按钮原始位置”,后面计算位移才有依据。
2. 计算拖动后的新位置
updatePosition(dx, dy) {
let newLeft = this.originX + dx
let newTop = this.originY + dy
const maxLeft = window.innerWidth - this.buttonWidth
const maxTop = window.innerHeight - this.buttonHeight
if (newLeft < 0) newLeft = 0
if (newTop < 0) newTop = 0
if (newLeft > maxLeft) newLeft = maxLeft
if (newTop > maxTop) newTop = maxTop
this.left = newLeft
this.top = newTop
}
这段代码的重点是边界限制。
如果不限制,用户一拖就可能把悬浮球拖出屏幕,最后找都找不到。
所以一定要把坐标限制在:
left >= 0top >= 0left <= 屏幕宽度 - 按钮宽度top <= 屏幕高度 - 按钮高度
这一步是“能拖动”变成“好用”的关键。
3. 区分拖动和点击
这是很多人容易漏掉的点。
如果按钮既能拖动又能点击,那就必须区分:
- 轻点一下:执行“回到首页”
- 明显拖动:只移动位置,不触发跳转
一般通过阈值判断:
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
this.moved = true
}
结束时再判断:
onDragEnd() {
this.dragging = false
document.removeEventListener('mousemove', this.onDragging)
document.removeEventListener('mouseup', this.onDragEnd)
if (!this.moved) {
this.goHome()
}
}
这个设计很重要,因为它避免了一个常见问题:
- 用户想拖一下
- 松手却误触跳转
加上这个判断后,交互会稳定很多。
四、移动端为什么要 preventDefault
移动端拖动时,建议在 touchmove 里阻止默认行为:
onTouchMove(e) {
if (!this.dragging) return
const t = e.touches[0]
const dx = t.clientX - this.startX
const dy = t.clientY - this.startY
if (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>
脚本
export default {
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 = true
this.moved = false
this.startX = e.clientX
this.startY = e.clientY
this.originX = this.left
this.originY = this.top
document.addEventListener('mousemove', this.onDragging)
document.addEventListener('mouseup', this.onDragEnd)
},
onDragging(e) {
if (!this.dragging) return
const dx = e.clientX - this.startX
const dy = e.clientY - this.startY
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) this.moved = true
this.updatePosition(dx, dy)
},
onDragEnd() {
this.dragging = false
document.removeEventListener('mousemove', this.onDragging)
document.removeEventListener('mouseup', this.onDragEnd)
if (!this.moved) this.goHome()
},
onTouchStart(e) {
const t = e.touches[0]
this.dragging = true
this.moved = false
this.startX = t.clientX
this.startY = t.clientY
this.originX = this.left
this.originY = this.top
document.addEventListener('touchmove', this.onTouchMove, { passive: false })
document.addEventListener('touchend', this.onTouchEnd)
},
onTouchMove(e) {
if (!this.dragging) return
const t = e.touches[0]
const dx = t.clientX - this.startX
const dy = t.clientY - this.startY
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) this.moved = true
this.updatePosition(dx, dy)
e.preventDefault()
},
onTouchEnd() {
this.dragging = false
document.removeEventListener('touchmove', this.onTouchMove)
document.removeEventListener('touchend', this.onTouchEnd)
if (!this.moved) this.goHome()
},
updatePosition(dx, dy) {
let newLeft = this.originX + dx
let newTop = this.originY + dy
const maxLeft = window.innerWidth - this.buttonWidth
const maxTop = window.innerHeight - this.buttonHeight
newLeft = Math.max(0, Math.min(newLeft, maxLeft))
newTop = Math.max(0, Math.min(newTop, maxTop))
this.left = newLeft
this.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 8px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
justify-content: center;
user-select: none;
cursor: pointer;
}
七、这类方案的优点
总结一下,这种悬浮球实现方式有几个明显优势:
- 全局常驻,路由切换不丢失
- 交互自然,支持拖动和点击
- 可控性强,方便做显示隐藏
- 层级清晰,容易压住普通内容
- 对移动端友好,体验比较稳定
如果是 H5、Vue App、政务类门户、工具类应用,这套思路都很适合。
结语
首页悬浮球看似只是一个小按钮,其实涉及了布局、事件、手势、层级和交互细节。
真正好用的实现,不是“能显示就行”,而是要做到:
- 全局稳定出现
- 拖动顺手
- 点击不误触
- 层级不打架
如果你也在做类似功能,推荐直接按“根组件挂载 + fixed 定位 + 拖拽边界限制 + 点击/拖动区分”这条路线来实现,基本就能一次做对。
更多推荐


所有评论(0)