el-table multi-level table header drag and drop

Scene

The business needs to do multi-level header dragging. The specific requirements are as follows:
A parent header without a child header cannot be dragged. The first serial number cannot be dragged. The parent header is transposed when dragged. When a child header under the same parent header is dragged, the child header is transposed. Different parent tables When the sub-header is dragged under the head, the sub-header remains unchanged and the two parent headers are swapped.

1. Code

1. Page part

What I have here is a data table with multi-level headers. The Element UI library is used to build the table component, and the sortable.js plug-in is used to implement the drag-and-drop sorting function of table columns.
In the template part, the component is used to display table data, and the header column and sub-column are rendered through the v-for instruction loop. Each column has corresponding attributes set, such as label, align, sortable, etc. The code for displaying the corresponding data page in the table is as follows:

<template>
<el-table :data="tableData" tooltip-effect="dark" class="financeTotalTwo financeTotal_spfcjhz"
style="width: 100%" height="100%" :size="GLOBAL.tableSize" ref="multipleTable">
<el-table-column type="index" width="80" label="serial number" align="center"></el-table-column>
<el-table-column
v-for="(item, index) in titleList"
v-if="item.show"
:label="item.label"
:align="item.align"
:sortable="item.sortable"
:prop="item.prop"
:min-width="item.width"
:class-name="(item.prop == 'cityName' ||item.prop == 'tjDate' || item.prop == 'whHouseUseName' ||item.prop == 'blockName' || item.prop =='districtName' ) ? 'disableDrag':'allowDrag'"
:key="item.prop + index"
class="allowDrag"
>
<template slot-scope="scope">
<div>{<!-- -->{ scope.row[item.prop] }}</div>
</template>
<el-table-column
v-for="(ziitem, zindex) in item.children"
v-if="ziitem.show & amp; & amp; item.children.length >0"
:label="ziitem.label"
:align="ziitem.align"
:sortable="ziitem.sortable"
:prop="ziitem.prop"
:min-width="zitem.width"
class-name="zallowDrag"
:class="'draggable'"
:key="ziitem.prop + zindex"
>
<template slot-scope="scope">
<div class="finance_table_alignRight">{<!-- -->{ scope.row[zitem.prop] }}</div>
</template>
</el-table-column>
</el-table-column>
</el-table>
</template>

2. Table data and drag and drop processing part

In the script part, the component’s data objects are defined, including tableHeaderList, titleList and tableData. tableHeaderList is used to store table header configuration information. titleList filters out valid table header information based on tableHeaderList. tableData is the actual table data to be displayed.
In the component’s life cycle hook function, the loadTitleList method is called to initialize the effective header list, and the columnDrop method is delayed through setTimeout to initialize the drag and drop function of the table columns.
The loadTitleList method traverses the tableHeaderList, determines whether the header is valid based on the show attribute, and if valid, adds it to the titleList.
The columnDrop method initializes the drag-and-drop function of table columns and binds the parent header column and child header column respectively. At the end of dragging, perform corresponding operations on titleList according to the starting index and ending index of dragging, including header exchange and sub-header exchange:

