JavaScript

超轻量级php框架startmvc

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

更新时间:2020-08-02 01:12:01 作者:startmvc
因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到

因业务需求,需要一个树形表格,并且支持拖拽排序,任意未知插入,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实现可以拖拽的树形表格实例详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

vue 树形表格 vue 拖拽的树形表格