基於Vue實現可以拖拽的樹形表格(原創)
阿新 • • 發佈:2018-12-15
因業務需求,需要一個樹形表格,並且支援拖拽排序,任意未知插入,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是入口元件,定義整體結構
row是遞迴元件(核心元件)
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>View Code
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 }View Code
clolmn和space比較簡單,這裡就不過多闡述
上面就是整個實現過程,元件在chrome上執行穩定,因為用H5的dragable,所以相容會有點問題,後續會修改拖拽的實現方式,手動實現拖拽
開源不易,如果本文對你有所幫助,請給我個star