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


Posted in Javascript onOctober 18, 2018

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

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

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

下面是所有实现代码

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

Javascript 相关文章推荐
javascript 面向对象编程 聊聊对象的事
Sep 17 Javascript
js 操作符实例代码
Oct 24 Javascript
JQuery之拖拽插件实现代码
Apr 14 Javascript
JS获取鼠标坐标的实例方法
Jul 18 Javascript
让angularjs支持浏览器自动填表
Nov 10 Javascript
jquery html5 视频播放控制代码
Nov 06 Javascript
vue绑定设置属性的多种方式(5)
Aug 16 Javascript
js实现轮播图的两种方式(构造函数、面向对象)
Sep 30 Javascript
详解关于Vue2.0路由开启keep-alive时需要注意的地方
Sep 18 Javascript
vue实现element表格里表头信息提示功能(推荐)
Nov 20 Javascript
如何在微信小程序中使用骨架屏的步骤
Jun 12 Javascript
在vue中封装的弹窗组件使用队列模式实现方法
Jul 23 Javascript
JavaScript的词法结构精华篇
Oct 17 #Javascript
Javascript中parseInt的正确使用方式
Oct 17 #Javascript
教你如何编写Vue.js的单元测试的方法
Oct 17 #Javascript
详解vue如何使用rules对表单字段进行校验
Oct 17 #Javascript
Vue绑定内联样式问题
Oct 17 #Javascript
react 应用多入口配置及实践总结
Oct 17 #Javascript
vue+echarts实现动态绘制图表及异步加载数据的方法
Oct 17 #Javascript
You might like
PHP 数组排序方法总结 推荐收藏
2010/06/30 PHP
php实现汉字验证码和算式验证码的方法
2015/03/07 PHP
PHP读取大文件末尾N行的高效方法推荐
2016/06/03 PHP
Laravel5.5 数据库迁移:创建表与修改表示例
2019/10/23 PHP
JS中字符问题(二进制/十进制/十六进制及ASCII码之间的转换)
2008/11/03 Javascript
Mootools 1.2教程 同时进行多个形变动画
2009/09/15 Javascript
自定义一个jquery插件[鼠标悬浮时候 出现说明label]
2011/06/27 Javascript
使用JavaScript动态设置样式实现代码及演示动画
2013/01/25 Javascript
jQuery实现单行文字间歇向上滚动源代码
2013/06/02 Javascript
判断ie的两种简单方法
2013/08/12 Javascript
JavaScript中使用Math.floor()方法对数字取整
2015/06/15 Javascript
Jquery+Ajax+PHP+MySQL实现分类列表管理(上)
2015/10/28 Javascript
浅谈JS中json数据的处理
2016/06/30 Javascript
AngularJS中filter的使用实例详解
2017/08/25 Javascript
javascript按顺序加载运行js方法
2017/12/01 Javascript
vue项目中引入vue-datepicker插件的详解
2019/05/14 Javascript
微信小程序-可移动菜单的实现过程详解
2019/06/24 Javascript
python中from module import * 的一个坑
2014/07/20 Python
使用Protocol Buffers的C语言拓展提速Python程序的示例
2015/04/16 Python
python select.select模块通信全过程解析
2017/09/20 Python
python+django加载静态网页模板解析
2017/12/12 Python
教你用Python创建微信聊天机器人
2020/03/31 Python
Python实现的基于优先等级分配糖果问题算法示例
2018/04/25 Python
python对一个数向上取整的实例方法
2020/06/18 Python
美国工业用品采购网站:Zoro.com
2020/10/27 全球购物
计算机大学生的自我评价
2013/10/15 职场文书
应聘面试自我评价
2014/01/24 职场文书
汽车维修求职信
2014/06/15 职场文书
2014小学语文教学工作总结
2014/12/17 职场文书
保密法制宣传月活动总结
2015/05/07 职场文书
2015年网络舆情工作总结
2015/07/24 职场文书
如何用JS实现网页瀑布流布局
2021/04/24 Javascript
tensorflow中的梯度求解及梯度裁剪操作
2021/05/26 Python
使用Python的开发框架Brownie部署以太坊智能合约
2021/05/28 Python
基于HTML十秒做出淘宝页面
2021/10/24 HTML / CSS
2007年老电脑安装win11会怎么样? 网友实测win11在老电脑运行良好
2021/11/21 数码科技