最近业务需要做类似思维导图的组织结构树功能,需要能动态增删改节点,且每层的节点样式并不相同
网上能找到的组织结构图插件有:
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,但它只是相当于提供了模板,要实现自己的功能需要对插件进行改造。
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后,找到node_modules->vue2-org-tree文件夹
其中org-tree.vue为组件入口,node.js使用渲染函数动态渲染节点组件
关于渲染函数,看这个:https://cn.vuejs.org/v2/guide/render-function.html
org-tree.vue:
{$emit('on-expand', e, data)}"
@on-node-click="(e, data) => {$emit('on-node-click', e, data)}"
/>
此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)
}
},