Vue Element UI 结合 sortable.js 实现嵌套表格行内拖拽排序
本文介绍在 Vue 项目中利用 Element UI 表格组件与 sortable.js 库,实现多层嵌套表格的行内拖拽排序功能。通过后端返回的 id、next、prev 属性维护数据顺序,结合深拷贝与递归加载逻辑,确保拖拽时不跨主级且能正确更新排序状态。
在 Vue 项目中,结合 Element UI 与 sortable.js 实现嵌套表格的拖拽功能。
项目需求涉及一个多层嵌套表格,要求行数据支持拖拽排序,但限制不能跨主级拖拽。由于下拉操作需异步获取数据,经过评估,最终选择使用 Vue 配合 Element UI 进行开发。鉴于 Element UI 原生表格缺乏拖拽排序能力,因此引入 sortable.js 来补充这一功能。
后端接口返回的排序规则中,每条数据包含三个关键属性:id、next 和 prev。
系统利用这三个属性实时动态更新表格的排序状态。
- id 代表当前数据的唯一标识。
- next 指向下一条数据的 id。
- prev 指向上一条数据的 id。
HTML 模板部分结构如下:

Data 数据定义部分如下:
flattArray:[],
originalData:[],
tableData: [],
arr:[],
maps:new Map()此处定义了四个数组以及一个 Map 对象。
flattArray 用于存储平铺后的数据;originalData 同样为平铺数据,主要用于数据出错时的回退参考,无实际业务用途;tableData 是用于表格渲染的核心数据;arr 则用于存储点击下拉时异步请求获取的子级数据。
maps 对象用于在 tableData 发生变化后,重新渲染数据时使用。
Methods 方法部分实现如下:
首先定义了一个深拷贝方法,确保在修改某条数据时,其他数据不受影响。
//深拷贝
getNewObject(val) {
let resObj = JSON.stringify(val);
return JSON.parse(resObj);
},页面加载时从接口获取 tableData 数据,并对其进行深拷贝处理。
getDetails(id){
axios.get(url).then(res =>{
if(res.data.code === 0){
this.tableData = res.data.data
this.tableData.map( item =>{
item.hasChildren = true
delete item.children
})
this.flattArray = this.getNewObject(this.tableData)
this.originalData = this.getNewObject(this.tableData)
}else{
this.$message.error('网络错误');
}
}).catch(function (error) {
this.$message.error('网络错误');
});
},此时表格已初步渲染。由于下拉数据需异步获取,因此在 Element 提供的下拉事件中触发数据请求。此处获取子级数据并赋值给 maps,利用递归逻辑将数据平铺并组装。
//点击下拉图标异步加载数据
load(tree, treeNode, resolve) {
this.maps.set(tree.id, { tree, treeNode, resolve })
axios.get(`url` + tree.id).then(res => {
if (res.data.code == 0) {
this.arr = res.data.data
this.arr.map(item => {
item.hasChildren = true
delete item.children
this.flattArray.push(item)
})
resolve(this.arr)
const tree = buildTree(this.flattArray, 1);
//组装tree
function buildTree(nodes, parent) {
const res = [];
for (let item of nodes) {
if (item.parentId === parent) {
const children = buildTree(nodes, item.id);
if (children.length) {
item.children = children;
}
res.push(item);
}
}
return res;
}
//平铺tree
let result = [];
function flatTree(nodes, parentId) {
if (!nodes || nodes.length === 0) return [];
nodes.forEach(node => {
result.push(node);
return flatTree(node.children, node.id);
});
}
flatTree(tree, 1);
this.originalData = result
this.getNewObject(this.originalData)
} else {
this.$message.error('没有更多消息了');
}
})
},定义的行拖拽方法中,由于排序依赖三个属性,因此需考虑三种情况。
- 第一种情况是拖动至最上级,此时 prev 值为空。
- 第二种情况是拖动至最下级,此时 next 值为空。
- 第三种情况为正常拖动,此时 next 和 prev 均不为空。
rowDrop() {
this.$nextTick(() => {
const tbody = document.querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
animation: 300,
sort: true,
onEnd({
oldIndex,
newIndex,
item
}) {
const sourceObj = _this.originalData[oldIndex - 1] // 原本的位置
const targetObj = _this.originalData[newIndex - 1] // 移动到的位置
const frontObj = _this.originalData[newIndex - 2] //移动后位置的上一级
const behindObj = _this.originalData[newIndex] //移动后位置的下一级
if(sourceObj.parentId != targetObj.parentId) {
_this.$message.error("不支持跨级拖动,请拖回原来位置")
location.reload();
return;
}
let data = [];
if( oldIndex < newIndex ){//向下移动
//上一级
let predata = {
id: targetObj.id,
next: sourceObj.id,
prev: targetObj.prev
}
//自己
let curdata = {
id: sourceObj.id,
next: targetObj.next,
prev: targetObj.id,
groupId: sourceObj.groupId
}
//下一级
let nextdata = null
if (behindObj != undefined && sourceObj.parentId == behindObj.parentId) {
nextdata = {
id: behindObj.id,
next: behindObj.next,
prev: sourceObj.id
}
}
if(nextdata){
data.push(curdata, predata, nextdata)
}else{
data.push(curdata, predata)
}
_this.postdata(data, sourceObj.parentId)
}else if( oldIndex > newIndex ){//向上移动
//上一级
let predata = null
if (frontObj != undefined && sourceObj.parentId == frontObj.parentId) {
predata = {
id: frontObj.id,
next: sourceObj.id,
prev: frontObj.prev
}
}
//自己
let curdata = {
id: sourceObj.id,
next: targetObj.id,
prev: targetObj.prev,
groupId: sourceObj.groupId
}
//下一级
let nextdata = {
id: targetObj.id,
next: targetObj.next,
prev: sourceObj.id
}
if(predata){
data.push(curdata, predata, nextdata)
}else{
data.push(curdata, nextdata)
}
_this.postdata(data, sourceObj.parentId)
}
}
})
})
},在 mounted 生命周期钩子中加载行改变方法。
mounted() {
this.rowDrop()
},最后动态更新数据以反映排序结果。
refreshLoadTree(parentId) {
// 根据父级id取出对应节点数据
const {tree, treeNode, resolve} = this.maps.get(parentId)
this.$set(this.$refs.tableData.store.states.lazyTreeNodeMap, parentId, [])
if (tree) {
this.load(tree, treeNode, resolve)
}
},至此,需求基本实现。未做更深入挖掘,可能存在潜在问题,但因时间久远,具体细节已遗忘。
总结
以上为个人经验分享,旨在提供参考,欢迎支持本站。
Vue前端日历插件VCalendar集成与使用指南
2026-10-31 18:16:55
uniapp scroll-view 组件实战:Vue3 改造与滚动逻辑详解
2026-10-31 14:49:47
ElementUI el-calendar 周月视图切换与自定义实现
2026-10-31 14:48:43
el-input 限制输入只允许输入浮点型的解决思路
2026-10-31 12:30:12
Vue解决跨域问题的三种常用方案:JSONP、CORS与代理服务器
2026-10-31 07:07:05








