先说核心结论:全局在线客服悬浮球这事儿,要是靠 v-if 或 show 控制显隐,或者写在某个页面里反复挂载,那基本是给自己挖坑。正确的做法是:App 端必须走原生视图方案(plus.nativeObj.View 或 nvue),小程序必须用 cover-view 加上隐藏的 video 作为挂点,H5 就简单了,position: fixed 搞定。但最关键的是——三端逻辑必须分离,状态必须统一抽离到全局 store。

App 端:别用 Vue 组件,改用原生视图
很多开发者踩过坑:App 里用 Vue 组件写悬浮球,结果切个页面按钮就没了。原因不难理解——App 的 webview 渲染层本身就不支持跨页面的 Vue 组件“常驻”,v-if="false" 一卸载,按钮就从原生视图树里彻底消失,不会自动复活。常见现象包括:onShow 里设 this.show = true 没反应,首页有按钮、二级页就没了,真机上偶尔闪一下就消失。
解决方案其实很明确:
- 在
App.vue的onLaunch中只初始化一次,用plus.nativeObj.View创建(比如 ID 设为'float-customer'),之后只调用view.show()或view.hide()来控制显隐。 - 点击事件必须通过
view.addEventListener('click', handler)来绑定,@click在这里是无效的。 - 千万不要把按钮写在
pages.json的每个页面里,更不要在各自的mounted中重复创建。 - 如果需要拖拽,监听
touchmove后用view.setStyles({ left, top })更新位置,边界要根据uni.getSystemInfoSync().safeArea动态计算,不能写死某个px值。
微信小程序:cover-view 总被遮挡?检查父容器和事件写法
小程序的 cover-view 有个硬性约束:它必须是 video、map、camera 等原生组件的直接子节点才能生效。而 uni-app 默认没有全局挂载点,如果强行用 position: fixed 的普通 view,很容易被导航栏、tabBar 或下拉刷新容器截断,甚至穿透不可点。
这里有一组实际可行的方案:
- 在
App.vue的底部加一个隐藏的,再把cover-view插入其中作为子节点。 cover-view不支持v-if和@click,事件必须用bind:touchstart等原生写法,并且只响应其内部子节点(比如cover-image)。- 显隐控制用
hidden属性绑定(如:hidden="!store.show"),v-show在这里不适用。 - 拖动时每次更新坐标后,必须调用
coverView.setStyle({ left: x + 'px', top: y + 'px' }),只改 data 是不会生效的。
状态管理和通信:让悬浮球跨页面保持活性
悬浮球不是“每个页面加一个”,而是全局唯一的实例。它的显隐、位置、是否展开等状态,一旦散落在各页面 data 里,切页就会丢失。点击回调如果写在某个页面的 methods 里,目标页面还没加载时就会报错。
正确的做法是:
- 状态全部抽离到
Pinia、Vuex或uni.$u.store(如果用 uView 的话),在App.vue中用watch监听变化并同步到视图。 - 点击事件统一注册在
App.vue,通过uni.$emit('customer:open')广播出去。各业务页面在onLoad或onShow中用uni.$on('customer:open', handler)订阅。 - 必须警惕的是:一定要在
onUnload中调用uni.$off('customer:open'),否则重复绑定会导致多次执行,造成逻辑混乱。 - 不要在悬浮球组件里
import其他页面模块,也别调getCurrentPages()——nvue 页面拿不到 vue 页面实例。
movable-area 拖拽卡顿?吸附逻辑别硬写条件分支
很多实现直接写 if (lastX > screenWidth / 2) { goRight() },结果在窄屏手机上吸附不准,边缘留白还大。正确的做法是把按钮当圆心,算当前坐标到四边的距离,取最小值后做阈值判断。
movable-area必须设position: fixed; width: 100%; height: 100%,否则拖拽区域会错位。movable-area上加pointer-events: none,避免遮挡底层页面点击;movable-view内部内容设pointer-events: auto,否则点不动。- 初始位置建议在
mounted里用uni.getSystemInfo()获取真实屏幕宽高后再计算,别用固定像素值。 - 吸附逻辑的核心是:先算出
distToLeft = x、distToRight = screenWidth - x - width、distToTop = y、distToBottom = screenHeight - y - height,取Math.min(...)后对比阈值(比如 60rpx)来决定吸附方向。
话说回来,最容易出问题的三个地方是:App 端的权限配置、小程序 cover-view 的父容器约束、以及跨页面通信时没清理 uni.$on。这三处任何一处出错,都会导致悬浮球“看起来在,但点不着”或“切页就失联”。
