Vue2项目引入sortablejs实现表格行拖曳排序

需求背景与核心目标

在基于 Vue2 和 Element UI 构建的可编辑表格组件中,需实现以下核心目标:

  1. 仅允许点击表格第一列(序号列)的拖拽手柄触发行拖曳,禁止点击其他列触发拖拽;
  2. 拖曳过程中视觉反馈清晰,无禁止符号、无卡顿;
  3. 拖曳结束后同步更新表格数据,并能将排序结果提交至后端;
  4. 兼容 Element UI 表格的原有功能(如分页、编辑、表单验证等),无样式和事件冲突。

效果

一、安装 sortablejs

bash 复制代码
npm i sortablejs @types/sortablejs --save

二、在表格组件文件中引入

javascript 复制代码
<script>
......其他引入
import Sortable from 'sortablejs';
</script>

三、具体使用方法

1、模板中

在 Element UI 表格的序号列中添加拖拽手柄 DOM,为拖拽触发提供专属区域:

html 复制代码
<!-- 序号 + 拖拽手柄 -->
<el-table-column
  v-if="showIndex || enableDragSort"
  column-key="index"
  label="序号"
  width="55"
  align="left"
>
  <template slot-scope="scope">
    <div
      class="drag-handle-wrapper"
      :data-key="scope.row[rowKeyField] || scope.$index"
    >
      <span v-if="enableDragSort" class="drag-handle-icon">
        <i class="el-icon-d-caret icon-class"/>
      </span>
      <span class="index-number">{{ scope.$index + 1 }}</span>
    </div>
  </template>
</el-table-column>

2、逻辑代码

定义一些需要的变量

javascript 复制代码
  props:{
  // 行唯一标识字段(默认id)
    rowKeyField: {
      type: String,
      default: 'id'
    }
  },
  data() {
    return {
      sortableObj: null,
      // 拖拽初始化标记
      dragInitialized: false,
    };
  },

在组件挂载阶段初始化 Sortable 实例,重点解决 Element UI 表格的事件拦截、拖拽区域限制问题,销毁前清理掉:

javascript 复制代码
  mounted() {
    if (this.enableDragSort) {
      this.$nextTick(() => {
        // 延迟初始化,确保DOM完全渲染
        setTimeout(() => {
          this.initSortable();
        }, 500);
      });
    }
  },
  beforeDestroy() {
    // 销毁Sortable实例,避免内存泄漏
    this.destroySortable()
  },
  methods: {
    // 初始化拖拽
    initSortable() {
      if (this.dragInitialized) {
        this.destroySortable();
      }
      if (!this.enableDragSort || !this.$refs.sortTable || !this.tableData || this.tableData.length <= 1) {
        return;
      }

      try {
        const tbody = this.$refs.sortTable.$el.querySelector(".el-table__body-wrapper tbody");

        if (!tbody) {
          console.error("无法找到tbody元素");
          return;
        }

        this.sortableObj = Sortable.create(tbody, {
          animation: 150,
          draggable: ".el-table__row",
          handle: ".drag-handle-wrapper, .drag-handle-icon",
          ghostClass: "sortable-ghost",
          chosenClass: "sortable-chosen",
          dragClass: "sortable-drag",
          dataIdAttr: "data-key",

          // 简化事件处理
          onStart: (evt) => {
            // 阻止默认行为但允许传播
            evt.originalEvent.preventDefault();
            evt.item.style.opacity = "0.5";
          },

          onEnd: (evt) => {
            evt.item.style.opacity = "";

            const { oldIndex, newIndex } = evt;
            if (oldIndex === newIndex) return;

            // 更新数据
            const currentData = [...this.tableData];
            const movedItem = currentData.splice(oldIndex, 1)[0];
            currentData.splice(newIndex, 0, movedItem);

            // 触发更新
            this.$emit("sort-change", {
              oldIndex,
              newIndex,
              sortedData: currentData,
              sortedIds: currentData.map((item) => item[this.rowKeyField]),
            });

            // 重新初始化
            this.$nextTick(() => {
              this.destroySortable();
              setTimeout(() => {
                this.initSortable();
              }, 100);
            });
          },

          // 禁用所有过滤,确保拖拽能进行
          filter: "",
          preventOnFilter: false,
        });

        this.dragInitialized = true;
      } catch (error) {
        console.error("Sortable初始化失败:", error);
      }
    },
    // 销毁Sortable实例
    destroySortable() {
      if (this.sortableObj) {
        try {
          this.sortableObj.destroy();
        } catch (error) {
          console.error('销毁Sortable实例失败:', error);
        }
        this.sortableObj = null;
      }
      this.dragInitialized = false;
    },
}

3、样式

通过 CSS 限制拖拽交互区域,确保仅手柄可触发拖拽,同时优化视觉体验:

css 复制代码
<style lang="scss" scoped>
// 修复可能影响拖拽的Element UI样式
::v-deep .el-table__body-wrapper {
  user-select: none;
}

