更新记录

V1.0.0(2026-04-28) 下载此版本

  • 新增 uni-app x 专用组件:lf-dnd-x
  • 新增 uni-app x 拖拽项组件:lf-dnd-x-item
  • 新增 uni-app x 手柄组件:lf-dnd-x-handle
  • 新增 uni-app x 示例页面 demo-x.uvue
  • 保留传统 uni-app 稳定版组件:lf-dnd、lf-dnd-item、lf-dnd-handle
  • 支持基础列表拖拽排序
  • 支持多列宫格拖拽排序
  • 支持图片拖拽排序
  • 支持 disabled 禁用项
  • 支持 fixed 固定项
  • 支持 longpress 长按拖拽
  • 支持 useHandle 手柄拖拽
  • 支持拖拽悬浮、占位和让位动画
  • 支持 update:list、change、drag-start、drag-move、drop 事件
  • 已验证传统 uni-app 下 H5 Chrome、H5 Safari、Android App-vue、微信小程序可正常使用
  • 修复 Safari H5 图片长按触发系统菜单导致拖拽中断的问题
  • 优化 H5 鼠标拖拽体验
  • 优化移动端拖拽性能

平台兼容性

uni-app(5.07)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × ×

LfDnd 拖拽排序

LfDnd 是一个基于 uni-app / uni-app x 的拖拽排序组件,支持列表拖拽、宫格拖拽、多列布局、图片排序、禁用项、固定项、长按拖拽、手柄拖拽等场景。

插件同时提供两套组件:

项目类型 使用组件
传统 uni-app lf-dnd / lf-dnd-item / lf-dnd-handle
uni-app x lf-dnd-x / lf-dnd-x-item / lf-dnd-x-handle

平台兼容

已验证平台

平台 状态
H5 Chrome 正常
H5 Safari 正常
Android App-vue 正常
微信小程序 正常

适配说明

平台 说明
传统 uni-app 已验证 H5 Chrome、H5 Safari、Android App-vue、微信小程序
uni-app x 已提供专用 uvue 组件,已验证 H5 Chrome、H5 Safari、Android App-vue、微信小程序
app-nvue 暂不支持
其他小程序 暂未适配,需自行测试

安装方式

将插件目录放入项目的 uni_modules 下:

你的项目/
├─ pages/
├─ static/
├─ pages.json
└─ uni_modules/
   └─ lf-dnd/
      ├─ components/
      ├─ pages/
      ├─ package.json
      ├─ readme.md
      └─ changelog.md

插件目录必须是:

uni_modules/lf-dnd

不要出现下面这种错误结构:

uni_modules/lf-dnd-plugin/uni_modules/lf-dnd
uni_modules/uni_modules/lf-dnd

组件选择

传统 uni-app

使用:

<lf-dnd>
  <lf-dnd-item>
    ...
  </lf-dnd-item>
</lf-dnd>

uni-app x

使用:

<lf-dnd-x>
  <lf-dnd-x-item>
    ...
  </lf-dnd-x-item>
</lf-dnd-x>

注意:uni-app x 初版建议使用 :list + @update:list,不要优先依赖 v-model:list


传统 uni-app 基础用法

<template>
  <view class="page">
    <lf-dnd
      v-model:list="list"
      :item-height="88"
      :gap="10"
      @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>

uni-app x 基础用法

<template>
  <view class="page">
    <lf-dnd-x
      :list="list"
      :item-height="88"
      :gap="10"
      @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="150"
  :gap="16"
  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="150"
  :gap="16"
  :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;
}

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'
});

注意:

  1. path 不要写 .vue.uvue 后缀。
  2. path 不要以 / 开头。
  3. navigateTourl 建议以 / 开头。
  4. demo 页面配置要放在顶层 pages 中,不要放进分包 subPackages,否则路径会被拼接成分包路径。

错误示例:

{
  "root": "pages/CSS",
  "pages": [
    {
      "path": "uni_modules/lf-dnd/pages/demo-x/demo-x"
    }
  ]
}

这个会被解析成:

pages/CSS/uni_modules/lf-dnd/pages/demo-x/demo-x

导致页面不存在。


API

lf-dnd / lf-dnd-x Props

属性 类型 默认值 说明
list Array [] 拖拽数据列表
column Number 1 列数,1 为普通列表,大于 1 为宫格
itemHeight Number 50 每项高度,单位 px
gap Number 0 项目间距
disabled Boolean false 是否禁用整个拖拽容器
longpress Boolean false 是否开启长按拖拽
longpressDelay Number 350 长按触发时间,单位 ms
useHandle Boolean false 是否只允许通过手柄拖拽
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 是否禁用当前手柄

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 }

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 编译环境首次渲染时插槽参数可能为空,直接解构会报错。


常见问题

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. H5 可以拖,App 或小程序不能拖

优先检查:

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

隐私、权限声明

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

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

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

许可协议

MIT协议

暂无用户评论。