公司项目需求,要在页面中实现一个公司组织架构图,并实现每个节点可以通过点击事件增加子节点,删除子节点。当时首先想到的是用echart的树图实现,但是echart的树图的节点不能自定义样式,而且点击事件也不是那么方便,于是就放弃了。然后在github上找找轮子,发现两个轻量的组织架构树组件,一个是vue版本,另一个是react版本的。在此记录一下使用步骤。
组织树org-tree
vue版本:vue-org-tree
这是一个纯vue版本,我喜欢它的缩进方式和样式,同时它也是github上星星最多的。它只是相当于提供了模板,要实现自己的功能需要对插件进行改造。github项目地址
安装
npm install --save-dev vue2-org-tree
// 只引入vue2-org-tree运行会报错少包
npm install --save-dev less less-loader
注意,demo中有两个bug,节点无法展开问题:
// 底下这个函数一定要写两个参数,如果只写onExpand(data),data.expand结果是undefined
// 这个应该是插件的bug,一般只写一个参数也是可以
onExpand(e,data) {
if ("expand" in data) {
data.expand = !data.expand;
if (!data.expand && data.children) {
this.collapse(data.children);
}
} else {
this.$set(data, "expand", true);
}
}
点击node要跳转链接,点击展开收缩按钮也会跳转问题
增加判断语句即可,默认展开收缩按钮对象里的url肯定是空
onNodeClick(e, data) {
//console.log(data.label);
if(data.url==null){
return false
}else{
window.open(data.url)
}
},
效果图
使用
刚开始把玩一番,颇为得意,很轻松的解决了烦人的需求。真是天道酬勤,哈哈哈哈,报应不爽啊,才懒一会儿,就被泼了凉水,因为我发现一个很让人绝望的问题。虽然这个组件支持你把数据传进来,然后渲染出效果。然后点两下开关,横排竖排显示。但是node节点不支持扩展,也就是说node节点只能显示字符串,不能自己自定义样式,这可不能满足变态的需求啊。可以看到节点方法,这个方法的返回值会渲染的节点中显示
renderContent(h, data) {
return data.label;
}
我尝试着在返回值中加入html标签,发现会当做字符串显示,而不会解析成dom。本想着放弃了,于是想着试试返回JSX模板,vue默认是不支持JSX,需要导入插件才能支持,这就是会用到一个 Babel plugin 插件,然后导入以下组件
npm install babel-plugin-syntax-jsx --save-dev
npm install babel-plugin-transform-vue-jsx --save-dev
npm install babel-helper-vue-jsx-merge-props --save-dev
npm install babel-preset-env --save-dev
修改项目根目录下的.babelrc文件,加入以下内容
{
"presets": ["es2015"],
"plugins": ["transform-vue-jsx"]
}
然后就可以这样写node的初始化方法了
renderContent: function(h, data) {
return (
<div>
<li>{data.label}</li>
</div>)
}
组件完整代码
<template>
<div>
<div class="container">
<div class="col-md-10 col-md-offset-1">
<div class="page-header">
<h3>基于Vue的组织架构树组件</h3>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form class="form-horizontal row">
<div class="col-md-4">
<div class="checkbox">
<label>
<input
type="checkbox"
v-model="horizontal"
> Horizontal
</label>
</div>
</div>
<div class="col-md-4">
<div class="checkbox">
<label>
<input
type="checkbox"
v-model="collapsable"
> Collapsable
</label>
</div>
</div>
<div class="col-md-4">
<div class="checkbox">
<label>
<input
type="checkbox"
v-model="expandAll"
@change="expandChange"
> Expand All
</label>
</div>
</div>
<br>
<div class="col-md-6">
<div class="form-group">
<label class="control-label col-md-5">labelClassName:</label>
<div class="col-md-7">
<select
class="form-control"
v-model="labelClassName"
>
<option value="bg-white">bg-white</option>
<option value="bg-orange">bg-orange</option>
<option value="bg-gold">bg-gold</option>
<option value="bg-gray">bg-gray</option>
<option value="bg-lightpink">bg-lightpink</option>
<option value="bg-chocolate">bg-chocolate</option>
<option value="bg-tomato">bg-tomato</option>
</select>
</div>
</div>
</div>
</form>
</div>
</div>
<br>
<div style="margin-left: 150px"><el-button type="primary" @click.native="testBtn">主要按钮</el-button></div>
<div class="text-center" style="text-align: center;">
<vue2-org-tree name="test"
:data="orgTree"
:horizontal="horizontal"
:collapsable="collapsable"
:label-class-name="labelClassName"
:render-content="renderContent"
@on-expand="onExpand"
@on-node-click="onNodeClick"
/>
</div>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
export default {
data() {
return {
orgTree: {
id: 0,
label: "XXX科技有限公司",
children: [
{
id: 2,
label: "产品研发部",
children: [
{
id: 5,
label: "研发-前端"
},
{
id: 6,
label: "研发-后端"
},
{
id: 9,
label: "UI设计"
},
{
id: 10,
label: "产品经理"
}
]
},
{
id: 3,
label: "销售部",
children: [
{
id: 7,
label: "销售一部"
},
{
id: 8,
label: "销售二部"
}
]
},
{
id: 4,
label: "财务部"
},
{
id: 9,
label: "HR人事"
}
]
},
horizontal: false,
collapsable: true,
expandAll: true,
labelClassName: "bg-orange"
};
},
methods: {
testBtn(){
this.orgTree={
id: 0,
label: "XXX科技有限公司",
children: [
{
id: 2,
label: "产品研发部",
children: [
{
id: 5,
label: "研发-前端"
},
{
id: 6,
label: "研发-后端",
children: [
{
id: 13,
label: "Java研发组"
},
{
id: 14,
label: "C++研发组"
}
]
},
{
id: 9,
label: "UI设计"
},
{
id: 10,
label: "产品经理"
}
]
},
{
id: 3,
label: "销售部",
children: [
{
id: 7,
label: "销售一部"
},
{
id: 8,
label: "销售二部"
}
]
},
{
id: 4,
label: "财务部",
children: [
{
id: 10,
label: "财务一部"
},
{
id: 11,
label: "财务二部"
}
]
},
{
id: 9,
label: "HR人事"
}
]
}
},
renderContent: function(h, data) {
return (
<div>
<li>{data.label}</li>
</div>)
},
onExpand: function(e, data) {
if ('expand' in data) {
data.expand = !data.expand;
if (!data.expand && data.children) {
this.collapse(data.children)
}
} else {
this.$set(data, 'expand', true)
}
},
onNodeClick: function(e, data) {
//console.log(data.label);
if(data.url==null){
return false
}else{
window.open(data.url)
}
},
collapse: function(list) {
let _this = this
list.forEach(function(child) {
if (child.expand) {
child.expand = false
}
child.children && _this.collapse(child.children)
})
},
expandChange: function() {
this.toggleExpand(this.data, this.expandAll)
},
toggleExpand: function(data, val) {
let _this = this;
if (Array.isArray(data)) {
data.forEach(function(item){
_this.$set(item, 'expand', val)
if (item.children) {
_this.toggleExpand(item.children, val)
}
})
} else {
this.$set(data, 'expand', val)
if (data.children) {
_this.toggleExpand(data.children, val)
}
}
}
}
};
</script>
<style type="text/css">
.org-tree-node-label {
white-space: nowrap;
}
.bg-white {
background-color: white;
}
.bg-orange {
background-color: orange;
}
.bg-gold {
background-color: gold;
}
.bg-gray {
background-color: gray;
}
.bg-lightpink {
background-color: lightpink;
}
.bg-chocolate {
background-color: chocolate;
}
.bg-tomato {
background-color: tomato;
}
</style>
react版本:react-org-tree
这是一个纯react版本,同时也是vue-org-tree的react版本,但似乎与vue-org-tree不是一个作者,vue-org-tree的原作者开始并没有提供react版本。react-org-tree完全还原了vue-org-tree的全部功能和样式,为需要使用react版本的的组织树提供了便利。react-org-tree项目地址,可以关注下
安装
修复了vue版本的少包问题,只要导入react-org-tree即可
npm install react-org-tree
效果图
横向展示图
纵向展示图
组件完整代码
import React, {Component} from "react";
import {PageHeaderWrapper} from "@ant-design/pro-layout";
import { Collapse,Button,Rate } from 'antd';
import OrgTree from 'react-org-tree';
const { Panel } = Collapse;
const MyNodeComponent = node => (
<div className="initechNode" key={node.id} onClick={() => nodeClick(node)}>
<li>{node.label}</li>
<Rate allowHalf defaultValue={node.rate} />
</div>
);
function nodeClick(node) {
alert(node.label);
}
function callback(key) {
console.log(key);
}
class MyOrgTree extends Component {
constructor(props){
super(props);
this.state = {
orgTree: {
id: 0,
label: 'XXX股份有限公司',
rate:5,
children: [{
id: 1,
rate:3,
label: '技术部',
children: [{
id: 4,
rate:2.5,
label: '后端工程师'
}, {
id: 5,
rate:2,
label: '前端工程师'
}, {
id: 6,
rate:2.5,
label: '运维工程师'
}]
}, {
id: 2,
rate:3.5,
label: '人事部'
}, {
id: 3,
rate:4,
label: '销售部'
}]
},
horizontal : false,
collapsable : true,
expandAll : true
};
}
testBtn=()=>{
const org = {
id: 0,
label: 'XXX股份有限公司',
rate:5,
children: [{
id: 1,
rate:3,
label: '技术部',
children: [{
id: 4,
rate:2.5,
label: '后端工程师'
}, {
id: 5,
rate:2,
label: '前端工程师'
}, {
id: 6,
rate:2.5,
label: '运维工程师'
}]
}, {
id: 2,
rate:3.5,
label: '人事部',
children:[{
id: 15,
rate:2,
label: '人事一部'
}, {
id: 16,
rate:2.5,
label: '人事二部'
}]
}, {
id: 3,
rate:4,
label: '销售部'
}]
};
this.setState({
orgTree:org
});
this.toggleExpand(org,true);
};
testBtn1=()=>{
this.setState({
horizontal:false
});
};
testBtn2=()=>{
this.setState({
horizontal:true
});
};
toggleExpand=(data, val) => {
// eslint-disable-next-line no-underscore-dangle
const _this = this;
if (Array.isArray(data)) {
data.forEach(function (item) {
item.expand = val;
if (item.children) {
_this.toggleExpand(item.children, val);
}
});
} else {
data.expand = val;
if (data.children) {
_this.toggleExpand(data.children, val);
}
}
this.forceUpdate();
};
render() {
return (
<PageHeaderWrapper>
<div>
<Collapse defaultActiveKey={['1']} onChange={callback}>
<Panel header="This is panel header 1" key="1">
<OrgTree
data={this.state.orgTree}
horizontal={this.state.horizontal}
collapsable={this.state.collapsable}
expandAll={this.state.expandAll}
renderContent={MyNodeComponent}
/>
</Panel>
<Panel header="This is panel header 1" key="2">
<Button style={{marginLeft:'20px'}} type="primary" onClick={this.testBtn}>添加节点</Button>
<Button style={{marginLeft:'20px'}} type="primary" onClick={this.testBtn1}>纵向展示</Button>
<Button style={{marginLeft:'20px'}} type="primary" onClick={this.testBtn2}>横向展示</Button>
</Panel>
</Collapse>
</div>
</PageHeaderWrapper>
);
}
}
export default MyOrgTree;
orgchart
react-orgchart是一个纯的react的组织树插件,网上有个叫orgchart的组件,它是基于jquery的,使用时必须导入jquery。而这里使用的react-orgchart则不需要依赖其他东西,npm官方地址。react-orgchart基本也能实现react-org-tree的效果,唯一不足的是没有横向展示,只有纵向展示。此外节点不支持收缩展开。不过这一点可以自己实现,大致思路如下:react-orgchart渲染出来是使用table来实现的,每个node实际上在tr中,而子节点所在的tr和父节点的tr是兄弟元素。因此在节点上添加点击事件,获取当前dom所在的tr,然后把这个tr的所有兄弟tr隐藏即可。下面介绍一下简单使用
安装
npm install react-orgchart
效果图
使用
首先引入组件和样式
import OrgChart from 'react-orgchart';
import 'react-orgchart/index.css';
刚开始使用的时候呢,发现它没有vue版本的好看,不过随后我就发现它的每个节点也是可以自定义样式的,每个节点也是一个组件,因此可以轻松的改写样式,节点上写点击事情也非常容易,对react兼容的非常好。不像vue版本那样需要额外引入支持JSX的插件,节点组件代码如下
const MyNodeComponent = ({node}) => {
return (
<div className="initechNode" onClick={() => alert("Hi my real name is: " + node.actor)}>{ node.name }</div>
);
};
组件完整代码
import React, {Component} from "react";
import {PageHeaderWrapper} from "@ant-design/pro-layout";
import { Collapse,Button } from 'antd';
import OrgChart from 'react-orgchart';
import 'react-orgchart/index.css';
const { Panel } = Collapse;
const MyNodeComponent = ({node}) => (
<div className="initechNode" style={{border:'1px dashed #000',width:'100px',margin:'0 auto'}} onClick={() => alert(node.actor)}>
{ node.name }
</div>
);
function callback(key) {
console.log(key);
}
class MyOrgTree extends Component {
constructor(props){
super(props);
this.state = {
orgTree:{
name: "root",
actor: "root",
children: [
{
name: "Peter Gibbons",
actor: "Ron Livingston",
children: [
{
name: "And More!!",
actor: "Gary Cole1"
},
{
name: "And More!!",
actor: "Gary Cole1"
}
]
},
{
name: "test",
actor: "test"
},
{
name: "Bob Slydell",
actor: "John C. McGi..."
},
]
}
};
}
testBtn=()=>{
this.setState({
orgTree:{
name: "root",
actor: "root",
children: [
{
name: "Peter Gibbons",
actor: "Ron Livingston",
children: [
{
name: "And More!!",
actor: "Gary Cole1"
},
{
name: "And More!!",
actor: "Gary Cole1"
}
]
},
{
name: "test",
actor: "Stephen Root",
children: [
{
name: "test1",
actor: "test1",
children: [
{
name: "test3",
actor: "test3"
},
{
name: "test4",
actor: "test4"
}
]
},
{
name: "test2",
actor: "test2"
}
]
},
{
name: "Bob Slydell",
actor: "John C. McGi..."
},
]
}
})
};
render() {
return (
<PageHeaderWrapper>
<div>
<Collapse defaultActiveKey={['1']} onChange={callback}>
<Panel header="This is panel header 1" key="1">
<OrgChart tree={this.state.orgTree} NodeComponent={MyNodeComponent} />
</Panel>
<Panel header="This is panel header 1" key="2">
<Button type="primary" onClick={this.testBtn}>Primary</Button>
</Panel>
</Collapse>
</div>
</PageHeaderWrapper>
);
}
}
export default MyOrgTree;