1. 程式人生 > >Vue 樹元件

Vue 樹元件

       因為專案中使用比較多樹形元件的原因,嘗試使用過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的一個模板,在插槽內就可以使用這兩個屬性為所欲為地自定義節點內容啦。