::v-deep .el-table__row {
  -webkit-user-drag: element;
}
</style>
<style lang="scss">
// 全局拖拽样式(必须放在scoped外面)
.drag-handle-wrapper {
  display: flex;
  align-items: center;
  cursor: grab !important;
  padding: 8px 0;

  &:active {
    cursor: grabbing !important;
  }

  .drag-handle-icon {
    margin-right: 5px;
    opacity: 0.6;
    transition: opacity 0.2s;
    cursor: grab !important;

    &:hover {
      opacity: 1;
    }

    &:active {
      cursor: grabbing !important;
    }
  }
}
.icon-class{
  width: 14px;
  vertical-align: middle;
  &:hover {
    color: $primary;
  }
}
// 修复Element UI表格的拖拽问题
.el-table {
  // 允许拖拽
  .el-table__body-wrapper {
    cursor: default !important;

    tbody {
      cursor: default !important;
    }
  }

  // 表格行样式
  .el-table__row {
    cursor: default !important;

    &.sortable-ghost {
      opacity: 0.5;
      background: #f5f7fa !important;
    }

    &.sortable-chosen {
      background: #e8f4ff !important;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    &.sortable-drag {
      opacity: 0.8;
      background: #e8f4ff !important;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    }
  }
}

// 禁止符号修复
* {
  &[draggable="true"] {
    -webkit-user-drag: element !important;
    user-drag: element !important;
  }
}
</style>

四、父组件中使用

使用封装好的表格组件

html 复制代码
    <EditTable
      ......其他配置项
      :enableDragSort="hasEditPermission"
      rowKeyField="id"
      @sort-change="handleSortChange"
    />

接收返回的排序结果

javascript 复制代码
    // 处理排序变化(可选)
    handleSortChange({ oldIndex, newIndex, sortedData, sortedIds }) {
      sortedData.forEach((item, index) => {

        item.sort_order = index + 1;
      });
      this.tableData = sortedData
    }

五、遇到的问题与解决方案

1. 拖拽事件被 Element UI 拦截(onStart/onEnd 不触发)

问题原因:Element UI 表格内部会阻止鼠标事件传播,导致 Sortable 无法捕获完整的拖拽事件;

解决方案:启用forceFallback: true强制使用原生拖拽,取消stopImmediatePropagation()事件拦截,调用doLayout()强制刷新表格布局。

2. 整行均可拖拽,无法限制仅手柄触发

问题原因:Sortable 默认允许点击可拖拽元素任意区域触发拖拽,Element UI 表格行的事件穿透导致范围失控;

解决方案

通过handle配置精确指定拖拽手柄选择器;

启用filter过滤非手柄区域;

样式层面设置整行pointer-events: none,仅序号列开放事件。

3. 拖拽时出现禁止符号

问题原因:拖拽目标元素的pointer-events为none、选择器匹配失败或 fallback 配置不当;

解决方案

确保拖拽手柄的pointer-events: auto;

降低fallbackTolerance至 1,减少触发门槛;

为拖拽中的行设置高z-index,避免被遮挡。

4. 内存泄漏风险

问题原因:组件销毁时未销毁 Sortable 实例,导致 DOM 事件残留;

解决方案:在beforeDestroy钩子中调用destroySortable()销毁实例。

  • 启用forceFallback绕过 Element UI 事件拦截,是解决拖拽事件不触发的核心;

  • 结合handle和filter配置 + 样式层面的pointer-events限制,可精准控制拖拽触发区域;

  • 组件生命周期中及时销毁 Sortable 实例,避免内存泄漏;

  • 拖拽后的数据源同步需通过$emit通知父组件,保证数据单向流的规范性。

相关推荐
叫我一声阿雷吧3 小时前
JS实现响应式导航栏(移动端汉堡菜单)|适配多端+无缝交互【附完整源码】
开发语言·javascript·交互
是做服装的同学3 小时前
服装软件ERP管理系统实现智能化流程管控与业务协同
大数据·经验分享·其他
GISer_Jing3 小时前
前端营销(AIGC II)
前端·react.js·aigc
NEXT063 小时前
深度解析 JWT:从 RFC 原理到 NestJS 实战与架构权衡
前端·typescript·nestjs
浅念-4 小时前
C++ string类
开发语言·c++·经验分享·笔记·学习
程序员林北北4 小时前
【前端进阶之旅】节流与防抖:前端性能优化的“安全带”与“稳定器”
前端·javascript·vue.js·react.js·typescript
寻星探路5 小时前
【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战
java·前端·javascript·css·c++·ai·html
未来之窗软件服务6 小时前
未来之窗昭和仙君(六十九)前端收银台行为异常检测—东方仙盟练气
前端·仙盟创梦ide·东方仙盟·昭和仙君
大叔编程奋斗记6 小时前
两个日期间的相隔年月计算
前端·salesforce