基于Vue實現(xiàn)可以拖拽的樹形表格實例詳解
因業(yè)務(wù)需求,需要一個樹形表格,并且支持拖拽排序,任意未知插入,github搜了下,真不到合適的,大部分樹形表格都沒有拖拽功能,所以決定自己實現(xiàn)一個。這里分享一下實現(xiàn)過程,項目源代碼請看github,插件已打包封裝好,發(fā)布到npm上
本博文會分為兩部分,第一部分為使用方式,第二部分為實現(xiàn)方式

安裝方式
npm i drag-tree-table --save-dev
使用方式
import dragTreeTable from 'drag-tree-table'
模版寫法
<dragTreeTable :data="treeData" :onDrag="onTreeDataChange"></dragTreeTable>
data參數(shù)示例
{
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在表格拖拽時觸發(fā),返回新的list
onTreeDataChange(lists) {
this.treeData.lists = lists
}
到這里組件的使用方式已經(jīng)介紹完畢
實現(xiàn)
•遞歸生成樹姓結(jié)構(gòu)(非JSX方式實現(xiàn))
•實現(xiàn)拖拽排序(借助H5的dragable屬性)
•單元格內(nèi)容自定義展示
組件拆分-共分為四個組件
dragTreeTable.vue是入口組件,定義整體結(jié)構(gòu)
row是遞歸組件(核心組件)
clolmn單元格,內(nèi)容承載
space控制縮進
看一下dragTreeTable的結(jié)構(gòu)
<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的框架,并實現(xiàn)拖拽邏輯
filter函數(shù)用來匹配當(dāng)前鼠標(biāo)懸浮在哪個行內(nèi),并分為三部分,上中下,并對當(dāng)前匹配的行進行高亮
resetTreeData當(dāng)drop觸發(fā)時調(diào)用,該方法會重新生成一個新的排完序的數(shù)據(jù),然后返回父組件
下面是所有實現(xiàn)代碼
<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支持傳入函數(shù),這樣可以實現(xiàn)自定義展示,渲染數(shù)據(jù)時需要判斷是否有子節(jié)點,有的畫遞歸調(diào)用本身,并傳入子節(jié)點數(shù)據(jù)
結(jié)構(gòu)如下
<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比較簡單,這里就不過多闡述
上面就是整個實現(xiàn)過程,組件在chrome上運行穩(wěn)定,因為用H5的dragable,所以兼容會有點問題,后續(xù)會修改拖拽的實現(xiàn)方式,手動實現(xiàn)拖拽
總結(jié)
以上所述是小編給大家介紹的基于Vue實現(xiàn)可以拖拽的樹形表格實例詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
vue監(jiān)聽input標(biāo)簽的value值方法
今天小編就為大家分享一篇vue監(jiān)聽input標(biāo)簽的value值方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08
Vue結(jié)合高德地圖實現(xiàn)HTML寫自定義信息彈窗全過程
最近開發(fā)中遇到一個多個點繪制,并實現(xiàn)點擊事件,出現(xiàn)自定義窗口顯示相關(guān)信息等功能,下面這篇文章主要給大家介紹了關(guān)于Vue結(jié)合高德地圖實現(xiàn)HTML寫自定義信息彈窗的相關(guān)資料,需要的朋友可以參考下2023-04-04
vue+element-ui+sortable.js實現(xiàn)表格拖拽功能
這篇文章主要為大家詳細(xì)介紹了vue+element-ui+sortable.js實現(xiàn)表格拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04