<script>
import Sortable from 'sortablejs';
import { moveArr, strMapToObj} from './utils/utils'
export default {
name: "landSj",
components: {
},
data() {
return {
tableHeaderList: [
//Multi-level header data
{
"id":4,
"show":true,
"prop":"tjDate",
"label":"time",
"width":130,
"sortable":false,
"children":[
\t\t\t\t\t\t\t\t\t
],
"align":"center",
"width":130,
"template":true
},
{
"id":5,
"show":true,
"label":"Supply information",
"prop":'gy',
"sortable":false,
"children":[
{
"id":6,
"show":true,
"prop":"gyts",
"label":"Number of sets supplied",
"sortable":"gyts",
"align":"center",
"width":130,
"template":true
},
{
"id":7,
"show":true,
"prop":"gymj",
"label":"Supply area (㎡)",
"sortable":"gymj",
"align":"center",
"width":130,
"template":true
}
],
"align":"center",
"width":130,
"template":true
},
{
"id":11,
"show":true,
"label":"Forecast transaction information",
"sortable":false,
"prop":'yccj',
"children":[
{
"id":12,
"show":true,
"prop":"ysts",
"label":"Predicted number of transactions",
"sortable":"ysts",
"align":"center",
"width":130,
"template":true
},
{
"id":13,
"show":true,
"prop":"ysmj",
"label":"Predicted area (㎡)",
"sortable":"ysmj",
"align":"center",
"width":130,
"template":true
},
{
"id":14,
"show":true,
"prop":"yszj",
"label":"Forecast transaction amount (10,000 yuan)",
"sortable":"yszj",
"align":"center",
"width":130,
"template":true
},
{
"id":15,
"show":true,
"prop":"ysjj",
"label":"Forecast average transaction price",
"sortable":"ysjj",
"align":"center",
"width":130,
"template":true
}
],
"align":"center",
"width":130,
"template":true
},
{
"id":21,
"show":true,
"label":"Inventory information",
"sortable":false,
"prop":'cl',
"children":[
{
"id":22,
"show":true,
"prop":"clts",
"label":"Number of sets in stock",
"sortable":"clts",
"align":"center",
"width":130,
"template":true
},
{
"id":23,
"show":true,
"prop":"clmj",
"label":"Inventory area (㎡)",
"sortable":"clmj",
"align":"center",
"width":130,
"template":true
},
{
"id":24,
"show":true,
"prop":"xzxclts",
"label":"Restricted number of sets",
"sortable":"xzxclts",
"align":"center",
"width":130,
"template":true
},
{
"id":25,
"show":true,
"prop":"xzxclmj",
"label":"Restricted area (㎡)",
"sortable":"xzxclmj",
"align":"center",
"width":130,
"template":true
}
],
"align":"center",
"width":130,
"template":true
}
],
titleList:[],
tableData:[
{
"cityId": null,
"cityName": null,
"districtId": null,
"districtName": null,
"blockId": null,
"blockName": null,
"tjDate": "2023-01-01-2023-10-26",
"gyts": "30119",
"gymj": "4027187.55",
"qngyts": null,
"qngymj": null,
"ysts": "60022",
"ysmj": "7741787.72",
"yszj": "25548525",
"ysjj": "33001",
"qnysts": null,
"qnysmj": null,
"qnyszj": null,
"qnysjj": null,
"clts": "0",
"clmj": "0.00",
"xzxclts": "0",
"xzxclmj": "0.00"
}
]
};
},
created() {
},
mounted() {
this.loadTitleList();
setTimeout(() => {
this.columnDrop();
}, 100);
},
beforeUpdate(){
this.$nextTick(() => { //After the data is loaded, re-render the table
if(this.$refs['multipleTable']){
this.$refs['multipleTable'].doLayout();
}
})
},
methods: {
/**
* Get valid form
*/
loadTitleList(){
this.titleList = []
// Manufacturing data
for(var i = 0;i <this.tableHeaderList.length;i + + ){
if(this.tableHeaderList[i].show){
this.titleList.push(this.tableHeaderList[i]);
}
};
},
//Table column drag and drop
columnDrop() {
//Drag the parent table header column
const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
this.sortable = Sortable.create(wrapperTr, {
handle:".allowDrag",
animation: 180,
delay: 0,
filter: function (evt) {
const element = evt.target;
// Determine whether the element has the allowDrag class name, if so, it can be dragged
return element.classList.contains('allowDrag');
},
onEnd: evt => {
if (evt.oldIndex === evt.newIndex) return;
//Header sorting, the first column is the serial number, the second column is the time, because the requirement requires that the serial number and time do not participate in drag and drop, so it is reduced by 1
moveArr(this.titleList, evt.newIndex -1, evt.oldIndex -1)
},
onMove(e) {
return e.related.className.indexOf('allowDrag') > 1;
}
})
//Drag the subheader column
const wrapperTrx = document.querySelector('.el-table__header-wrapper tr + tr')
this.sortablex = Sortable.create(wrapperTrx, {
handle:".zallowDrag",
animation: 180,
delay: 0,
onEnd: evt => {
if (evt.oldIndex === evt.newIndex) return;
// Calculate the starting index and ending index of the subheader
let startIndex = 0;
for (let i = 1; i <this.titleList.length; i + + ) {
const childrenLength = this.titleList[i].children.length;
if (startIndex <= evt.oldIndex & amp; & amp; evt.oldIndex < startIndex + childrenLength) {
const adjustedOldIndex = evt.oldIndex - startIndex;
const adjustedNewIndex = evt.newIndex - startIndex;
if (adjustedNewIndex >= childrenLength) {
// If the length of the current dragging parent header is exceeded, the sub-header operation will not be performed and the parent header exchange operation will be performed directly.
moveArr(this.titleList, i, i + 1)
break;
}
// Determine whether to switch under children of the same subheader
if (adjustedOldIndex >= 0 & amp; & amp; adjustedOldIndex < childrenLength & amp; & amp; adjustedNewIndex >= 0 & amp; & amp; adjustedNewIndex < childrenLength) {
//'Same parent header and subheader exchange operation
moveArr(this.titleList[i].children, adjustedNewIndex, adjustedOldIndex);
break;
}else{
//Not the same parent header, only perform parent header operations
moveArr(this.titleList, i, i-1)
break;
}
}
startIndex + = childrenLength;
}
\t\t\t\t\t\t\t
},
onMove(e) {
return e.related.className.indexOf('allowDrag') > 1;
}
})
},
}
};
</script>
 moveArr(arr, newIndex, oldIndex) {
    arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0])
}