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