经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
基于Vue实现可以拖拽的树形表格
来源:cnblogs  作者:北风吹雪  时间:2018/10/18 8:57:00  对本文有异议

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

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

安装方式

  1. npm i drag-tree-table --save-dev

使用方式

  1. import dragTreeTable from 'drag-tree-table'

 模版写法

  1. <dragTreeTable :data="treeData" :onDrag="onTreeDataChange"></dragTreeTable>

data参数示例

  1. {
  2.   lists: [
  3.   {
  4.     "id":40,
  5.     "parent_id":0,
  6.     "order":0,
  7.     "name":"动物类",
  8.     "open":true,
  9.     "lists":[]
  10.   },{
  11.     "id":5,
  12.     "parent_id":0,
  13.     "order":1,
  14.     "name":"昆虫类",
  15.     "open":true,
  16.     "lists":[
  17.       {
  18.         "id":12,
  19.         "parent_id":5,
  20.         "open":true,
  21.         "order":0,
  22.         "name":"蚂蚁",
  23.         "lists":[]
  24.       }
  25.     ]
  26.   },
  27.   {
  28.     "id":19,
  29.     "parent_id":0,
  30.     "order":2,
  31.     "name":"植物类",
  32.     "open":true,
  33.     "lists":[]
  34.   }
  35.  ],
  36.   columns: [
  37.   {
  38.    type: 'selection',
  39.    title: '名称',
  40.    field: 'name',
  41.    width: 200,
  42.    align: 'center',
  43.    formatter: (item) => {
  44.      return '<a>'+item.name+'</a>'
  45.    }
  46.   },
  47.   {
  48.     title: '操作',
  49.     type: 'action',
  50.     width: 350,
  51.     align: 'center',
  52.     actions: [
  53.       {
  54.         text: '查看角色',
  55.         onclick: this.onDetail,
  56.         formatter: (item) => {
  57.           return '<i>查看角色</i>'
  58.         }
  59.       },
  60.       {
  61.         text: '编辑',
  62.         onclick: this.onEdit,
  63.         formatter: (item) => {
  64.           return '<i>编辑</i>'
  65.         }
  66.       }
  67.     ]
  68.   },
  69.   ]
  70. }

 onDrag在表格拖拽时触发,返回新的list

  1. onTreeDataChange(lists) {
  2.     this.treeData.lists = lists
  3. }

到这里组件的使用方式已经介绍完毕

实现

  • 递归生成树姓结构(非JSX方式实现)

  • 实现拖拽排序(借助H5的dragable属性)

  • 单元格内容自定义展示

组件拆分-共分为四个组件

  dragTreeTable.vue是入口组件,定义整体结构

  row是递归组件(核心组件)

  clolmn单元格,内容承载

  space控制缩进

看一下dragTreeTable的结构

  1. <template>
  2.     <div class="drag-tree-table">
  3.         <div class="drag-tree-table-header">
  4.           <column
  5.             v-for="(item, index) in data.columns"
  6.             :width="item.width"
  7.             :key="index" >
  8.             {{item.title}}
  9.           </column>
  10.         </div>
  11.         <div class="drag-tree-table-body" @dragover="draging" @dragend="drop">
  12.           <row depth="0" :columns="data.columns"
  13.             :model="item" v-for="(item, index) in data.lists" :key="index">
  14.         </row>
  15.         </div>
  16.     </div>
  17. </template>

看起来分原生table很像,dragTreeTable主要定义了tree的框架,并实现拖拽逻辑

filter函数用来匹配当前鼠标悬浮在哪个行内,并分为三部分,上中下,并对当前匹配的行进行高亮

resetTreeData当drop触发时调用,该方法会重新生成一个新的排完序的数据,然后返回父组件

下面是所有实现代码

  1.   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 (> 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 = whereInsert101           }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.1111           hoverBlock.children[1].style.opacity = 0.1112           hoverBlock.children[2].style.opacity = 0.1113         }114       },115       resetTreeData() {116         if (this.targetId === undefined) return 117         const newList = []118         const curList = this.data.lists119         const _this = this120         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_id129                 needPushList.push(curDragItem)130                 needPushList.push(obj)131               } else if (_this.whereInsert === 'center'){132                 curDragItem.parent_id = item.id133                 obj.lists.push(curDragItem)134                 needPushList.push(obj)135               } else {136                 curDragItem.parent_id = item.parent_id137                 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 (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 = null167         var _this = this168         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               break174             } 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.   1 <template>  2         <div class="tree-block" draggable="true" @dragstart="dragstart($event)"  3             @dragend="dragend($event)">  4             <div class="tree-row" 
  2.   5                 @click="toggle" 
  3.   6                 :tree-id="model.id"  7                 :tree-p-id="model.parent_id"> 
  4.   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                         <class="action-item" 26                             v-for="(acItem, acIndex) in subItem.actions" 27                             :key="acIndex" 28                             type="text" size="small" 
  5.  29                             @click.stop.prevent="acItem.onclick(model)"> 30                             <: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                         <class="el-icon-caret-top"></i> 43                     </div> 44                     <div class="hover-block center-block"> 45                         <class="el-icon-caret-right"></i> 46                     </div> 47                     <div class="hover-block next-block"> 48                         <class="el-icon-caret-bottom"></i> 49                     </div> 50                 </div> 51             </div> 52             <row 
  6.  53                 v-show="model.open" 54                 v-for="(item, index) in model.lists" 
  7.  55                 :model="item" 56                 :columns="columns" 57                 :key="index" 
  8.  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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号