基于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 相关文章推荐
基于jquery实现漂亮的动态信息提示效果
Aug 02 Javascript
js精度溢出解决方案
Dec 02 Javascript
javascript ajax 仿百度分页函数
Oct 29 Javascript
浅谈JavaScript Date日期和时间对象
Dec 29 Javascript
JS实现样式清新的横排下拉菜单效果
Oct 09 Javascript
三分钟带你玩转jQuery.noConflict()
Feb 15 Javascript
深入理解jQuery()方法的构建原理
Dec 05 Javascript
javascript cookie的基本操作(添加和删除)
Jul 24 Javascript
基于js中的原型(全面讲解)
Sep 19 Javascript
解决vue路由后界面没有变化,但是链接有的问题
Sep 01 Javascript
Angular resolve基础用法详解
Oct 03 Javascript
vue 使用vue-i18n做全局中英文切换的方法
Oct 29 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运算符的知识大全
2011/11/03 PHP
PHP性能优化准备篇图解PEAR安装
2011/12/05 PHP
九个你必须知道而且又很好用的php函数和特点
2013/08/08 PHP
析构函数与php的垃圾回收机制详解
2013/10/28 PHP
PHP模拟asp.net的StringBuilder类实现方法
2015/08/08 PHP
通过源码解析Laravel的依赖注入
2018/01/22 PHP
学习YUI.Ext 第二天
2007/03/10 Javascript
JQuery CSS样式控制 学习笔记
2009/07/23 Javascript
jQuery 实现侧边浮动导航菜单效果
2014/12/26 Javascript
javascript实现可拖动变色并关闭层窗口实例
2015/05/15 Javascript
javascript常用经典算法实例详解
2015/11/25 Javascript
js验证真实姓名与身份证号,手机号的简单实例
2016/07/18 Javascript
详解vue 模拟后台数据(加载本地json文件)调试
2017/08/25 Javascript
JavaScript常见事件对象与操作实例总结
2019/01/05 Javascript
前后端常见的几种鉴权方式(小结)
2019/08/04 Javascript
vue 实现根据data中的属性值来设置不同的样式
2020/08/04 Javascript
nodejs+express最简易的连接数据库的方法
2020/12/23 NodeJs
[01:01:52]DOTA2-DPC中国联赛定级赛 SAG vs iG BO3第二场 1月9日
2021/03/11 DOTA
跟老齐学Python之永远强大的函数
2014/09/14 Python
Python的Django框架中的URL配置与松耦合
2015/07/15 Python
详解Python的Django框架中的模版继承
2015/07/16 Python
Django 2.0版本的新特性抢先看!
2018/01/05 Python
python 判断linux进程,并杀死进程的实现方法
2019/07/01 Python
python实现银行账户系统
2021/02/22 Python
html5启动原生APP总结
2020/07/03 HTML / CSS
欧缇丽英国官方网站:Caudalie英国
2016/08/17 全球购物
英国儿童设计师服装的领先零售商:Base
2019/03/17 全球购物
Linux如何命名文件--使用文件名时应注意
2012/01/22 面试题
普通院校学生的自荐信
2013/11/27 职场文书
环保倡议书100字
2014/05/15 职场文书
2014年个人债务授权委托书范本
2014/09/22 职场文书
就业协议书范本
2014/10/08 职场文书
怎样做好公众演讲能力?
2019/08/28 职场文书
django学习之ajax post传参的2种格式实例
2021/05/14 Python
无线电通信名词解释
2022/02/18 无线电
Golang ort 中的sortInts 方法
2022/04/24 Golang