首页 > 编程 > JavaScript > 正文

基于Vue实现可以拖拽的树形表格实例详解

2019-11-19 12:41:05
字体:
来源:转载
供稿:网友

因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,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触发时调用,该方法会重新生成一个新的排完序的数据,然后返回父组件

下面是所有实现代码

<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支持传入函数,这样可以实现自定义展示,渲染数据时需要判断是否有子节点,有的画递归调用本身,并传入子节点数据

结构如下

<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实现可以拖拽的树形表格实例详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对武林网网站的支持!

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表