使用 SortableJS 实现vue3 + Element Plus 表格拖拽排序

本文将详细介绍如何使用 SortableJS 在 Vue3 + Element Plus 项目中实现 el-table 表格行的拖拽排序功能。

快速前往:
SortableJS GitHub
SortableJS中文网

实现效果

技术栈

  • Vue 3
  • Element Plus
  • SortableJS

核心实现代码

1. 安装依赖

bash 复制代码
npm install sortablejs

2. 表格组件实现

js 复制代码
<template>
  <div class="table-draggable">
    <el-table :data="dataList" row-key="id" v-loading="loading">
      <el-table-column label="排序" width="80" align="center">
        <img class="icon-touch" src="@/assets/icon/touchMove.svg" />
      </el-table-column>
      <el-table-column 
        v-for="item in head" 
        :key="item.prop" 
        :label="item.label" 
        :prop="item.prop" 
        :width="item.width">
      </el-table-column>
      <el-table-column label="操作" width="140" align="center" fixed="right">
        <template #default="{ row }">
          <el-button type="danger" link @click="handleDelele(row)">删除</el-button>
          <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script setup>
import sortable from 'sortablejs'
import { onMounted, ref } from 'vue'

// 表格数据
const dataList = ref([])
const loading = ref(false)

// 表头配置
const head = [
  { label: '阶段序号', prop: 'stageNo' },
  { label: '开业阶段', prop: 'stageName' },
  { label: '奖励说明', prop: 'rewardDesc' }
]

// 初始化拖拽功能
const initSortable = () => {
  // 获取表格的 tbody 元素
  const tbody = document.querySelector('.table-draggable .el-table__body-wrapper tbody')
  
  if (!tbody) return
  
  // 创建 Sortable 实例
  sortable.create(tbody, {
    // 指定可拖拽的子元素
    draggable: '.table-draggable .el-table__row',
    // 动画效果
    animation: 150,
    // 拖拽结束回调
    onEnd({ newIndex, oldIndex }) {
      // newIndex 拖动到的新的索引;oldIndex 没拖动前的索引
      // 处理数据重新排序
      const currRow = dataList.value.splice(oldIndex, 1)[0]
      dataList.value.splice(newIndex, 0, currRow)
      
      // 调用 API 保存排序结果
      saveSortOrder()
    }
  })
}

// 保存排序结果到后端
const saveSortOrder = () => {
  const stageIds = dataList.value.map(x => x.id)
  // 调用 API 接口
  api.dragSortOpeningStageList({ stageIds })
}

// 组件挂载后初始化拖拽
onMounted(() => {
  loadData()
  initSortable()
})

// 加载数据
const loadData = () => {
  loading.value = true
  api.getOpeningStageList()
    .then(res => {
      dataList.value = res.data
    })
    .finally(() => {
      loading.value = false
    })
}
</script>

<style scoped>
.table-draggable {
  margin-top: 8px;
}
.icon-touch {
  width: 14px;
  cursor: pointer;
}
</style>

实现要点解析

1. SortableJS 初始化

javascript 复制代码
sortable.create(tbody, {
  draggable: '.table-draggable .el-table__row',
  animation: 150,
  onEnd({ newIndex, oldIndex }) {
    // 处理排序逻辑
  }
})

关键参数说明:

  • draggable: 指定可拖拽的元素选择器
  • animation: 拖拽动画时长(毫秒)
  • onEnd: 拖拽结束时的回调函数

2. 数据同步处理

当用户完成拖拽后,我们需要同步更新前端数据和后端存储:

javascript 复制代码
onEnd({ newIndex, oldIndex }) {
  // 1. 更新前端数据
  const currRow = dataList.value.splice(oldIndex, 1)[0]
  dataList.value.splice(newIndex, 0, currRow)
  
  // 2. 保存到后端
  const stageIds = dataList.value.map(x => x.id)
  api.dragSortOpeningStageList({ stageIds })
}

3. 获取正确的 DOM 元素

由于 Element Plus 的表格结构比较复杂,需要准确找到 tbody 元素:

javascript 复制代码
const tbody = document.querySelector('.table-draggable .el-table__body-wrapper tbody')

进阶:拖拽表格组件

可以把上述代码整合为可复用的拖拽表格组件 DragTable.vue

js 复制代码
<template>
  <div class="drag-table-container">
    <div class="table-header">
      <span class="title">{{ title }}</span>
      <el-button type="primary" @click="$emit('add')">添加</el-button>
    </div>
    <div class="table-draggable">
      <el-table :data="data" row-key="id" v-loading="loading" :element-loading-text="loadingText">
        <el-table-column label="排序" width="80" align="center">
          <img class="drag-handle" :src="`${mdFileBaseUrl}/stand/icon/touchMove.svg`" />
        </el-table-column>
        <el-table-column
          v-for="column in columns"
          :key="column.prop"
          :label="column.label"
          :prop="column.prop"
          :width="column.width"
          :formatter="column.formatter">
        </el-table-column>
        <el-table-column
          v-if="showActions"
          label="操作"
          width="140"
          align="center"
          fixed="right">
          <template #default="{ row }">
            <slot name="actions" :row="row">
              <el-button type="danger" link @click="$emit('delete', row)">删除</el-button>
              <el-button type="primary" link @click="$emit('edit', row)">编辑</el-button>
            </slot>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'
