更新记录
1.3.5(2026-09-01)
- 修复uniappx 部分版本无法正常使用
- uni-app x 滚动拖拽缓存条目字段与预览位置,移除每帧重复 JSON 序列化和全量排序计算。
- 滚动拖拽 Demo 使用组件稳定的默认插槽内容,修复 scoped slot 响应式代理导致数字为空。
- Demo 图片迁移到插件
static目录并使用模块绝对路径,保证 Android 打包后可访问。
1.3.0(2026-08-03)
修复
touchcancel改为取消拖拽,不再误提交排序(lf-dnd-item/lf-dnd-x-item)。- 固定项在拖拽预览与落点提交时保持槽位不动,仅非固定项互相让位。
- 明确并统一尺寸单位:Number 按 rpx;支持
'88px'/'88rpx'。 - H5 滚动拖拽:默认
touch-action: pan-y,恢复scroll-view原生滚动;scroll不再回写受控scroll-top。 - H5 长按屏蔽系统菜单(
contextmenu/-webkit-touch-callout)。 - 拖拽中拦截页面滚动(非 passive
touchmove+ 锁定body)。 - 滚动拖拽性能:位移/目标位节流合并更新;自动滚动时关闭让位动画,降低真机重排。
新增
- app-nvue:同目录提供
lf-dnd/lf-dnd-item/lf-dnd-handle/lf-dnd-scroll的.nvue实现,API 与 Vue 版一致。 - nvue demo:
pages/demo-nvue/demo-nvue。
优化
- 默认值与文档对齐:
longpressDelay=350、animationDuration=300、dragScale=1.03、dragOpacity=0.95。 - 滚动组件默认:
scrollThreshold=80、scrollSpeed等与文档一致。 - 同步修复到
lf-dnd/lf-dnd-scroll/lf-dnd-x/lf-dnd-scroll-x/ nvue。 - 滚动 demo 精简为单个多列样例。
- nvue 位移使用
translate(避免translate3d兼容问题)。
文档
- 补充尺寸单位约定、
drag-cancel事件与 nvue 适配说明。
V1.0.1(2026-04-30)
- 新增滚动拖拽组件
lf-dnd-scroll - 新增 uni-app x 滚动拖拽组件
lf-dnd-scroll-x - 支持长列表拖拽到容器顶部 / 底部时自动滚动
- 支持滚动容器内拖拽排序、占位、让位和释放排序
平台兼容性
uni-app(5.07)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ | √ | √ | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.07)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| √ | √ | √ | √ | - | - |
LfDnd 拖拽排序
LfDnd 是一个基于 uni-app / uni-app x 的拖拽排序组件,支持列表拖拽、宫格拖拽、多列布局、图片排序、禁用项、固定项、长按拖拽、手柄拖拽,以及滚动长列表拖拽排序。
插件同时提供传统 uni-app 与 uni-app x 两套组件。
平台兼容
图例:√ 已验证可按文档使用;u 代码路径存在、本版未做完整真机回归;- 本版不支持。目标是全端可用,下表是本版本真实状态。
| 运行时 / 端 | 状态 | 说明 |
|---|---|---|
| Vue2 / Vue3 | √ | v-model:list(Vue3)/ :list.sync(Vue2) |
| H5 Chrome / Safari | √ | |
| Android App-vue | √ | |
| iOS App-vue | u | 与 Android 同套 Vue 实现,本仓库未单独真机回归 |
| app-nvue | √ | 固定 itemHeight;autoHeight 本版不支持 |
| 鸿蒙 | - | 未适配 |
| 微信小程序 | u | 触摸可运行,未作为官方支持 |
| 其他小程序 | - | 未适配 |
| uni-app x Web / App | √ | 使用 lf-dnd-x;autoHeight 本版不支持 |
| uni-app x 鸿蒙 / 微信 | - | 未适配 |
autoHeight |
u | 仅 Vue H5 / App-vue;默认关闭,不影响原固定高度 |
nvue 页面会自动优先加载同目录
.nvue;请勿在 easycom 里写死.vue路径,否则 nvue 无法命中。
组件选择
| 场景 | uni-app Vue | app-nvue | uni-app x |
|---|---|---|---|
| 普通列表 / 宫格拖拽 | lf-dnd |
lf-dnd(.nvue) |
lf-dnd-x |
| 拖拽项 | lf-dnd-item |
lf-dnd-item(.nvue) |
lf-dnd-x-item |
| 手柄拖拽 | lf-dnd-handle |
lf-dnd-handle(.nvue) |
lf-dnd-x-handle |
| 滚动长列表拖拽 | lf-dnd-scroll |
lf-dnd-scroll(.nvue) |
lf-dnd-scroll-x |
普通列表、宫格、图片排序场景优先使用 lf-dnd。当列表内容很多,并且拖拽过程中需要在容器顶部 / 底部自动滚动时,使用 lf-dnd-scroll。
传统 uni-app 基础用法
<template>
<view class="page">
<lf-dnd
v-model:list="list"
item-height="88px"
gap="10px"
@change="handleChange"
>
<lf-dnd-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
:data="item"
>
<view class="row">
{{ item.text }}
</view>
</lf-dnd-item>
</lf-dnd>
</view>
</template>
<script setup>
import { ref } from 'vue';
const list = ref([
{ id: 1, text: '项目 1' },
{ id: 2, text: '项目 2' },
{ id: 3, text: '项目 3' },
{ id: 4, text: '项目 4' }
]);
const handleChange = e => {
console.log('排序完成:', e);
};
</script>
<style scoped>
.page {
min-height: 100vh;
padding: 24rpx;
background: #f6f7f9;
box-sizing: border-box;
}
.row {
height: 88px;
line-height: 88px;
padding: 0 24rpx;
background: #ffffff;
border-radius: 12rpx;
box-sizing: border-box;
}
</style>
Vue2 请用 :list.sync(组件同样 emit('update:list'),与 Vue3 的 v-model:list 对应,不要混用):
<lf-dnd :list.sync="list" item-height="88px" gap="10px" @change="handleChange">
<lf-dnd-item v-for="(item, index) in list" :key="item.id" :index="index" :data="item">
<view class="row">{{ item.text }}</view>
</lf-dnd-item>
</lf-dnd>
uni-app x 基础用法
uni-app x 初版建议使用 :list + @update:list,不要优先依赖 v-model:list。
<template>
<view class="page">
<lf-dnd-x
:list="list"
item-height="88px"
gap="10px"
@update:list="handleUpdate"
@change="handleChange"
>
<lf-dnd-x-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
:data="item"
>
<view class="row">
{{ item.text }}
</view>
</lf-dnd-x-item>
</lf-dnd-x>
</view>
</template>
<script lang="uts">
type DndItem = {
id: number,
text: string,
disabled: boolean,
fixed: boolean
}
export default {
data() {
return {
list: [
{ id: 1, text: '项目 1', disabled: false, fixed: false },
{ id: 2, text: '项目 2', disabled: false, fixed: false },
{ id: 3, text: '项目 3', disabled: false, fixed: false },
{ id: 4, text: '项目 4', disabled: false, fixed: false }
] as DndItem[]
}
},
methods: {
handleUpdate(list: DndItem[]) {
this.list = list
},
handleChange(e: any) {
console.log('排序完成', e)
}
}
}
</script>
<style>
.page {
min-height: 100%;
padding: 24rpx;
background-color: #f6f7f9;
}
.row {
height: 88px;
line-height: 88px;
padding-left: 24rpx;
padding-right: 24rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
</style>
多列宫格拖拽
传统 uni-app
<lf-dnd
v-model:list="imageList"
:column="3"
item-height="150rpx"
gap="16rpx"
longpress
>
<lf-dnd-item
v-for="(item, index) in imageList"
:key="item.id"
:index="index"
:data="item"
v-slot="slotProps"
>
<view
class="image-card"
:class="{ active: slotProps && slotProps.active }"
@contextmenu.prevent
@dragstart.prevent
>
<image
class="image"
:src="item.url"
mode="aspectFill"
draggable="false"
@contextmenu.prevent
@dragstart.prevent
/>
<view class="mask">
{{ item.text }}
</view>
</view>
</lf-dnd-item>
</lf-dnd>
uni-app x
<lf-dnd-x
:list="imageList"
:column="3"
item-height="150rpx"
gap="16rpx"
:longpress="true"
@update:list="handleImageUpdate"
>
<lf-dnd-x-item
v-for="(item, index) in imageList"
:key="item.id"
:index="index"
:data="item"
>
<view class="image-card">
<image class="image" :src="item.url" mode="aspectFill" />
<view class="mask">
{{ item.text }}
</view>
</view>
</lf-dnd-x-item>
</lf-dnd-x>
图片样式建议
Safari H5 长按图片可能会触发系统菜单,导致拖拽中断。图片拖拽场景建议加以下样式:
.image-card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 16rpx;
background: #f2f2f2;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
.image {
width: 100%;
height: 100%;
display: block;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
-webkit-user-select: none;
user-select: none;
}
.mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 56rpx;
line-height: 56rpx;
text-align: center;
color: #ffffff;
font-size: 24rpx;
background: rgba(0, 0, 0, 0.35);
pointer-events: none;
}
禁用项
disabled 表示当前项不可被拖动。
<lf-dnd-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
:disabled="item.disabled"
>
<view class="row" :class="{ disabled: item.disabled }">
{{ item.text }}
</view>
</lf-dnd-item>
const list = ref([
{ id: 1, text: '禁用项 1', disabled: true },
{ id: 2, text: '可拖拽项 1', disabled: false },
{ id: 3, text: '可拖拽项 2', disabled: false }
]);
固定项
fixed 表示当前项位置固定,不能拖动,也不能被其他项替换位置。
<lf-dnd-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
:fixed="item.fixed"
>
<view class="row" :class="{ fixed: item.fixed }">
{{ item.text }}
</view>
</lf-dnd-item>
const list = ref([
{ id: 1, text: '固定项 1', fixed: true },
{ id: 2, text: '普通项 1', fixed: false },
{ id: 3, text: '普通项 2', fixed: false }
]);
长按拖拽
开启 longpress 后,需要长按一段时间才会触发拖拽。
<lf-dnd
v-model:list="list"
longpress
:longpress-delay="350"
>
...
</lf-dnd>
uni-app x:
<lf-dnd-x
:list="list"
:longpress="true"
:longpress-delay="350"
@update:list="list = $event"
>
...
</lf-dnd-x>
手柄拖拽
开启 use-handle 后,只有拖动手柄区域才会触发排序。
传统 uni-app
<lf-dnd v-model:list="list" use-handle>
<lf-dnd-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
>
<view class="row row-between">
<text>{{ item.text }}</text>
<lf-dnd-handle>
<text class="handle">☰</text>
</lf-dnd-handle>
</view>
</lf-dnd-item>
</lf-dnd>
uni-app x
<lf-dnd-x
:list="list"
:use-handle="true"
@update:list="list = $event"
>
<lf-dnd-x-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
>
<view class="row row-between">
<text>{{ item.text }}</text>
<lf-dnd-x-handle>
<text class="handle">☰</text>
</lf-dnd-x-handle>
</view>
</lf-dnd-x-item>
</lf-dnd-x>
.row-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.handle {
font-size: 36rpx;
color: #999999;
}
滚动长列表拖拽
当列表内容超过一屏,并且需要拖拽到顶部 / 底部时自动滚动,建议使用滚动拖拽组件:
- uni-app:
lf-dnd-scroll - uni-app x:
lf-dnd-scroll-x
滚动拖拽组件内部使用 scroll-view 处理滚动。
uni-app 示例
<template>
<view class="page">
<lf-dnd-scroll
v-model:list="scrollList"
:height="620"
item-height="96px"
gap="12px"
:longpress="true"
@change="handleScrollChange"
>
<template v-slot="slotProps">
<view class="scroll-row">
{{ slotProps.item.text }}
</view>
</template>
</lf-dnd-scroll>
</view>
</template>
<script setup>
import { ref } from 'vue';
const scrollList = ref(
Array.from({ length: 30 }).map((_, index) => ({
id: index + 1,
text: `滚动项 ${index + 1}`
}))
);
const handleScrollChange = e => {
console.log('滚动排序完成:', e);
};
</script>
<style scoped>
.page {
padding: 24rpx;
background: #f6f7f9;
}
.scroll-row {
height: 96px;
line-height: 96px;
padding: 0 24rpx;
background: #ffffff;
border-radius: 12rpx;
box-sizing: border-box;
}
</style>
uni-app x 示例
<template>
<view class="page">
<lf-dnd-scroll-x
:list="scrollList"
:height="620"
item-height="96px"
gap="12px"
:longpress="true"
@update:list="handleUpdate"
@change="handleScrollChange"
>
<template v-slot="slotProps">
<view class="scroll-row">
{{ slotProps.item.text }}
</view>
</template>
</lf-dnd-scroll-x>
</view>
</template>
<script lang="uts">
type ScrollItem = {
id: number,
text: string
}
export default {
data() {
return {
scrollList: [
{ id: 1, text: '滚动项 1' },
{ id: 2, text: '滚动项 2' },
{ id: 3, text: '滚动项 3' },
{ id: 4, text: '滚动项 4' },
{ id: 5, text: '滚动项 5' },
{ id: 6, text: '滚动项 6' },
{ id: 7, text: '滚动项 7' },
{ id: 8, text: '滚动项 8' },
{ id: 9, text: '滚动项 9' },
{ id: 10, text: '滚动项 10' }
] as ScrollItem[]
}
},
methods: {
handleUpdate(list: ScrollItem[]) {
this.scrollList = list
},
handleScrollChange(e: any) {
console.log('滚动排序完成', e)
}
}
}
</script>
<style>
.page {
padding: 24rpx;
background-color: #f6f7f9;
}
.scroll-row {
height: 96px;
line-height: 96px;
padding-left: 24rpx;
padding-right: 24rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
</style>
Demo 页面
传统 uni-app demo
在 pages.json 顶层 pages 中添加:
{
"path": "uni_modules/lf-dnd/pages/demo/demo",
"style": {
"navigationBarTitleText": "LfDnd 拖拽排序"
}
}
跳转:
uni.navigateTo({
url: '/uni_modules/lf-dnd/pages/demo/demo'
});
uni-app x demo
在 pages.json 顶层 pages 中添加:
{
"path": "uni_modules/lf-dnd/pages/demo-x/demo-x",
"style": {
"navigationBarTitleText": "LfDnd X 拖拽排序"
}
}
跳转:
uni.navigateTo({
url: '/uni_modules/lf-dnd/pages/demo-x/demo-x'
});
滚动拖拽 demo
uni-app:
{
"path": "uni_modules/lf-dnd/pages/demo-scroll/demo-scroll",
"style": {
"navigationBarTitleText": "LfDnd Scroll"
}
}
uni-app x:
{
"path": "uni_modules/lf-dnd/pages/demo-scroll-x/demo-scroll-x",
"style": {
"navigationBarTitleText": "LfDnd Scroll X"
}
}
注意:
path不要写.vue或.uvue后缀。path不要以/开头。navigateTo的url建议以/开头。- demo 页面配置要放在顶层
pages中,不要放进分包subPackages,否则路径会被拼接成分包路径。
API
尺寸单位约定
itemHeight / gap 支持:
- Number / 纯数字字符串:按 rpx 换算(
uni.upx2px) '88px':按 px'88rpx':按 rpx
滚动容器的 height 仍按 px(Number 或 '600px')。
lf-dnd / lf-dnd-x Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
list |
Array | [] |
拖拽数据列表 |
column |
Number | 1 |
列数,1 为普通列表,大于 1 为宫格 |
itemHeight |
Number / String | 50 |
每项高度,Number 为 rpx,也可用 '88px' / '88rpx' |
autoHeight |
Boolean | false |
true 时按内容测量高度。默认关闭。仅 Vue H5 / App-vue;nvue / uni-app x 本版忽略 |
gap |
Number / String | 0 |
项目间距,单位规则同 itemHeight |
disabled |
Boolean | false |
是否禁用整个拖拽容器 |
longpress |
Boolean | false |
是否开启长按拖拽 |
longpressDelay |
Number | 350 |
长按触发时间,单位 ms |
useHandle |
Boolean | false |
是否只允许通过手柄拖拽 |
fixedMode |
String | strict |
固定项落点策略:strict / skip |
dragScale |
Number | 1.03 |
拖拽中缩放比例 |
dragOpacity |
Number | 0.95 |
拖拽中透明度 |
animationDuration |
Number | 300 |
让位动画时间 |
lf-dnd-item / lf-dnd-x-item Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
index |
Number | 必填 | 当前项索引 |
disabled |
Boolean | false |
当前项是否不可拖动 |
fixed |
Boolean | false |
当前项是否固定位置 |
data |
Object | {} |
当前项数据 |
lf-dnd-handle / lf-dnd-x-handle Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
disabled |
Boolean | false |
是否禁用当前手柄 |
lf-dnd-scroll / lf-dnd-scroll-x Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
list |
Array | [] |
拖拽数据列表 |
height |
Number / String | 600 |
滚动容器高度,单位 px |
itemHeight |
Number / String | 88 |
每项高度,Number 为 rpx,也可用 '96px' / '96rpx' |
autoHeight |
Boolean | false |
同 lf-dnd。仅 Vue H5 / App-vue;nvue / x 本版不支持 |
gap |
Number / String | 10 |
项目间距,单位规则同 itemHeight |
longpress |
Boolean | true |
是否长按拖拽 |
longpressDelay |
Number | 350 |
长按触发时间,单位 ms |
scrollThreshold |
Number | 80 |
距离容器顶部 / 底部多少 px 触发滚动 |
scrollSpeed |
Number | 8 |
每次自动滚动距离 |
disabled |
Boolean | false |
是否禁用整个拖拽容器 |
Events
lf-dnd / lf-dnd-x Events
| 事件 | 说明 | 返回值 |
|---|---|---|
update:list |
返回排序后的新数组 | newList |
change |
排序完成后触发 | { list, oldIndex, newIndex, item } |
drag-start |
开始拖拽 | { item, index } |
drag-move |
拖拽移动中 | { item, oldIndex, newIndex, deltaX, deltaY } |
drop |
释放拖拽 | { item, oldIndex, newIndex, list } |
drag-cancel |
拖拽取消(含 touchcancel) |
{ item, index } |
lf-dnd-scroll / lf-dnd-scroll-x Events
| 事件 | 说明 | 返回值 |
|---|---|---|
update:list |
返回排序后的新数组 | newList |
change |
排序完成后触发 | { list, oldIndex, newIndex, item } |
drag-start |
开始拖拽 | { item, index } |
drag-move |
拖拽移动中 | { item, oldIndex, newIndex, deltaY, scrollTop } |
drop |
释放拖拽 | { item, oldIndex, newIndex, list } |
scroll-change |
滚动位置变化 | { scrollTop } |
Slot
lf-dnd-item 默认插槽
传统 uni-app 中,lf-dnd-item 默认插槽会返回拖拽状态:
<lf-dnd-item
v-for="(item, index) in list"
:key="item.id"
:index="index"
v-slot="slotProps"
>
<view :class="{ active: slotProps && slotProps.active }">
{{ item.text }}
</view>
</lf-dnd-item>
建议使用:
v-slot="slotProps"
不要直接写:
v-slot="{ active }"
部分 uni-app 编译环境首次渲染时插槽参数可能为空,直接解构会报错。
lf-dnd-scroll 默认插槽
lf-dnd-scroll 默认插槽返回当前渲染项信息:
<lf-dnd-scroll v-model:list="list">
<template v-slot="slotProps">
<view>
{{ slotProps.item.text }}
</view>
</template>
</lf-dnd-scroll>
可用字段:
| 字段 | 说明 |
|---|---|
item |
当前项数据 |
index |
当前项索引 |
active |
是否为当前拖拽项 |
dragging |
是否正在拖拽 |
常见问题
1. 组件不识别
如果控制台出现:
Failed to resolve component: lf-dnd
Failed to resolve component: lf-dnd-item
Failed to resolve component: lf-dnd-handle
请检查插件目录是否正确:
uni_modules/lf-dnd/components/lf-dnd/lf-dnd.vue
uni_modules/lf-dnd/components/lf-dnd-item/lf-dnd-item.vue
uni_modules/lf-dnd/components/lf-dnd-handle/lf-dnd-handle.vue
如果 easycom 没有自动识别,可以在页面中手动引入:
<script setup>
import LfDnd from '@/uni_modules/lf-dnd/components/lf-dnd/lf-dnd.vue';
import LfDndItem from '@/uni_modules/lf-dnd/components/lf-dnd-item/lf-dnd-item.vue';
import LfDndHandle from '@/uni_modules/lf-dnd/components/lf-dnd-handle/lf-dnd-handle.vue';
</script>
uni-app x:
<script lang="uts">
import LfDndX from '@/uni_modules/lf-dnd/components/lf-dnd-x/lf-dnd-x.uvue'
import LfDndXItem from '@/uni_modules/lf-dnd/components/lf-dnd-x-item/lf-dnd-x-item.uvue'
import LfDndXHandle from '@/uni_modules/lf-dnd/components/lf-dnd-x-handle/lf-dnd-x-handle.uvue'
export default {
components: {
LfDndX,
LfDndXItem,
LfDndXHandle
}
}
</script>
2. demo 页面找不到
如果报错:
page `/pages/tabBar/uni_modules/lf-dnd/pages/demo-x/demo-x` is not found
说明 navigateTo 使用了相对路径。
错误:
uni.navigateTo({
url: 'uni_modules/lf-dnd/pages/demo-x/demo-x'
});
正确:
uni.navigateTo({
url: '/uni_modules/lf-dnd/pages/demo-x/demo-x'
});
3. Safari H5 图片长按弹出菜单
给图片和图片外层加:
-webkit-touch-callout: none;
-webkit-user-drag: none;
-webkit-user-select: none;
user-select: none;
pointer-events: none;
4. 页面很长时,应该用 lf-dnd 还是 lf-dnd-scroll?
如果只是普通列表或宫格排序,用 lf-dnd。
如果列表内容很多,拖拽过程中需要容器自动滚动,用 lf-dnd-scroll。
不建议在普通页面滚动上直接做自动滚动拖拽,建议使用 lf-dnd-scroll 内部 scroll-view 方案。
5. H5 可以拖,App 或小程序不能拖
优先检查:
- 是否组件没有识别成功。
itemHeight是否传入了正确高度(Number 为 rpx;需要 px 请写'88px')。- 多列布局中
column / itemHeight / gap是否和样式一致。 - 是否有外层元素拦截了
touchmove。 - 图片元素是否触发了默认拖拽或长按行为。

收藏人数:
购买源码授权版(
试用
赞赏(1)
下载 142
赞赏 1
下载 12591771
赞赏 1949
赞赏
京公网安备:11010802035340号