1. 程式人生 > >基於Vue實現可以拖拽的樹形表格(原創)

基於Vue實現可以拖拽的樹形表格(原創)

  因業務需求,需要一個樹形表格,並且支援拖拽排序,任意未知插入,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>&nbsp;
 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