import sortable from 'sortablejs'
const props = defineProps({
  data: {
    type: Array,
    default: () => []
  },
  columns: {
    type: Array,
    default: () => []
  },
  loading: {
    type: Boolean,
    default: false
  },
  loadingText: {
    type: String,
    default: '正在加载中'
  },
  title: {
    type: String,
    default: '数据列表'
  },
  showActions: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['sort-end', 'add', 'edit', 'delete'])

// 初始化拖拽
const initSortable = () => {
  nextTick(() => {
    const tbody = document.querySelector('.table-draggable .el-table__body-wrapper tbody')
    if (!tbody) return

    sortable.create(tbody, {
      draggable: '.table-draggable .el-table__row',
      animation: 150,
      ghostClass: 'sortable-ghost',
      chosenClass: 'sortable-chosen',
      onEnd({ newIndex, oldIndex }) {
        if (newIndex === oldIndex) return

        const newData = [...props.data]
        const [movedItem] = newData.splice(oldIndex, 1)
        newData.splice(newIndex, 0, movedItem)

        emit('sort-end', {
          newIndex,
          oldIndex,
          newData,
          movedItem
        })
      }
    })
  })
}

onMounted(() => {
  initSortable()
})
</script>

<style scoped>
.drag-table-container {
  width: 100%;
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.table-header .title {
  font-size: 16px;
  font-weight: bold;
}
.drag-handle {
  width: 14px;
  cursor: grab;
}
:deep(.sortable-ghost) {
  opacity: 0.5;
  background: #c8ebfb;
}
:deep(.sortable-chosen) {
  background-color: #ecf5ff;
}
</style>

使用示例

js 复制代码
<template>
  <DragTable
    :data="list"
    :columns="columns"
    :loading="loading"
    title="测试阶段管理"
    @sort-end="handleSortEnd"
    @add="handleAdd"
    @edit="handleEdit"
    @delete="handleDelete"
  />
</template>

<script setup>
import { ref } from 'vue'
import DragTable from './components/DragTable.vue'

const list = ref([
  { id: 1, stageName: '阶段1', rewardDesc: '奖励100元' },
  { id: 2, stageName: '阶段2', rewardDesc: '奖励200元' },
  { id: 3, stageName: '阶段3', rewardDesc: '奖励300元' },
  { id: 4, stageName: '阶段4', rewardDesc: '奖励400元' }
])
const loading = ref(false)

const columns = [
  { label: '阶段名称', prop: 'stageName', width: 100},
  { label: '奖励说明', prop: 'rewardDesc' }
]

const handleSortEnd = ({ newData }) => {
  // 调用 API 保存排序
  console.log('排序后的数组:', newData)
}

const handleAdd = () => {
  // 添加逻辑
}

const handleEdit = (row) => {
  // 编辑逻辑
}

const handleDelete = (row) => {
  // 删除逻辑
}
</script>

注意事项

  1. DOM 元素获取时机:确保在表格渲染完成后初始化 SortableJS
  2. 数据同步:拖拽后要及时更新数据和调用后端接口
  3. 性能优化:对于大数据量的表格,考虑虚拟滚动
  4. 移动端适配:SortableJS 支持触摸事件,但需要测试移动端体验

总结

通过 SortableJS 实现 Element Plus 表格的拖拽排序功能,可以大大提升用户体验。关键点在于正确获取 DOM 元素、处理数据同步以及提供良好的视觉反馈。本文提供的组件可以作为一个基础模板,根据实际需求进行扩展和优化。

相关推荐
星链引擎5 小时前
大语言模型的技术突破与稳定 API 生态的构建
前端
还是大剑师兰特5 小时前
TypeScript 面试题及详细答案 100题 (71-80)-- 模块与命名空间
前端·javascript·typescript
玉宇夕落5 小时前
HTML5 音乐敲击乐静态界面
前端
海在掘金611275 小时前
告别"拼写错误":TS如何让你的代码"字字精准"
前端
用户47949283569155 小时前
什么是XSS攻击,怎么预防,一篇文章带你搞清楚
前端·javascript·安全
摸着石头过河的石头5 小时前
深入理解JavaScript事件流:从DOM0到DOM3的演进之路
前端·javascript·性能优化
卡尔特斯6 小时前
油猴脚本支持的所有 UserScript
前端
披萨心肠6 小时前
理解JavaScript中的函数参数传递
前端·javascript
吞吞07116 小时前
Alpine.js 技术文档
前端