vue-org-tree 組織結構圖組件應用及源碼分析

需求:

最近業務需要做類似思維導圖的組織結構樹功能,需要能動態增刪改節點,且每層的節點樣式並不相同

可用插件:

網上能找到的組織結構圖插件有:

1.orgchart.js 地址:https://github.com/dabeng/OrgChart.js

這個縮進方式太另類了,個人不喜歡;

2.vue-orgchart 地址:https://github.com/spiritree/vue-orgchart

這個是對第一個的vue改造版,同樣縮進另類且樣式不美觀

3.vue-org-tree 地址:https://github.com/hukaibaihu/vue-org-tree

這是一個純vue版本,我喜歡它的縮進方式和樣式,同時它也是github上星星最多的。

經過對比,我選擇了vue-org-tree,但它只是相當於提供了模板,要實現自己的功能需要對插件進行改造。

 

vue-org-tree安裝和使用:

npm:

# use npm
npm i vue2-org-tree --save

# use yarn
yarn add vue2-org-tree

main.js

import Vue from 'vue'
import Vue2OrgTree from 'vue2-org-tree'

Vue.use(Vue2OrgTree)

組件應用案例看這個:https://github.com/hukaibaihu/vue-org-tree/blob/gh-pages/index.html

可以直接複製運行。api也直接看github官網

可以看出來此組件官方只給了展示數據功能,增刪改功能並沒有明確給出,這就需要我們自己對組件進行改造。

因爲是vue,所以對節點的增刪改離不開對數據的增刪改,側重點是對於數據的處理纔對。

 

vue-org-tree源碼分析

我這裏簡單分析一下源碼和改造

安裝好vue-org-tree後,找到node_modules->vue2-org-tree文件夾

其中org-tree.vue爲組件入口,node.js使用渲染函數動態渲染節點組件

關於渲染函數,看這個:https://cn.vuejs.org/v2/guide/render-function.html

org-tree.vue:

<template>
  <div class="org-tree-container">
    <div class="org-tree" :class="{horizontal, collapsable}">
      <org-tree-node
        :data="data"
        :props="props"
        :horizontal="horizontal"
        :label-width="labelWidth"
        :collapsable="collapsable"
        :render-content="renderContent"
        :label-class-name="labelClassName"
		:selectedKey="selectedKey"
		:selectedClassName="selectedClassName"
        @on-expand="(e, data) => {$emit('on-expand', e, data)}"
        @on-node-click="(e, data) => {$emit('on-node-click', e, data)}"
      />
    </div>
  </div>
</template>

<script>
import render from './node'

export default {
  name: 'Vue2OrgTree',
  components: {
    OrgTreeNode: {
      render,
      functional: true
    }
  },
  props: {
    data: {
      type: Object,
      required: true
    },
    props: {
      type: Object,
      default: () => ({
        label: 'nodeName',
        expand: 'expand',
        children: 'children',
		dataCnt:'dataCnt',
		labelType:'nodeType',
		selectedKey:'nodeId'
      })
    },
    horizontal: Boolean,
    selectedKey: [String,Number],
    collapsable: Boolean,
    renderContent: Function,
    labelWidth: [String, Number],
    labelClassName: [Function, String],
    selectedClassName: [Function, String]
  }
}
</script>

<style lang="less">
@import '../../styles/org-tree';
</style>

此vue的功能主要是接收數據和參數用的,org-tree-node是用來生成結構圖節點的。重點在於這裏:

一般來說組件都會引入一個vue

這裏直接引入了一個render,一看就是個函數式渲染,一般的渲染函數爲:

render: function (createElement) {
  return createElement('h1', "這是生成的內容")
}

createElement爲vue中的生成dom的方法,在vue中一般用”h”代替。

如有時初始化vue時這麼寫:

new Vue({
  render: h => h(App),
}).$mount('#app')

相當於:

render:function(createElement){
    return createElment(App)
}

直接生成組件元素,省去了設置components

類似於js原生中的createElement方法,不過原生中傳的參數爲節點名,如:

var btn=document.createElement("BUTTON");

而vue中的createElement中的參數 先看教程:

https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0

第一個參數一般爲html標籤名或者組件(如上面例子直接傳一個組件都能渲染出來),第二個爲配置元素屬性,第三個爲配置子元素(可傳字符串或者vnode數組)

vnode就是通過createElement創建出來的虛擬節點。

(太繞了,一定要好好看文檔掌握基礎知識)

但這種渲染函數沒有狀態管理和事件監聽,所以vue還可以在render下面加一個functional:true,把它變成“函數式組件”

教程:https://cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6

render: function (createElement, context) {
    // ...
},
functional:ture

