在线DEMO查看

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

安装方式

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>&nbsp;
</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,所以兼容会有点问题,后续会修改拖拽的实现方式,手动实现拖拽

开源不易,如果本文对你有所帮助,请给我个star

------

作者:木法传

基于Vue实现可以拖拽的树形表格(原创)的更多相关文章

  1. Vue.Draggable实现拖拽效果(采坑小记)

    之前有写过Vue.Draggable实现拖拽效果(快速使用)(http://www.cnblogs.com/songdongdong/p/6928945.html)最近项目中要用到这个拖拽的效果,当产 ...

  2. Nuxt|Vue仿探探/陌陌卡片式滑动|vue仿Tinder拖拽翻牌效果

    探探/Tinder是一个很火的陌生人社交App,趁着国庆假期闲暇时间倒腾了个Nuxt.js项目,项目中有个模块模仿探探滑动切换界面效果.支持左右拖拽滑动like和no like及滑动回弹效果. 一览效 ...

  3. vue自定义事件---拖拽

    margin布局拖拽 Vue.directive('drag', { bind(el, binding, vnode, oldVnode) { const dialogHeaderEl = el.qu ...

  4. 原生js拖拽、jQuery拖拽、vue自定义指令拖拽

    原生js拖拽: <!DOCTYPE html> <html lang="en"> <head> <meta charset="U ...

  5. vue draggable 火狐拖拽搜索问题

    最近在使用vuedraggable做导航时候,谷歌拖拽是没问题的,但是在火狐测试时候,拖拽时候是可以成功,但是火狐还是打开了一个新的tab,并且搜索了,一开始想着是阻止默认行为,但是在@end时间中阻 ...

  6. Vue 表单拖拽排序

    Vue table表单拖拽 业务需求: 因为数据展示使用的是 elementUI 的 Table进行数据展示的,现在的需求是通过拖拽表单进行表单排序.同时,动态修改表单中的数据排列顺序.查阅了好多资料 ...

  7. vue el-transfer新增拖拽排序功能---sortablejs插件

    <template> <!-- target-order="unshift"必须设置,如果不设置的话后台穿的value值得顺序会被data重置 -  --> ...

  8. vue v-dialogDrag: 弹窗拖拽

    Vue.directive('dialogDrag', { inserted:function(el) { const dragDom = el.querySelector('.jsPropupLay ...

  9. Vue.Draggable实现拖拽效果(快速使用)

    1.下载包:npm install vuedraggable 配置:package.json "dependencies": { "element-ui": & ...

随机推荐

  1. 2 模拟登录_Post表单方式(针对chinaunix有效,针对csdn失效,并说明原因)

    参考精通Python网络爬虫实战 首先,针对chinaunix import urllib.request #原书作者提供的测试url url="http://bbs.chinaunix.n ...

  2. 使用 Azure PowerShell 将 IaaS 资源从经典部署模型迁移到 Azure Resource Manager

    以下步骤演示了如何使用 Azure PowerShell 命令将基础结构即服务 (IaaS) 资源从经典部署模型迁移到 Azure Resource Manager 部署模型. 也可根据需要通过 Az ...

  3. zabbix_3.0安装部署与中文支持

    Zabbix 3.0界面焕然一新,一改10多年的老面孔,alpha4的更新具体记录如下:http://www.zabbix.com/rn3.0.0alpha4.php What's New in 3. ...

  4. WINDOW各种命令

  5. 乘风破浪:LeetCode真题_013_Roman to Integer

    乘风破浪:LeetCode真题_013_Roman to Integer 一.前言 上一节我们讨论了如何把阿拉伯数字转换成罗马数字,现在我们需要思考一下如何把罗马数字转换成阿拉伯数字,其实我们仔细观擦 ...

  6. TFS使用笔记

    TFS是用来存储文件的服务器,放置不同版本的文件.因此文件的数量和内容因版本不同而不同. 在Fig-00中,服务器和本地的对应文件夹Common下的因版本不一致,所以文件数量是不一致的. Fig-00 ...

  7. source insight设置问题 [问题点数:20分,结帖人leecapacity]

    http://cache.baiducontent.com/c?m=9f65cb4a8c8507ed4fece763104687270e54f7327d818c027fa3cf1fd5791d1c05 ...

  8. codeforces 549F Yura and Developers(分治、启发式合并)

    codeforces 549F Yura and Developers 题意 给定一个数组,问有多少区间满足:去掉最大值之后,和是k的倍数. 题解 分治,对于一个区间,找出最大值之后,分成两个区间. ...

  9. jquery ui tabs(选项卡)插件

    参考文档:http://www.css88.com/jquery-ui-api/tabs/ html代码: <div id="tabs"> <ul> < ...

  10. Spring JPA 主键

    JPA主键生成器和主键生成策略 JPA中创建实体时,需要声明实体的主键及其主键生成策略.我们有一个实体类叫做Email,其主键上声明如下: @Id @Column(name = "EMAIL ...