更新记录

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=350animationDuration=300dragScale=1.03dragOpacity=0.95
  • 滚动组件默认:scrollThreshold=80scrollSpeed 等与文档一致。
  • 同步修复到 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-appuni-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 固定 itemHeightautoHeight 本版不支持
鸿蒙 - 未适配
微信小程序 u 触摸可运行,未作为官方支持
其他小程序 - 未适配
uni-app x Web / App 使用 lf-dnd-xautoHeight 本版不支持
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"
  }
}

注意:

  1. path 不要写 .vue.uvue 后缀。
  2. path 不要以 / 开头。
  3. navigateTourl 建议以 / 开头。
  4. 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 或小程序不能拖

优先检查:

  1. 是否组件没有识别成功。
  2. itemHeight 是否传入了正确高度(Number 为 rpx;需要 px 请写 '88px')。
  3. 多列布局中 column / itemHeight / gap 是否和样式一致。
  4. 是否有外层元素拦截了 touchmove
  5. 图片元素是否触发了默认拖拽或长按行为。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率: