element UI树形表格拖动创建子级
呜呜呜,手上项目最新的需求要求写一个树形图表格,这就算了,还要拖动,单行的表格拖动也不复杂,网上一搜就是很多,用sortablejs轻松搞定。然而树形的表格,拖动的时候就不那么容易了,并且平常拖动基本上是交换顺序的作用,我们的需求不一样,是将一条数据拖到另一条数据下面变成子级,折腾了我半天,于是又来这里哭诉了,哈哈哈。
言归正传,开始编写基于elementUI的拖拽表格吧。好在element 已经做好了树形表格,我只需要加上拖动功能,拖动还是使用的sortablejs,主要就是计算拖动后的数据比较麻烦。
直接看代码吧:
html 部分:
<el-table :data="tableData" style="width: 100%;margin-bottom: 20px;" :row-key="rowKey" border ref="treeTable" :expand-row-keys="expandRow" @cell-mouse-enter.once='rowDrop' :tree-props="treeProps"> <slot></slot> </el-table>
js部分:
js部分我写了几个函数,其中:
getExpandRow函数是遍历层级,根据设置的默认展开到第几层,拿到符合条件的id数组,表格根据这个数组默认展开几级。
getDealData函数也是遍历树形结构,处理数据,将树形结构平铺,一条一条的存入数组,这是因为element 的树形表格拖动时是一行一行的拖动的,用sortablejs拖动拿到的也是每一行的下标,这样存入数组了之后,就能通过下标拿到拖动的这一行的数据和即将放入的那一行的数据,并且存的时候我将children也存入了的,这样根据下标拿到的数据就是当前节点和下面的所有子节点。就可以将其整个的移动到目标节点下面。
rowDrop是初始化拖动表格的函数,调用时没有在mouted里调用,是为了避免在表格还没有渲染好就去调用,这样会报错,放在表格的cell-mouse-enter.once事件里,并用once修饰符,执行一次,就可以了。rowDrop里对树形数据进行了处理,处理前都去执行了getDealData,是因为每次拖动了,表格都会重新排序。处理树形数据的时候需要依据平铺的数组。
重点是changeData函数,它是rowDrop的具体实施,主要考虑到几个方面:
1.父级不能移动到子级下面;
2.子级已经在当前的父级下面了,则不需要再插入,否则会重复;
3.因为只想遍历一次数据,所以在遍历的时候同时做了删除拖动的节点,并将这个节点移动到目标节点下面;
4.最后重新渲染表格时,我最开始用了doLayout,发现没有生效,界面上树形的折叠按钮显示缩进不对,用$nextTick()也还是不对,最后决定就还是先清空数组,然后再赋值,这样表格就重新渲染了。
import Sortable from 'sortablejs' export default { props: { data: { type: Array, default: () => [] }, treeProps: { type: Object, default: () => { return { children: 'children', hasChildren: 'hasChildren' } } }, rowKey: { type: String, default: 'id' }, expandLevel: { type: Number, default: 3 } }, watch: { data: { handler: function(val) { this.tableData = JSON.parse(JSON.stringify(val)) this.getExpandRow(val) }, deep: true, immediate: true } }, data() { return { selectObj: {}, tableData: [], flattArray: [], expandRow: [] } }, mounted() { this.getDealData() }, methods: { getExpandRow(data) { const result = [] const children = this.treeProps.children const rowKey = this.rowKey let level = 0 // 默认展开三级 const func = (arr, parent) => { arr.forEach(item => { if (item[children] && item[children].length !== 0) { if (level < this.expandLevel) { result.push(item[rowKey] + '') } level++ func(item[children], item) } }) } func(data) this.expandRow = result }, getDealData() { const result = [] const children = this.treeProps.children const rowKey = this.rowKey const func = function(arr, parent) { arr.forEach(item => { const obj = Object.assign(item) if (parent) { if (obj.parentIds) { obj.parentIds.push(parent[rowKey]) } else { obj.parentIds = [parent[rowKey]] } } // 将每一级的数据都一一装入result,不需要删除下面的children,方便拖动的时候根据下标直接拿到整个数据,包括当前节点的子节点 result.push(obj) if (item[children] && item[children].length !== 0) { func(item[children], item) } }) } func(this.tableData) this.flattArray = result }, rowDrop() { const tbody = document.querySelector('.el-table__body-wrapper tbody') const self = this Sortable.create(tbody, { onEnd({ newIndex, oldIndex }) { self.getDealData() const sourceObj = self.flattArray[oldIndex] const targetObj = self.flattArray[newIndex] // 改变要显示的树形数据 self.changeData(sourceObj, targetObj) } }) }, changeData(sourceObj, targetObj) { let flag = 0 const data = Object.assign(this.tableData) const children = this.treeProps.children const rowKey = this.rowKey const func = function(arr, parent) { for (let i = arr.length - 1; i >= 0; i--) { const item = arr[i] // 判断是否是原来拖动的节点,如果是,则删除 if (item[rowKey] === sourceObj[rowKey] && (!parent || parent && parent[rowKey] !== targetObj[rowKey])) { arr.splice(i, 1) flag++ } // 判断是否是需要插入的节点,如果是,则装入数据 if (item[rowKey] === targetObj[rowKey]) { if (item[children]) { // 判断源数据是否已经是在目标节点下面的子节点,如果是则不移动了 let repeat = false item[children].forEach(e => { if (e[rowKey] === sourceObj[rowKey]) { repeat = true } }) if (!repeat) { sourceObj.parentIds = [] item[children].unshift(sourceObj) } } else { sourceObj.parentIds = [] item[children] = [sourceObj] } flag++ } // 判断是否需要循环下一级,如果需要则进入下一级 if (flag !== 2 && item[children] && item[children].length !== 0) { func(item[children], item) } else if (flag === 2) { break } } } // 检测是否是将父级拖到子级下面,如果是则数据不变,界面重新回到原数据 if (targetObj.parentIds) { if (targetObj.parentIds.findIndex(_ => _ === sourceObj[this.rowKey]) === -1) { func(data) } else { this.$message.error('不能将父级拖到子级下面') } } else { func(data) } this.$set(this, 'tableData', []) // 重新渲染表格,用doLayout不生效,所以重新装了一遍 this.$nextTick(() => { this.$set(this, 'tableData', data) }) } } }