基于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 相关文章推荐
一个用js实现控制台控件的代码
Sep 04 Javascript
2012年开发人员的16款新鲜的jquery插件体验分享
Dec 28 Javascript
extjs 分页使用jsp传递数据示例
Jul 29 Javascript
基于jQuery实现的无刷新表格分页实例
Feb 17 Javascript
基于JavaScript实现类似于百度学术高级检索功能
Mar 02 Javascript
js滑动提示效果代码分享
Mar 10 Javascript
微信小程序中子页面向父页面传值实例详解
Mar 20 Javascript
详解Vue+axios+Node+express实现文件上传(用户头像上传)
Aug 10 Javascript
浅谈webpack性能榨汁机(打包速度优化)
Jan 09 Javascript
vue 路由守卫(导航守卫)及其具体使用
Feb 25 Javascript
angular中的post请求处理示例详解
Jun 30 Javascript
详解前端任务构建利器Gulp.js使用指南
Apr 30 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
在“咖啡之国”感受咖啡文化
2021/03/03 咖啡文化
Yii使用CLinkPager分页实例详解
2014/07/23 PHP
php实现每日签到功能
2018/11/29 PHP
Aliyun Linux 编译安装 php7.3 tengine2.3.2 mysql8.0 redis5的过程详解
2020/10/20 PHP
JQuery中$(document)是什么意思有什么作用
2014/07/21 Javascript
javascript实现base64 md5 sha1 密码加密
2015/09/09 Javascript
js 判断各种数据类型的简单方法(推荐)
2016/08/29 Javascript
jQuery检查元素存在性(推荐)
2016/09/17 Javascript
微信小程序 教程之数据绑定
2016/10/18 Javascript
jQuery实现动态添加tr到table的方法
2016/12/26 Javascript
jQuery插件zTree实现的多选树效果示例
2017/03/08 Javascript
微信小程序request出现400的问题解决办法
2017/05/23 Javascript
使用Node.js实现RESTful API的示例
2017/08/01 Javascript
详解Vue.js Mixins 混入使用
2017/09/15 Javascript
限时抢购-倒计时的完整实例(分享)
2017/09/17 Javascript
Node.js学习之地址解析模块URL的使用详解
2017/09/28 Javascript
React Native中TabBarIOS的简单使用方法示例
2017/10/13 Javascript
js中如何完美的解析数据
2018/03/18 Javascript
Python中实现结构相似的函数调用方法
2015/03/10 Python
Python操作MongoDB数据库PyMongo库使用方法
2015/04/27 Python
Python Nose框架编写测试用例方法
2017/10/26 Python
Python利用ORM控制MongoDB(MongoEngine)的步骤全纪录
2018/09/13 Python
numpy 对矩阵中Nan的处理:采用平均值的方法
2018/10/30 Python
对python for 文件指定行读写操作详解
2018/12/29 Python
Python实现图片转字符画的代码实例
2019/02/22 Python
python3安装OCR识别库tesserocr过程图解
2020/04/02 Python
Django模板获取field的verbose_name实例
2020/05/19 Python
python cv2.resize函数high和width注意事项说明
2020/07/05 Python
Keras构建神经网络踩坑(解决model.predict预测值全为0.0的问题)
2020/07/07 Python
会计出纳员的自我评价
2014/01/15 职场文书
关于迟到的检讨书
2014/01/26 职场文书
《将心比心》教学反思
2014/04/08 职场文书
2015年国庆节慰问信
2015/03/23 职场文书
mysql死锁和分库分表问题详解
2021/04/16 MySQL
MySQL 重写查询语句的三种策略
2021/05/10 MySQL
对象析构函数__del__在Python中何时使用
2022/03/22 Python