嗚嗚嗚,手上項目最新的需求要求寫一個樹形圖表格,這就算了,還要拖動,單行的表格拖動也不復雜,網上一搜就是很多,用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) }) } } }
