因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到合适的,大部分树形表格都没有拖拽功能,所以决定自己实现一个。这里分享一下实现过程,项目源代码请看github,插件已打包封装好,发布到npm上
本博文会分为两部分,第一部分为使用方式,第二部分为实现方式
安装方式
npm i drag-tree-table --save-dev
使用方式
import dragTreeTable from 'drag-tree-table'
模版写法
<dragTreeTable :data="treeData" :onDrag="onTreeDataChange"></dragTreeTable>
data参数示例
{
lists: [
{
"id":40,
"parent_id":0,
"order":0,
"name":"动物类",
"open":true,
"lists":[]
},{
"id":5,
"parent_id":0,
"order":1,
"name":"昆虫类",
"open":true,
"lists":[
{
"id":12,
"parent_id":5,
"open":true,
"order":0,
"name":"蚂蚁",
"lists":[]
}
]
},
{
"id":19,
"parent_id":0,
"order":2,
"name":"植物类",
"open":true,
"lists":[]
}
],
columns: [
{
type: 'selection',
title: '名称',
field: 'name',
width: 200,
align: 'center',
formatter: (item) => {
return '<a>'+item.name+'</a>'
}
},
{
title: '操作',
type: 'action',
width: 350,
align: 'center',
actions: [
{
text: '查看角色',
onclick: this.onDetail,
formatter: (item) => {
return '<i>查看角色</i>'
}
},
{
text: '编辑',
onclick: this.onEdit,
formatter: (item) => {
return '<i>编辑</i>'
}
}
]
},
]
}
onDrag在表格拖拽时触发,返回新的list
onTreeDataChange(lists) {
this.treeData.lists = lists
}
到这里组件的使用方式已经介绍完毕
实现
- 递归生成树性结构(非JSX方式实现)
- 实现拖拽排序(借助H5的dragable属性)
- 单元格内容自定义展示
组件拆分-共分为四个组件
dragTreeTable.vue是入口组件,定义整体结构
clolmn单元格,内容承载
space控制缩进
看一下dragTreeTable的结构
<template>
<div class="drag-tree-table">
<div class="drag-tree-table-header">
<column
v-for="(item, index) in data.columns"
:width="item.width"
:key="index" >
{{item.title}}
</column>
</div>
<div class="drag-tree-table-body" @dragover="draging" @dragend="drop">
<row depth="0" :columns="data.columns"
:model="item" v-for="(item, index) in data.lists" :key="index">
</row>
</div>
</div>
</template>
看起来分原生table很像,dragTreeTable主要定义了tree的框架,并实现拖拽逻辑
filter函数用来匹配当前鼠标悬浮在哪个行内,并分为三部分,上中下,并对当前匹配的行进行高亮
resetTreeData当drop触发时调用,该方法会重新生成一个新的排完序的数据,然后返回父组件
下面是所有实现代码
1 <script> 2 import row from './row.vue' 3 import column from './column.vue' 4 import space from './space.vue' 5 document.body.ondrop = function (event) { 6 event.preventDefault(); 7 event.stopPropagation(); 8 } 9 export default { 10 name: "dragTreeTable", 11 components: { 12 row, 13 column, 14 space 15 }, 16 props: { 17 data: Object, 18 onDrag: Function 19 }, 20 data() { 21 return { 22 treeData: [], 23 dragX: 0, 24 dragY: 0, 25 dragId: '', 26 targetId: '', 27 whereInsert: '' 28 } 29 }, 30 methods: { 31 getElementLeft(element) { 32 var actualLeft = element.offsetLeft; 33 var current = element.offsetParent; 34 while (current !== null){ 35 actualLeft += current.offsetLeft; 36 current = current.offsetParent; 37 } 38 return actualLeft 39 }, 40 getElementTop(element) { 41 var actualTop = element.offsetTop; 42 var current = element.offsetParent; 43 while (current !== null) { 44 actualTop += current.offsetTop; 45 current = current.offsetParent; 46 } 47 return actualTop 48 }, 49 draging(e) { 50 if (e.pageX == this.dragX && e.pageY == this.dragY) return 51 this.dragX = e.pageX 52 this.dragY = e.pageY 53 this.filter(e.pageX, e.pageY) 54 }, 55 drop(event) { 56 this.clearHoverStatus() 57 this.resetTreeData() 58 }, 59 filter(x,y) { 60 var rows = document.querySelectorAll('.tree-row') 61 this.targetId = undefined 62 for(let i=0; i < rows.length; i++) { 63 const row = rows[i] 64 const rx = this.getElementLeft(row); 65 const ry = this.getElementTop(row); 66 const rw = row.clientWidth; 67 const rh = row.clientHeight; 68 if (x > rx && x < (rx + rw) && y > ry && y < (ry + rh)) { 69 const diffY = y - ry 70 const hoverBlock = row.children[row.children.length - 1] 71 hoverBlock.style.display = 'block' 72 const targetId = row.getAttribute('tree-id') 73 if (targetId == window.dragId){ 74 this.targetId = undefined 75 return 76 } 77 this.targetId = targetId 78 let whereInsert = '' 79 var rowHeight = document.getElementsByClassName('tree-row')[0].clientHeight 80 if (diffY/rowHeight > 3/4) { 81 console.log(111, hoverBlock.children[2].style) 82 if (hoverBlock.children[2].style.opacity !== '0.5') { 83 this.clearHoverStatus() 84 hoverBlock.children[2].style.opacity = 0.5 85 } 86 whereInsert = 'bottom' 87 } else if (diffY/rowHeight > 1/4) { 88 if (hoverBlock.children[1].style.opacity !== '0.5') { 89 this.clearHoverStatus() 90 hoverBlock.children[1].style.opacity = 0.5 91 } 92 whereInsert = 'center' 93 } else { 94 if (hoverBlock.children[0].style.opacity !== '0.5') { 95 this.clearHoverStatus() 96 hoverBlock.children[0].style.opacity = 0.5 97 } 98 whereInsert = 'top' 99 } 100 this.whereInsert = whereInsert 101 } 102 } 103 }, 104 clearHoverStatus() { 105 var rows = document.querySelectorAll('.tree-row') 106 for(let i=0; i < rows.length; i++) { 107 const row = rows[i] 108 const hoverBlock = row.children[row.children.length - 1] 109 hoverBlock.style.display = 'none' 110 hoverBlock.children[0].style.opacity = 0.1 111 hoverBlock.children[1].style.opacity = 0.1 112 hoverBlock.children[2].style.opacity = 0.1 113 } 114 }, 115 resetTreeData() { 116 if (this.targetId === undefined) return 117 const newList = [] 118 const curList = this.data.lists 119 const _this = this 120 function pushData(curList, needPushList) { 121 for( let i = 0; i < curList.length; i++) { 122 const item = curList[i] 123 var obj = _this.deepClone(item) 124 obj.lists = [] 125 if (_this.targetId == item.id) { 126 const curDragItem = _this.getCurDragItem(_this.data.lists, window.dragId) 127 if (_this.whereInsert === 'top') { 128 curDragItem.parent_id = item.parent_id 129 needPushList.push(curDragItem) 130 needPushList.push(obj) 131 } else if (_this.whereInsert === 'center'){ 132 curDragItem.parent_id = item.id 133 obj.lists.push(curDragItem) 134 needPushList.push(obj) 135 } else { 136 curDragItem.parent_id = item.parent_id 137 needPushList.push(obj) 138 needPushList.push(curDragItem) 139 } 140 } else { 141 if (window.dragId != item.id) 142 needPushList.push(obj) 143 } 144 145 if (item.lists && item.lists.length) { 146 pushData(item.lists, obj.lists) 147 } 148 } 149 } 150 pushData(curList, newList) 151 this.onDrag(newList) 152 }, 153 deepClone (aObject) { 154 if (!aObject) { 155 return aObject; 156 } 157 var bObject, v, k; 158 bObject = Array.isArray(aObject) ? [] : {}; 159 for (k in aObject) { 160 v = aObject[k]; 161 bObject[k] = (typeof v === "object") ? this.deepClone(v) : v; 162 } 163 return bObject; 164 }, 165 getCurDragItem(lists, id) { 166 var curItem = null 167 var _this = this 168 function getchild(curList) { 169 for( let i = 0; i < curList.length; i++) { 170 var item = curList[i] 171 if (item.id == id) { 172 curItem = JSON.parse(JSON.stringify(item)) 173 break 174 } else if (item.lists && item.lists.length) { 175 getchild(item.lists) 176 } 177 } 178 } 179 getchild(lists) 180 return curItem; 181 } 182 } 183 } 184 </script>
row组件核心在于递归,并注册拖拽事件,v-html支持传入函数,这样可以实现自定义展示,渲染数据时需要判断是否有子节点,有的画递归调用本身,并传入子节点数据
结构如下
1 <template> 2 <div class="tree-block" draggable="true" @dragstart="dragstart($event)" 3 @dragend="dragend($event)"> 4 <div class="tree-row" 5 @click="toggle" 6 :tree-id="model.id" 7 :tree-p-id="model.parent_id"> 8 <column 9 v-for="(subItem, subIndex) in columns" 10 v-bind:class="'align-' + subItem.align" 11 :field="subItem.field" 12 :width="subItem.width" 13 :key="subIndex"> 14 <span v-if="subItem.type === 'selection'"> 15 <space :depth="depth"/> 16 <span v-if = "model.lists && model.lists.length" class="zip-icon" v-bind:class="[model.open ? 'arrow-bottom' : 'arrow-right']"> 17 </span> 18 <span v-else class="zip-icon arrow-transparent"> 19 </span> 20 <span v-if="subItem.formatter" v-html="subItem.formatter(model)"></span> 21 <span v-else v-html="model[subItem.field]"></span> 22 23 </span> 24 <span v-else-if="subItem.type === 'action'"> 25 <a class="action-item" 26 v-for="(acItem, acIndex) in subItem.actions" 27 :key="acIndex" 28 type="text" size="small" 29 @click.stop.prevent="acItem.onclick(model)"> 30 <i :class="acItem.icon" v-html="acItem.formatter(model)"></i> 31 </a> 32 </span> 33 <span v-else-if="subItem.type === 'icon'"> 34 {{model[subItem.field]}} 35 </span> 36 <span v-else> 37 {{model[subItem.field]}} 38 </span> 39 </column> 40 <div class="hover-model" style="display: none"> 41 <div class="hover-block prev-block"> 42 <i class="el-icon-caret-top"></i> 43 </div> 44 <div class="hover-block center-block"> 45 <i class="el-icon-caret-right"></i> 46 </div> 47 <div class="hover-block next-block"> 48 <i class="el-icon-caret-bottom"></i> 49 </div> 50 </div> 51 </div> 52 <row 53 v-show="model.open" 54 v-for="(item, index) in model.lists" 55 :model="item" 56 :columns="columns" 57 :key="index" 58 :depth="depth * 1 + 1" 59 v-if="isFolder"> 60 </row> 61 </div> 62 63 </template> 64 <script> 65 import column from './column.vue' 66 import space from './space.vue' 67 export default { 68 name: 'row', 69 props: ['model','depth','columns'], 70 data() { 71 return { 72 open: false, 73 visibility: 'visible' 74 } 75 }, 76 components: { 77 column, 78 space 79 }, 80 computed: { 81 isFolder() { 82 return this.model.lists && this.model.lists.length 83 } 84 }, 85 methods: { 86 toggle() { 87 if(this.isFolder) { 88 this.model.open = !this.model.open 89 } 90 }, 91 dragstart(e) { 92 e.dataTransfer.setData('Text', this.id); 93 window.dragId = e.target.children[0].getAttribute('tree-id') 94 e.target.style.opacity = 0.2 95 }, 96 dragend(e) { 97 e.target.style.opacity = 1; 98 99 } 100 } 101 }
clolmn和space比较简单,这里就不过多阐述
上面就是整个实现过程,组件在chrome上运行稳定,因为用H5的dragable,所以兼容会有点问题,后续会修改拖拽的实现方式,手动实现拖拽
开源不易,如果本文对你有所帮助,请给我个star