因为项目中使用比较多树形组件的原因,尝试使用过iview以及element-ui的树组件,两个组件库都非常优秀,但是在用它们的树组件来实现需求时都不甚满意。主要体现在样式的难于控制、以及操作的便捷性上。在阅读element-ui的代码后,结合自己的需求自己写了一个树组件,代码在github上开源,并且发布到了npm上(目前已发布了1.0稳定版本,将会持续维护)。你可以通过npm或者yarn包管理工具安装这个组件到你的项目中使用。本文会叙述这个组件的功能、如何使用这个组件,关于组件源代码请查看github仓库(关于Vue树组件的渲染原理可以查看我前面写的文章)。
一、功能和效果
1. 功能:
- 基本操作:增加、删除、修改操作直接修改数据即可,数据控制视图。
- 节点内容自定义:你可以在一个节点中自定义你想展示的内容,比如常见的在节点中增加操作按钮。
- 节点缩进竖线:当树的同一级节点略多的时候,本组件会用不同颜色的竖线来告诉你哪些节点为同一层级,并且这些竖线的颜色你可以自由控制而不一定非得使用默认颜色。
- 缩进控制:如果你在项目中使用过其它的树组件,比如iview或者element-ui的树组件(iview和element-ui是两个非常优秀而全面的前端组件库),就会发现当树的层数增多时,树的宽度固定,这时最深层的节点已经缩进到视窗的最右边,你不得不设置overflow属性来拖动查看完整的节点数据。在本组件中,你不必为这个问题烦恼了,你可以控制每一层缩进的最大距离maxIndent和所有层的最大缩进距离indentLimit,这意味着,如果你设置indentLimit为40,那最深层的缩进距离就是40% x _树宽度_,而每一层的缩进距离都会根据最大缩进距离和层数动态计算。
- 拖动操作:如果你使用过element-ui的树组件,你会发现拖动操作非常难受,拖动的提示仅是一条细线,拖动为子节点和拖为下一节点的区别不明显,而最难受的是拖为子节点和拖为下一节点的操作你得小心翼翼控制鼠标的上下位置,才能拖动到你想要的位置。在本组件中,拖动到相对哪个节点的哪个相对位置你会得到清晰的反馈,并且操作过程会更加舒服。
2. 先来看看效果(本例的效果即为github仓库中的例子,你可以安装该项目体验):
二、使用该组件
你可以使用包管理工具npm或者yarn安装本组件在你的项目中进行使用,使用方法如下:
1.安装
使用npm:
npm install simple-vue-tree --save
使用yarn:
yarn add simple-vue-tree --save
2.在项目入口文件中引入组件
import 'simple-vue-tree'
import 'simple-vue-tree/dist/lib/simple-tree.css'
3.可以在项目中直接使用
最简单的例子如下(完整的API文档可以查看github仓库的readme文档,这个例子仅仅传入了树组件数据,没有传入任何其它props,因此只会渲染一颗最简单的树结构出来):
<simple-tree
:treeData="treeData">
<simple-tree/>
您通常需要更复杂的功能,比如像上述动图中的操作效果,这时使用可能会稍微复杂一些,上述效果的实现大概如下(例子使用iview组件以及stylus样式语言,你可以在github仓库的src/samples/HelloWorld查看):
<template>
<div class="container">
<simple-tree
class="tree"
:allowDrag="allowDrag"
:allowDrop="allowDrop"
@tree-drop="handleDrop"
@content-click="handleContentClick"
:indentLine="true"
:indentLimit="40"
:treeData="treeData"
draggable>
<div
class="node-content"
slot-scope="{ parentData, data }"
@dblclick="editNode(data)"
:class="data.id === chooseNode ? 'current-node' : ''">
<div class="node-name">{{ data.title }}</div>
<div class="node-divide"></div>
<div class="node-menu-icons">
<Icon
class="node-menu-icon"
@click.stop="addBrother(parentData, data)"
type="md-add-circle"
title="添加同级节点"/>
<Icon
@click.stop="addChild(data)"
class="node-menu-icon"
type="md-add"
title="添加子级节点"/>
<Icon
@click.stop="deleteNode(parentData, data)"
class="node-menu-icon"
type="md-trash"
title="删除节点"/>
</div>
</div>
</simple-tree>
<Modal
title="输入节点名称"
@on-ok="saveNode"
@on-cancle="clearEditingInfo"
v-model="editingInfo.show">
<Input
ref="titleInput"
v-model="editingInfo.title"
@on-enter="saveNode">
</Input>
</Modal>
</div>
</template>
<script>
export default {
data () {
return {
nodeID: 100,
treeData: [{
id: 1,
title: 'node-1',
children: [{
id: 2,
title: 'node-2',
children: [{
id: 3,
title: 'node-3'
},
{
id: 4,
title: 'node-4'
},
{
id: 5,
title: 'node-5'
}]
}]
}],
editingInfo: {
show: false,
title: '',
info: {}
},
chooseNode: 0
}
},
methods: {
allowDrag (data) {
return true
},
allowDrop (dragVNode, dropVNode, position) {
return true
},
handleDrop (dragVNode, dropVNode, dropType) {
let parentData, insertIndex
if (dropType === 'before' || dropType === 'after') {
parentData = dropVNode.parentData
let dropNodeIndex = dropVNode.parentData.children.indexOf(dropVNode.nodeData)
insertIndex = dropType === 'before' ? dropNodeIndex : dropNodeIndex + 1
} else {
parentData = dropVNode.nodeData
if (!parentData.children) {
this.$set(parentData, 'children', [])
}
insertIndex = parentData.children.length
}
let dragNodeIndex = dragVNode.parentData.children.indexOf(dragVNode.nodeData)
dragVNode.parentData.children.splice(dragNodeIndex, 1)
parentData.children.splice(insertIndex, 0, dragVNode.nodeData)
},
handleContentClick (event, vNode) {
this.chooseNode = vNode.nodeData.id
},
addBrother (parentData, data) {
let newNode = {
id: this.nodeID++,
title: ''
}
let index = parentData.children.indexOf(data)
parentData.children.splice(index + 1, 0, newNode)
this.editNode(newNode)
},
addChild (data) {
let newNode = {
id: this.nodeID++,
title: ''
}
if (!data.children) {
this.$set(data, 'children', [])
}
data.children.unshift(newNode)
this.editNode(newNode)
},
deleteNode (parentData, data) {
let index = parentData.children.indexOf(data)
parentData.children.splice(index, 1)
},
editNode (data) {
this.editingInfo.show = true
this.editingInfo.title = data.title
this.editingInfo.info = data
this.$nextTick(() => {
this.$refs.titleInput.focus()
})
},
saveNode (data) {
this.editingInfo.info.title = this.editingInfo.title
this.clearEditingInfo()
},
clearEditingInfo () {
this.editingInfo = {
show: false,
title: '',
info: {}
}
}
}
}
</script>
<style lang="stylus" scoped>
.container
position fixed
top 10%
bottom 10%
left 0
right 0
user-select none
.tree
width 50%
height 100%
box-shadow 0 0 2px 1px #3361D8
border-radius 5px
padding 0.5rem
margin 0 auto
.node-content
display flex
box-shadow 0 0 1px 0 #A1BFFC
align-items center
margin 2px
.node-name
padding 0 2px
word-break break-all
display -webkit-box
-webkit-line-clamp 3
-webkit-box-orient vertical
overflow-y hidden
.node-divide
flex auto
.node-menu-icons
display flex
align-items center
font-size 1rem
opacity 0
.node-menu-icon
cursor pointer
&:active
position relative
left 1px
top 1px
&:hover
background #ECF2FC
.node-menu-icons
opacity 1
&.current-node
background #D0DEF8
</style>
这里需要特别说明一下的是自定义节点内容slot-scope(这是一个强大的功能,了解更多)部分,通过slot-scope作用域插槽来自定义节点内容,相当于接收节点数据nodeData和节点父组件数据parentData的一个模板,在插槽内就可以使用这两个属性为所欲为地自定义节点内容啦。