SortableJs树形拖拽使用指南
使用sortable.js拖拽树形结构数据
在网上研究许久没有找到合适的,自己写了一个组件,可同级、跨级拖动,如有bug可自行修改。
使用
安装SortableJs
$ npm install sortablejs --save
定义个事件总线 使用store也可以
// eventBus.js
import Vue from 'vue'
const Bus = new Vue()
export default Bus
main.js全局引入事件总线
import Bus from '@/plugins/eventBus'
Vue.prototype.$bus = Bus
定义VUE树形组件
<template>
<div class="tree-list nested-sortable" ref="nestedSortable">
<div v-for="(item, index) in value" :key="item.id" class="tree-list-item" :class="`nested-${deep}`" :data-tid="item.id">
<div class="title">
<span class="drag-node">
<i v-if="!root && parent.paramType !== 'JSONArray'" class="el-icon-rank drag-icon"></i>
</span>
<span class="open-arrow">
<template v-if="item.paramType === 'JSONObject' || item.paramType === 'JSONArray'">
<el-button :icon="hiddenList.includes(item.id) ? 'el-icon-arrow-right' : 'el-icon-arrow-down'" type="text" size="" @click="hiddenNode(item.id)"></el-button>
</template>
</span>
<span>{{ item.columnName }}</span>
<span class="tree-list-btns">
<el-button icon="el-icon-plus" type="text" size="" @click="addNode(index)"></el-button>
<el-button v-if="!root && parent.paramType !== 'JSONArray'" icon="el-icon-minus" type="text" size="" @click="removeNode(index)"></el-button>
</span>
</div>
<template v-if="item.children">
<sort-list v-model="item.children" :deep="deep + 1" :root="false" class="nestedSortable-children" :class="{ 'hidden': hiddenList.includes(item.id) }" :data-pid="item.id" :parent="item"></sort-list>
</template>
</div>
</div>
</template>
<script>
import SortList from './SortList.vue'
import Sortable from 'sortablejs'
export default {
name: 'SortList',
components: {
SortList
},
props: {
value: {
type: Array,
default: () => {
return []
}
},
// 节点深度
deep: Number,
// 是否根节点
root: {
type: Boolean,
default: false
},
// 父节点数据
parent: {
type: Object,
default: null
}
},
data() {
return {
hidden: false,
// 当前隐藏的节点ID数组
hiddenList: [],
// 排序实例
sortInstance: null
}
},
mounted() {
// 在递归组件中初始化实例,根节点不必拖动
!this.root && this.initSortable()
// 根节点监听排序做数据变更
if (this.root) {
this.$bus.$on('treeList:sort', (data) => {
console.log('treeList:sort', data)
// 新节点的数据
let newList = this.findArr(data.newPid)
// 旧节点的数据
let oldList = this.findArr(data.oldPid)
// 删除旧位置的数据
let delRow = oldList.splice(data.oldIndex, 1)[0]
// 新位置插入数据
newList.splice(data.newIndex, 0, delRow)
})
}
},
beforeDestroy () {
// 销毁监听
if (this.root) {
this.$bus.$off('treeList:sort')
this.sortInstance = null
}
},
methods: {
// 隐藏子级
hiddenNode(id){
let hasHidden = this.hiddenList.findIndex(item => item===id)
if (this.hiddenList.includes(id)) {
this.hiddenList.splice(hasHidden, 1)
} else {
this.hiddenList.push(id)
}
},
// 根据ID查询数组
findArr(id) {
let list
const deepFind = (arr) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === id) {
list = arr[i].children
break
}
if (arr[i].children?.length) {
deepFind(arr[i].children)
}
}
}
deepFind(this.value || [])
return list
},
// 根据ID查询父节点数据
findParent (id) {
let obj = null
const deepFind = (arr) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === id) {
obj = arr[i]
break
}
if (arr[i].children?.length) {
deepFind(arr[i].children)
}
}
}
deepFind(this.value || [])
return obj
},
// 增加节点
addNode (index) {
let list = this.value
list.splice(index+1, 0, {
'id': 'item-1-1', // 自己生成
'columnName': 'item-1-1',
'paramType': 'String',
'paramField': 'in',
'paramName': '名称',
'isNull': 1,
'paramExample': '张三',
'description': '三三',
'isRequired': '1'
})
},
// 删除节点
removeNode (index) {
let list = this.value
list.splice(index, 1)
},
// 初始化拖拽实例
initSortable () {
let _this = this
let el = this.$refs['nestedSortable']
this.sortInstance = new Sortable(el, {
group: {
name: 'nested-'+ _this.deep,
put (evt) {
// 控制是否可以拖入
if (_this.parent.paramType === 'JSONObject') {
return true
} else {
return false
}
},
pull(){
// 控制是否可以进行拖拽
if(_this.parent.paramType === 'JSONArray') {
return false
}
return true
}
},
handle: '.drag-node',
sort: true,
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onEnd(evt) {
let { newIndex, oldIndex, to, item } = evt
console.log('-----onEnd-----', newIndex, oldIndex, evt)
// 触发顶层数据变更,使用store也行
_this.$bus.$emit('treeList:sort', {
newIndex,
oldIndex,
id: item.tid,
newPid: to.dataset.pid,
oldPid: _this.parent.id
})
}
});
}
}
};
</script>
<style lang='scss' scoped>
.tree-list {
.tree-list-item {
margin-left: 30px;
}
.tree-list-btns {
display: inline-block;
margin-left: 20px;
}
.drag-node {
display: inline-block;
width: 20px;
height: 20px;
&:hover {
cursor: move;
}
}
.hidden {
display: none;
}
.open-arrow {
display: inline-block;
width: 25px;
}
}
</style>
使用
<template>
<div>
<sort-list v-model="json" :root="true" :deep="1"></sort-list>
</div>
</template>
<script>
import SortList from './SortList.vue'
export default {
name: 'SortTableList',
components: {
SortList
},
mixins: [],
data() {
return {
json: [
{
'columnName': 'root',
'paramType': 'JSONObject',
'paramField': 'in',
'paramName': '名称',
'id': 'item-0',
'children': [
{
'id': 'item-1-1',
'columnName': 'item-1-1',
'paramType': 'String',
'paramField': 'in',
'paramName': '名称',
'isNull': 1,
'paramExample': '张三',
'description': '三三',
'isRequired': '1'
}, {
'id': 'item-1-2',
'columnName': 'item-1-2 JSONObject',
'paramType': 'JSONObject',
'paramField': 'in',
'paramName': '名称',
'isNull': 1,
'paramExample': '张三',
'description': '三三',
'isRequired': '',
'children': [
{
'id': 'item-1-2-1',
'columnName': 'item-1-2-1',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item-1-2-1',
'description': '1111',
'isRequired': '1'
}, {
'id': 'item-1-2-2',
'columnName': 'item-2-2',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item222',
'description': '222',
'isRequired': '1'
}, {
'id': 'item-1-2-3',
'columnName': 'item-1-2 JSONObject',
'paramType': 'JSONObject',
'paramField': 'in',
'paramName': '名称',
'isNull': 1,
'paramExample': '张三',
'description': '三三',
'isRequired': '',
'children': [
{
'id': 'item-1-2-3-1',
'columnName': 'item-1-2-3-1',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item-1-2-3-1',
'description': '1111',
'isRequired': '1'
}, {
'id': 'item-1-2-3-2',
'columnName': 'item-1-2-3-2',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item-1-2-3-2',
'description': 'item-1-2-3-2',
'isRequired': '1'
}
]
}
]
}, {
'id': 'item-1-3',
'columnName': 'item-1-3 JSONArray',
'paramType': 'JSONArray',
'paramField': 'in',
'paramName': '名称',
'isNull': 1,
'paramExample': '张三',
'description': '三三',
'isRequired': '1',
'children': [{
'id': 'item-1-3-1',
'columnName': 'item-1-3-1',
'paramType': 'JSONObject',
'paramField': 'in',
'paramName': '名称',
'children': [
{
'id': 'item-1-3-1-1',
'columnName': 'item-1-3-1-1',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item111',
'description': '1111',
'isRequired': '1'
}, {
'id': 'item-1-3-1-2',
'columnName': 'item-1-3-1-2',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item222',
'description': '222',
'isRequired': '1'
}, {
'id': 'item-1-3-1-3',
'columnName': 'item-1-3-1-3',
'paramType': 'String',
'paramField': 'in',
'paramName': 'item-1',
'isNull': 1,
'paramExample': 'item-1-3-1-3',
'description': '222',
'isRequired': '1'
}
]
}]
}
]
}
]
}
},
watch: {
json : {
deep: true,
handler(val) {
console.log(JSON.stringify(val, null, 2))
}
}
}
};
</script>