因为项目中使用比较多树形组件的原因,尝试使用过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仓库中的例子,你可以安装该项目体验):

Vue 树组件

二、使用该组件

你可以使用包管理工具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的一个模板,在插槽内就可以使用这两个属性为所欲为地自定义节点内容啦。

相关文章: