直接手写 touchstart + touchmove + splice 是唯一跨端稳定的九宫格拖拽方案,因 movable-view 不支持 v-for 动态列表、坐标映射失效、尺寸固定且跨端行为不一致,而手写方案通过实时查位置、中线 hover 判定、二维转一维索引及防抖 splice 实现稳定交互。

直接手写 touchstart + touchmove + splice 是唯一跨端稳定的方案;用 movable-view 或第三方 uni-grid 插件,在小程序真机上必掉序、H5 上必错位,补空、换行、hover 判定全崩。
为什么不能用 movable-view 做九宫格拖拽
movable-view 只支持单个可移动块,不支持 v-for 渲染的动态列表——官方文档明确写了这点。你强行塞进去,结果是:
- 小程序里
touchstart后事件丢失,松手顺序不变,甚至整个页面失焦 - H5 看似能动,但
change回调只返回像素值x/y,没法映射回数组索引 - 无法实现“拖着一张图时,其他图自动让出空位”这个核心交互
- 每个
movable-view必须显式设width/height,而九宫格图片尺寸常动态,对齐难
touchmove 坐标必须归一化,否则 iOS 小程序和 H5 表现完全不一致
小程序(尤其 iOS)的 clientX/clientY 是相对于屏幕左上角,H5 是相对于视口;不做转换,手指一动图就飞出屏幕外。
- 永远别用
pageX/pageY——小程序里恒为0 - 在
touchstart里立刻存this.startY = event.touches[0].clientY,并加event.preventDefault() - 每次
touchmove都要重新调用uni.createSelectorQuery().select('.grid-item').boundingClientRect()查位置,不能缓存——滚动或数据更新后位置已变 - 判断 hover 位置时,比对的是触摸 Y 坐标与每个
rect.top + rect.height / 2(中线),不是rect.top或rect.bottom
交换数组必须用两段 splice,且要防抖 hover 触发
九宫格是二维布局(比如每行 3 列),但数组是一维的。你得把“当前拖到第 2 行第 1 列”转成一维索引,再考虑拖拽方向是否导致原索引偏移。
- 先用
Math.floor(index / 3)算行号,index % 3算列号,反向推目标位置也按同样规则映射 - 只在触摸 Y 跨过相邻项中线时触发一次交换,用
this.lastSwappedIndex记录上一次交换目标,避免touchmove高频重复调用splice - 交换必须用:
this.list.splice(newIndex, 0, this.list.splice(oldIndex, 1)[0]);如果oldIndex > newIndex,删掉前面一项后,newIndex后面所有索引自动减 1,所以插入位置就是newIndex,不用手动减 - 禁用
scroll-view滚动(加scroll-y="false"),否则 iOS 小程序会抢 touch 事件
九宫格渲染层必须用 flex-wrap,且 padding 代替 margin
别碰 display: grid——小程序基础库低于 2.17.0 时直接不识别,H5 端 gap 在 iOS 和安卓行为也不一致。
- 父容器必须设
flex-wrap: wrap,漏掉它子项会强行挤在一行溢出,iOS 微信里尤其明显 - 子项宽度写成
flex: 0 0 calc(33.333% - 2 * 8rpx),显式扣掉左右padding,避免box-sizing失效导致总宽超 100% - 用父容器
padding: 0 10rpx+ 子项padding: 20rpx 10rpx控制间隙,别用margin——会折叠、跨端计算偏差大 - 数据长度不是 3 的倍数时,必须后端或前端补空项(如
{ icon: '', text: '' }),否则最后一行只剩 1–2 个格子,视觉断裂
最易被忽略的是:拖拽过程中没禁用滚动、没节流 hover、没重查 boundingClientRect、没处理 oldIndex > newIndex 时的索引偏移——这四点只要漏一个,真机上就卡顿或错位,而且只在特定机型复现,调试极难定位。


















