基于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 相关文章推荐
优化网页之快速的呈现我们的网页
Jun 29 Javascript
js 判断checkbox是否选中的操作方法
Nov 09 Javascript
javascript之典型高阶函数应用介绍
Jan 10 Javascript
JavaScript访问CSS属性的几种方式介绍
Jul 21 Javascript
jquery获取当前元素索引值用法实例
Jun 10 Javascript
js实现当鼠标移到表格上时显示这一格全部内容的代码
Jun 12 Javascript
浅谈jquery设置和获得checkbox选中的问题
Aug 19 Javascript
JS获取中文拼音首字母并通过拼音首字母快速查找页面内对应中文内容的方法【附demo源码】
Aug 19 Javascript
JavaScript reduce和reduceRight详解
Oct 24 Javascript
详解用webpack的CommonsChunkPlugin提取公共代码的3种方式
Nov 09 Javascript
详解微信小程序实现跑马灯效果(附完整代码)
Apr 29 Javascript
Electron整合React使用搭建开发环境的步骤详解
Jun 07 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使用xmllint命令处理xml与html的方法
2014/12/15 PHP
WordPress开发中用于获取近期文章的PHP函数使用解析
2016/01/05 PHP
PHP中include/require/include_once/require_once使用心得
2016/08/28 PHP
php写一个函数,实现扫描并打印出自定目录下(含子目录)所有jpg文件名
2017/05/26 PHP
CheckBox 如何实现全选?
2006/06/23 Javascript
jqGrid日期格式的判断示例代码(开始日期与结束日期)
2013/11/08 Javascript
原生javascript实现的分页插件pagenav
2014/08/28 Javascript
JS中完美兼容各大浏览器的scrolltop方法
2015/04/17 Javascript
js代码实现点击按钮出现60秒倒计时
2021/01/28 Javascript
浅谈JavaScript的内置对象和浏览器对象
2016/06/03 Javascript
js实现可输入可选择的select下拉框
2016/12/21 Javascript
JS实现的样式切换功能tableCSS实例
2016/12/30 Javascript
微信小程序实现登录页云层漂浮的动画效果
2017/05/05 Javascript
Js参数RSA加密传输之jsencrypt.js的使用
2020/02/07 Javascript
加速vue组件渲染之性能优化
2020/04/09 Javascript
使用JavaScript获取Django模板指定键值数据
2020/05/27 Javascript
约瑟夫问题的Python和C++求解方法
2015/08/20 Python
python 实现查询Neo4j多节点的多层关系
2019/12/23 Python
Python中文分词库jieba,pkusegwg性能准确度比较
2020/02/11 Python
Python模块相关知识点小结
2020/03/09 Python
Python实现定时监测网站运行状态的示例代码
2020/09/30 Python
Python Unittest原理及基本使用方法
2020/11/06 Python
CSS3制作3D立方体loading特效
2020/11/09 HTML / CSS
Nisbets爱尔兰:英国最大的厨房和餐饮设备供应商
2019/01/26 全球购物
澳大利亚最大的护发和护肤品购物网站:RY
2019/12/26 全球购物
abstract class和interface有什么区别
2013/08/04 面试题
校三好学生主要事迹
2014/01/11 职场文书
学校做一个有道德的人活动方案
2014/08/23 职场文书
班级活动总结格式
2014/08/30 职场文书
学校施工安全责任书
2015/01/29 职场文书
2015年上半年党建工作总结
2015/03/30 职场文书
2015年班长个人工作总结
2015/04/03 职场文书
2015年人力资源部工作总结
2015/04/30 职场文书
千与千寻观后感
2015/06/04 职场文书
golang interface判断为空nil的实现代码
2021/04/24 Golang
万能密码的SQL注入漏洞其PHP环境搭建及防御手段
2021/09/04 SQL Server