那麼回到org-tree.vue中,node.js中返回的一定是類似 function(h,context){}的函數。

node.js:

// 判斷是否葉子節點
const isLeaf = (data, prop) => {
  return !(Array.isArray(data[prop]) && data[prop].length > 0)
}

// 創建 node 節點
export const renderNode = (h, data, context) => {
  const { props } = context
  const cls = ['org-tree-node']
  const childNodes = []
  const children = data[props.props.children]

  if (isLeaf(data, props.props.children)) {
    cls.push('is-leaf')
  } else if (props.collapsable && !data[props.props.expand]) {
    cls.push('collapsed')
  }

  childNodes.push(renderLabel(h, data, context))

  if (!props.collapsable || data[props.props.expand]) {
    childNodes.push(renderChildren(h, children, context))
  }

  return h('div', {
    domProps: {
      className: cls.join(' ')
    }
  }, childNodes)
}

// 創建展開摺疊按鈕
export const renderBtn = (h, data, { props, listeners }) => {
  const expandHandler = listeners['on-expand']

  let cls = ['org-tree-node-btn']

  if (data[props.props.expand]) {
    cls.push('expanded')
  }

  return h('span', {
    domProps: {
      className: cls.join(' ')
    },
    on: {
      click: e => expandHandler && expandHandler(e,data)
    }
  })
}

// 創建 label 節點
export const renderLabel = (h, data, context) => {
  const { props, listeners } = context
  const label = data[props.props.label]
	const labelType=data[props.props.labelType]
	const dataCnt=data[props.props.dataCnt]
	//console.log(label)
  const renderContent = props.renderContent
  const clickHandler = listeners['on-node-click']

  const childNodes = []
	if(labelType=="tag" || labelType=="root"){
		if (typeof renderContent === 'function') {
		  let vnode = renderContent(h, data)
		
		  vnode && childNodes.push(vnode)
		} else {
		  childNodes.push(label)
		}
	}else if(labelType=="domain"){
		childNodes.push(label)
		childNodes.push(h('br', {
    }))
		childNodes.push(dataCnt)
	}
  

  if (props.collapsable && !isLeaf(data, props.props.children)) {
    childNodes.push(renderBtn(h, data, context))
  }

  const cls = ['org-tree-node-label-inner']
  let { labelWidth, labelClassName, selectedClassName, selectedKey } = props
	
	if(labelType == "root"){
		cls.push("bg-blue")
	}else if(labelType == "tag"){
		cls.push("bg-orange")
	}else if(labelType == "domain"){
		cls.push("bg-gray")
	}

  if (typeof labelWidth === 'number') {
    labelWidth += 'px'
  }

  if (typeof labelClassName === 'function') {
    labelClassName = labelClassName(data)
  }

  labelClassName && cls.push(labelClassName)

  // add selected class and key from props
  if (typeof selectedClassName === 'function') {
    selectedClassName = selectedClassName(data)
  }
  //給選中節點加class
  if(selectedKey == data[props.props.selectedKey]){
	  cls.push(selectedClassName)
  }
  
 /* console.log(selectedKey)
  console.log(selectedClassName)
  selectedClassName && selectedKey && data[selectedKey] && cls.push(selectedClassName) */

  return h('div', {
    domProps: {
      className: 'org-tree-node-label'
    }
  }, [h('div', {
    domProps: {
      className: cls.join(' ')
    },
    style: { width: labelWidth },
    on: {
      click: e => clickHandler && clickHandler(e, data)
    }
  }, childNodes)])
}

// 創建 node 子節點
export const renderChildren = (h, list, context) => {
  if (Array.isArray(list) && list.length) {
    const children = list.map(item => {
      return renderNode(h, item, context)
    })

    return h('div', {
      domProps: {
        className: 'org-tree-node-children'
      }
    }, children)
  }
  return ''
}

export const render = (h, context) => {
  const {props} = context
  return renderNode(h, props.data, context)
}

export default render

可見,確實返回的是render函數。

接下來的步驟就是renderNode-》renderLabel-》renderChildren-》renderBtn

理解了createElement方法,這些步驟都很好看懂。在裏面改自己的業務需求就行。

需要注意的是在renderLabel中節點和展開按鈕爲同一個label,一般點擊節點時肯定不想讓點擊按鈕也觸發,則這麼處理:

onNodeClick: function(e, data) {
		//看是節點還是按鈕
		var target=e.target;
		var classList=[...e.target.classList];
		var label=classList.find(item=> item=="org-tree-node-label-inner")
		if(label){
			this.$store.commit("setCurrentNode",data)
		}else{
			//alert(111)
		}
},

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章