高级表格:初步行拖拽
概述
在列拖拽的基础上,使用 SortableJS 实现表格行拖拽排序。行拖拽的核心区别在于需要指定拖拽手柄(handle),并处理行数据的响应式更新。本节从数据准备到事件绑定,完成完整的行拖拽功能。
行拖拽 vs 列拖拽对比
| 维度 | 列拖拽 | 行拖拽 |
|---|---|---|
| DOM 目标 | el-table__header-wrapper -> tr | el-table__body-wrapper -> tbody |
| 拖拽范围 | 表头区域 | 表体区域 |
| 数据操作 | 交换 columns 数组顺序 | 交换 data 数组顺序 |
| 拖拽手柄 | 可选 | 推荐(避免误触) |
| SortableJS 选项 | animation、onEnd | handle、animation、onEnd |
实现步骤
1. 准备本地响应式数据
行拖拽需要操作数据本身,因此需要创建本地数据副本:
import { ref, onBeforeMount, onUnmounted } from 'vue'
import Sortable from 'sortablejs'
import type { SortableEvent } from 'sortablejs'
const localData = ref<any[]>([])
onBeforeMount(() => {
localData.value = [...props.data]
})
ts
与列拖拽不同,行拖拽需要深拷贝或浅拷贝 props.data,因为 SortableJS 只操作 DOM,不直接修改 Vue 的响应式数据。
2. 创建行拖拽实例
行拖拽的 DOM 目标是 el-table__body-wrapper 下的 tbody,需要指定 handle 选项限制拖拽触发区域:
let sortableInstance: Sortable | null = null
function initRowDrop() {
const wrapper = document.querySelector(
'.el-table__body-wrapper'
) as HTMLElement
if (!wrapper) return
const tbody = wrapper.querySelector('tbody') as HTMLElement
if (!tbody) return
sortableInstance = Sortable.create(tbody, {
handle: '.drag-button', // 指定拖拽手柄
animation: 300,
onEnd: (evt: SortableEvent) => {
const { oldIndex, newIndex } = evt
if (oldIndex === undefined || newIndex === undefined) return
// 交换数据位置
const item = localData.value.splice(oldIndex, 1)[0]
localData.value.splice(newIndex, 0, item)
emit('drag-row-change', {
data: localData.value,
oldIndex,
newIndex,
})
},
})
}
ts
3. 添加拖拽图标到行首列
在 column 配置的第一列添加拖拽手柄图标:
<template>
<el-table :data="localData" row-key="id">
<el-table-column width="50" align="center">
<template #default>
<el-icon class="drag-button" style="cursor: move">
<Rank />
</el-icon>
</template>
</el-table-column>
<!-- 其他列 -->
</el-table>
</template>
vue
由于 column 是动态渲染的,需要在第一个 column 的 slot 中插入拖拽图标。drag-button 是自定义 class,与 SortableJS 的 handle: '.drag-button' 对应。
4. 生命周期管理
onMounted(() => {
initRowDrop()
})
onUnmounted(() => {
sortableInstance?.destroy()
})
ts
SortableJS 配置详解
SortableJS 提供了丰富的配置选项,行拖拽场景中常用的配置:
interface RowDropOptions {
handle?: string // 拖拽手柄选择器(限制拖拽触发区域)
animation?: number // 动画时长(ms)
ghostClass?: string // 拖拽占位元素样式类名
chosenClass?: string // 被选中元素样式类名
dragClass?: string // 正在拖拽元素样式类名
filter?: string // 不可拖拽元素选择器
draggable?: string // 可拖拽元素选择器
delay?: number // 拖拽延迟(ms),触屏设备防误触
easing?: string // 动画缓动函数
onEnd?: (evt: SortableEvent) => void
onMove?: (evt: MoveEvent, originalEvent: Event) => boolean | -1 | 1 | void
}
ts
核心参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
handle | string | CSS 选择器,只有匹配的元素才能触发拖拽 |
animation | number | 排序动画时长,单位 ms,建议 150-300 |
ghostClass | string | 放置位置的占位元素样式 |
filter | string | 不可拖拽的元素选择器,如合计行 |
draggable | string | 限定哪些子元素可被拖拽 |
onEnd | function | 拖拽结束回调,包含 oldIndex 和 newIndex |
onMove | function | 拖拽过程中实时回调,返回 false 可取消 |
onEnd 事件对象属性
onEnd: (evt: SortableEvent) => {
evt.item // 被拖拽的 HTMLElement
evt.to // 目标列表
evt.from // 来源列表
evt.oldIndex // 原始索引
evt.newIndex // 目标索引
evt.oldDraggableIndex // 原始可拖拽索引
evt.newDraggableIndex // 目标可拖拽索引
evt.clone // 克隆元素(如有)
evt.pullMode // 跨列表时的模式:'clone' | true
}
ts
数据交换逻辑
// 核心交换算法:从旧位置取出,插入新位置
function swapArrayItem(arr: any[], from: number, to: number) {
const item = arr.splice(from, 1)[0]
arr.splice(to, 0, item)
return arr
}
ts
拖拽前:[A, B, C, D, E]
^
oldIndex: 1
v
newIndex: 3
拖拽后:[A, C, D, B, E]
text
关键点:数据交换必须在 onEnd 回调中完成,不能依赖 SortableJS 的 DOM 排序。因为 SortableJS 只操作了 DOM 层面的移动,Vue 的响应式系统并不感知这个变化,需要手动同步数据。
关键实现细节
row-key 的作用
<el-table :data="localData" row-key="id">
vue
row-key帮助 Vue 正确追踪行元素,避免拖拽后渲染异常- 如果不设置
row-key,展开行等功能可能出现表现不一致 - 设置
row-key后需要确保每行数据有唯一标识字段
拖拽手柄的必要性
行拖拽必须指定 handle,原因:
- 表格行中有输入框、按钮等可交互元素
- 不指定 handle 会与这些元素的点击事件冲突
- handle 通常是一个拖拽图标,视觉上明确告知用户可拖拽
随机 ID 生成
当行数据没有唯一标识时,需要生成随机 ID:
const localData = ref(
props.data.map((item, index) => ({
...item,
_rowKey: `${Math.random().toString(36).slice(2, 8)}_${index}`,
}))
)
ts
使用 Math.random() 生成随机字符串拼接到 index 前面,避免当行数增多时 index 重复导致 key 冲突。
实践要点
- 行拖拽的 DOM 目标是
el-table__body-wrapper > tbody,与列拖拽的header-wrapper > tr不同 - 使用
handle选项限制拖拽触发区域,避免与行内交互元素冲突 - 数据交换必须在
onEnd回调中完成,不能依赖 SortableJS 的 DOM 排序 row-key是保证拖拽与表格功能兼容性的关键配置- 随机 ID 需要拼接 index 以防行数增多时产生重复 key
↑