n-draggable 拖拽排序
平台兼容性
| App-Android | App-iOS | App-Harmony | Web | 微信小程序 |
|---|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ | ✓ |
使用说明
列表与网格的拖拽排序:长按(默认)或触摸进入拖拽,拖拽项实时跟手,其余项按插入位置平滑让位,松手落位并回发重排后的新数组。支持单列列表与多列网格(columns)、方向限定(direction)、手柄触发(#handle 插槽)、逐项禁用(数据项 disabled 字段)与可关闭的九宫格编辑(closeable)。
html
<template>
<n-draggable v-model="apps" :columns="2" :long-press="false">
<template #item="{ item }">
<view class="grid-item">
<text class="grid-text">{{ item['text'] }}</text>
</view>
</template>
</n-draggable>
</template>
<script setup lang="uts">
import { ref } from "vue";
import { type NDraggableItem } from "@/uni_modules/new-ui/components/n-draggable/props";
const apps = ref<NDraggableItem[]>([
{ id: 1, text: "微信" },
{ id: 2, text: "支付宝", disabled: true },
{ id: 3, text: "浏览器" },
{ id: 4, text: "相册" }
]);
</script>
拖拽触发两种方式:
- 长按(默认
long-press为 true):按住约 350ms 进入拖拽,适合页面可滚动的列表场景,不会与滚动冲突。 - 触摸即拖(
long-press为 false):手指按下移动即拖拽,适合无滚动的九宫格编辑区。
数据项只读:排序与移除都在组件内部的数组副本上完成,传入的数组与项对象不会被就地修改;完成时通过 update:modelValue / change 回发重排后的新数组,直接赋回即可。
数据项可用字段(组件按约定读取):
| 字段 | 类型 | 说明 |
|---|---|---|
| disabled | boolean | 为 true 时该项不可拖拽、呈禁用视觉 |
手柄触发:提供 #handle 插槽后,只有手柄区域可以触发拖拽(长按/触摸模式同样受 long-press 控制),适合行内容需要保留点击、滑动等交互的列表。
html
<n-draggable v-model="cities" direction="vertical">
<template #item="{ item }">
<view class="row-item">
<text class="row-text">{{ item['name'] }}</text>
</view>
</template>
<template #handle>
<view class="row-handle">
<n-icon name="drag" :size="16" color="#9ca3af" />
</view>
</template>
</n-draggable>
实现说明(机制对照)
| 机制 | 参考做法 | 本库做法 | 分歧理由 |
|---|---|---|---|
| 值通道 | 一参考 modelValue v-model,另一参考 list 单向属性 + change |
modelValue v-model + change 同参同发 |
v-model 是库内统一规则;排序完成整体回发新数组 |
| 数据项改写 | 参考直接向使用者数据项注入内部 uid 字段 | 数据项只读,排序在内部数组副本完成 | 根治参考改写使用者数据对象的副作用 |
| 行高来源 | 一参考逐项实测几何;另一参考固定 itemHeight(默认 60)近似 |
拖拽启动时逐项实测,让位位移取「新旧位置几何差值」 | 实测 + 差值在异构行高下同样正确,固定行高近似无存在价值,不收录 itemHeight |
| 防误触 | 中心移出原项区域才启动排序预览 | 同左(sortingStarted 门槛) | 轻触/微动不触发重排,两参考一致 |
| 插入判定 | 单列按纵向最近中心、网格按欧氏最近中心 | 同左;direction 显式限定时用对应轴 |
并集保留方向限定,并让它参与判定轴选择 |
| 松手落位 | 数组重排 + 位移清零,落位瞬间与内容重排同步完成 | 同左 | 让位过渡已提供过程动画,落位跳变与重排同帧发生,感知为「就位」 |
| 触摸事件绑定 | 参考将 touchmove 直接绑在项上 | 非微信端绑在项上、拖拽中才调用 preventDefault 阻断滚动;微信端「触摸即拖」变体编译为 catch 阻断、「长按」变体不阻断 | 列表项是页面滚动的常规起点,未拖拽时必须放行滚动;微信端运行时 preventDefault 不生效,只能编译期 catch,「长按 + 页面滚动」组合在微信端为已知限制(拖拽功能不受影响) |
| 长按默认值 | 一参考默认 true、另一参考默认 false | 默认 true | 列表场景防误触优先;无滚动的九宫格由使用者显式传 false |
| 项 key | 小程序用下标、其他端用注入的 uid | 三端统一用下标 | 样式与位移全部按下标声明式推导,下标 key 下节点内容随重排整体更新,行为确定且无注入字段 |
| 关闭钮 | 参考提供 closeable 显示关闭钮 + close 事件 |
点击后组件自行移除该项并回发新数组,同时发 close(index) | 按参考演示行为落地,外部无需再操作数据 |
| 触觉反馈 | 参考经宿主全局事件触发震动 | 进入拖拽时调用 uni.vibrateShort | 官方振动 API,三端可用;宿主自定义事件机制不通用 |
API
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| model-value | NDraggableItem[] | [] | 数据数组,供 v-model 使用;排序完成时组件回发重排后的新数组(原数组不被就地修改) |
| columns | number | 1 | 列数:1 为单列纵向列表,大于 1 时为网格布局(每项宽度 = 100/列数) |
| direction | NDraggableDirection | "all" | 拖拽方向限定:all 自由、vertical 仅纵向、horizontal 仅横向;同时决定插入位置判定轴 |
| long-press | boolean | true | 是否长按触发拖拽;false 为触摸即拖 |
| disabled | boolean | false | 组件级禁用:整组不可拖拽、项呈禁用视觉 |
| closeable | boolean | false | 可关闭模式:每项右上角显示关闭钮,点击后该项从列表移除并回发新数组 |
| custom-class | string | "" | 组件根节点类名 |
| custom-style | string | "" | 组件根节点行内样式 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
| update:model-value | (list: NDraggableItem[]) | 排序完成或移除项时回发重排后的新数组,供 v-model 使用 |
| change | (list: NDraggableItem[]) | 排序完成时触发,参数为重排后的新数组 |
| start | (index: number) | 进入拖拽时触发,参数为拖拽项下标 |
| end | (index: number) | 拖拽结束时触发,参数为落位下标(未换位时为原下标) |
| close | (index: number) | 点击项关闭钮时触发,参数为被移除项的原下标 |
插槽
| 名称 | 参数 | 说明 |
|---|---|---|
| item | 项内容;dragging 为 true 且 index === dragIndex 时该项正在被拖拽 | |
| handle | — | 拖拽手柄;提供后仅手柄区域可触发拖拽 |
| close | — | 关闭钮内容,覆盖默认的红底白叉圆钮 |