基于Vue实现可以拖拽的树形表格(原创)
程序员文章站
2022-06-24 15:32:36
因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到合适的,大部分树形表格都没有拖拽功能,所以决定自己实现一个。这里分享一下实现过程,项目源代码请看github,插件已打包封装好,发布到npm上 本博文会分为两部分,第一部分为使用方式,第二部分为实现方式 安装方式 ......
因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到合适的,大部分树形表格都没有拖拽功能,所以决定自己实现一个。这里分享一下实现过程,项目源代码请看github,插件已打包封装好,发布到上
本博文会分为两部分,第一部分为使用方式,第二部分为实现方式
安装方式
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