欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于Vue实现可以拖拽的树形表格(原创)

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

  因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到合适的,大部分树形表格都没有拖拽功能,所以决定自己实现一个。这里分享一下实现过程,项目源代码请看github,插件已打包封装好,发布到上 

本博文会分为两部分,第一部分为使用方式,第二部分为实现方式

基于Vue实现可以拖拽的树形表格(原创)

安装方式

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触发时调用,该方法会重新生成一个新的排完序的数据,然后返回父组件

下面是所有实现代码

基于Vue实现可以拖拽的树形表格(原创)
  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支持传入函数,这样可以实现自定义展示,渲染数据时需要判断是否有子节点,有的画递归调用本身,并传入子节点数据

结构如下

基于Vue实现可以拖拽的树形表格(原创)
  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