React 组件封装之 Tree 树形控件
一、Tree 树形结构
组件说明:
实现树形控件,适用于组织架构、文章列表等链表结构的功能。
效果展示:
-
默认样式
2.自定义样式
3.用于组织架构的自定义样式
二、使用案例
- 默认样式案例
index.js
import React from 'react';
import Tree from './Tree';
export default class MyTree extends React.Component{
constructor(props){
super(props);
this.state = {
treeData:[{
title: 'parent 1',
key: '0-0',
children: [
{
title: 'parent 1-0',
key: '0-0-0',
children: [
{
title: 'leaf',
key: '0-0-0-0',
},
{
title: 'leaf',
key: '0-0-0-1',
},
{
title: 'leaf',
key: '0-0-0-2',
},
],
},
{
title: 'parent 1-1',
key: '0-0-1',
children: [
{
title: 'leaf',
key: '0-0-1-0',
},
],
},
{
title: 'parent 1-2',
key: '0-0-2',
children: [
{
title: 'leaf',
key: '0-0-2-0',
},
{
title: 'leaf',
key: '0-0-2-1',
},
],
},
],
},
]
}
}
onSelect(){
}
render(){
const {
treeData} = this.state;
return (<Tree
treeData={
treeData}
onSelect={
()=>this.onSelect()}
defaultExpandedKeys={
['0-0-0']}
/>
)
}
}
- 自定义样式
import React from 'react';
import {
Avatar} from 'antd'
import Tree from './Tree';
import defaultAvatar from '../../assets/home/default-avatar.png';
import redBadge from '../../assets/detail/red-badge.png';
import grayBadge from '../../assets/detail/grar-badge.png';
import blueBadge from '../../assets/detail/blue-badge.png';
const {
TreeNode } = Tree;
function loop(arr,keys) {
return arr.map((item)=>{
return <TreeNode title={
item.Job_Grade?
<div className="cm-flex cm-ai-c header-language-hover">
<div className="cm-position-relative">
<Avatar src={
item.avatar?item.avatar:defaultAvatar} size={
50}/>
<img src={
changeBadge(item.badge)} alt="" className="detail-badge"/>
</div>
<div className="cm-ml-01 cm-mtb-01 cm-flex-1">
<span className="cm-text-ellipsis cm-c-333 cm-fw-bold">{
item[keys.name]}({
item.English_Name})</span>
<div className="cm-c-999 cm-text-nowrap cm-fs-012 cm-lh-initial">{
item.PM_Job_Classification}{
item.Job_Grade}</div>
<div className="cm-c-999 cm-text-nowrap cm-fs-012 cm-lh-initial">{
item.company}</div>
</div>
</div>: <span className="cm-text-ellipsis cm-cursor-not">{
item[keys.name]}</span>
} key={
item[keys.id]} flag={
item.Job_Grade}>
{
item.children&&item.children.length>0?
loop.call(this,item.children,keys):null
}
</TreeNode>
})
}
function changeBadge(badge) {
if(badge === "左上"||badge === "左中"||badge === "左下"){
return redBadge;
}else if(badge === "中上"||badge === "中中"||badge === "中下"){
return blueBadge;
}else {
return grayBadge;
}
}
const generate = (arr,topId,keys,data=[])=>{
for(let i = 0;i<arr.length;i++){
let item = arr[i];
if(item[keys.parentId]===topId){
data.push(item);
for(let j = i+1;j<arr.length;j++){
let item1 = arr[j];
if(item[keys.id] === item1[keys.parentId]){
item.children = [];
generate(arr,item1[keys.parentId],keys,item.children);
}
}
}
}
return data;
}
class MyTree extends React.Component{
constructor(props){
super(props);
this.state = {
dataSource:[
{
deptId:10,deptName:"XX集团",parentDeptId:1},
{
deptId:11,deptName:"XXXX有限公司",parentDeptId:1},
{
deptId:2,deptName:"开发部",parentDeptId:10},
{
deptId:4,deptName:"销售部",parentDeptId:11},
{
deptId:5,deptName:"招聘部",parentDeptId:11},
{
deptId:14,deptName:"小红",parentDeptId:4,
badge:"左上",English_Name:"zhu dan",
Job_Grade:'P-10',PM_Job_Classification:"部门经理",company:'XXXX有限公司',
},
{
deptId:15,deptName:"张三",parentDeptId:14,Job_Grade:'P-11',PM_Job_Classification:"部门经理",
company:'XXXX有限公司',
English_Name:"zhang san",
badge:"中上",
},
{
deptId:16,deptName:"李四",parentDeptId:15,Job_Grade:'P-11',PM_Job_Classification:"部门经理",
badge:"右上",English_Name:"li si",
company:'XXXX有限公司'},
{
deptId:17,deptName:"王五",parentDeptId:16,Job_Grade:'P-11',PM_Job_Classification:"XXXX有限公司",
badge:"中下",English_Name:"wang wu",
company:'XXXX有限公司'},
],
}
}
onSelect(){
}
render(){
const {
dataSource} = this.state;
let keys = {
id:"key",parentId:"parentDeptId",topId:1,name:"title"};
let newData = generate(dataSource,keys.topId,keys);
return (
<Tree
onSelect={
()=>this.onSelect()}
defaultExpandedKeys={
['11']}
>
{
loop.call(this,newData,keys)}
</Tree>
)
}
}
export {
MyTree }
三、API 使用指南
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
defaultExpandedKeys | 默认展开指定的树节点 | string[] | 无 |
onSelect | 点击某一行触发的事件 | Function | 无 |
treeData | 链表结构的数据源 | Array | 无 |
key | 唯一key,不可重复 | Array | 无 |
title | 标题 | string | 无 |
children | 子节点 | Array | 无 |
四、源代码
Tree.js
import React from 'react';
import arrowDownGray from '../../assets/home/arraw-down-gray.png';
import arrowUpGray from '../../assets/home/arraw-up-gray.png';
class Tree extends React.Component{
constructor(props){
super(props);
this.state = {
keys:[]
}
}
componentWillMount(){
const {
defaultExpandedKeys,treeData,children} = this.props;
//处理默认样式
if(treeData && treeData.length>0){
if(Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length>0) {
treeData.map((item) => {
if (defaultExpandedKeys.includes(String(item.key))) {
item.isOpenChild = true;//展开子节点
}
});
}
}
//处理自定义样式
if(children && children.length>0){
let keys = [];
children.map((item)=>{
keys.push(item.key)
});
if(Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length>0){
children.map((item1)=>{
if(defaultExpandedKeys.includes(item1.key)){
if(Array.isArray(item1.props.children) && item1.props.children.length>0){
item1.props.children.map((item2)=>{
keys.push(item2.key);
});
}
}
});
}
this.setState({
keys
});
}
}
onTrigger(e,item){
e.stopPropagation();
let keys = this.state.keys;
if(item.props.children && item.props.children.length>0){
item.props.children.map((item1)=>{
//如果没有子节点就添加
if(!keys.includes(item1.key)){
keys.push(item1.key);
}else {
//否则过滤子节点
keys = keys.filter(item2=>item2!=item1.key);
}
});
}
this.setState({
keys
})
}
onSelect(e,item){
const {
onSelect} = this.props;
if(onSelect){
onSelect(item);
}
}
onTrigger1(e,item){
e.stopPropagation();
item.isOpenChild = !item.isOpenChild;
this.setState({
isRefresh:!this.state.isRefresh
})
}
loopTree(arr){
return arr.map((item,index)=>{
return <div key={
index}>
{
<div className="cm-flex cm-jc-sb cm-c-333 cm-ai-c cm-cursor-p">
<div className="cm-flex-1 cm-hover-bc-eee cm-p-01" onClick={
(e)=>this.onSelect(e,item)}>{
item.title}</div>
{
item.children?
<div onClick={
(e)=>this.onTrigger1(e,item)} className="cm-ml-01 cm-p-005">
<img src={
item.isOpenChild?arrowUpGray:arrowDownGray}
className="cm-img-01" alt="" />
</div> :null
}
</div>
}
{
item.children&&item.children.length&&item.isOpenChild?
<div className={
item.isOpenChild?"cm-display-block ":"cm-display-none"}><div className="cm-ml-02" >{
this.loopTree(item.children)}</div></div> :null
}
</div>
})
}
loopChild(arr){
const {
keys} = this.state;
return arr.map((item,index)=>{
return <div key={
index} className={
keys.includes(item.key)?"cm-display-block":"cm-display-none"}>
{
<div className="cm-flex cm-jc-sb cm-p-01 cm-ai-c cm-cursor-p">
<div className="cm-flex-1" onClick={
(e)=>this.onSelect(e,item)}>{
item}</div>
{
item.props.children?
<div onClick={
(e)=>this.onTrigger(e,item)} className="cm-ml-01 cm-p-005">
<img src={
keys.includes(item.props.children[0].key)?arrowUpGray:arrowDownGray}
className="cm-img-01" alt="" />
</div> :null
}
</div>
}
{
item.props.children&&item.props.children.length?
<div className="cm-ml-02">{
this.loopChild(item.props.children)}</div>:null
}
</div>
})
}
render(){
const {
treeData,children} = this.props;
return (
<>
{
treeData && treeData.length>0?this.loopTree(treeData):this.loopChild(children)}
</>
)
}
}
class TreeNode extends React.Component{
constructor(props){
super(props);
}
render(){
const {
title} = this.props;
return <div className="cm-c-333">
{
title}
</div>
}
}
Tree.TreeNode = TreeNode;
export default Tree;
五、总结
树形控件分为默认控件和自定义控件。
- 默认控件
【用户】
只需要把数据源处理成链表结构即可。
【处理方式】
使用递归将子节点一层层铺开。初始铺开的是第一层节点,然后通过点击向上/向下箭头,分别展开/收起子节点。实现的逻辑是给对象添加一个布尔类型的 isOpenChild
属性,然后通过 setState 更新页面。
- 自定义控件
【用户】
用户通过递归 TreeNode
子节点,可自定义 title
属性,实现自定义列表样式。
【处理方式】
自定义控件主要是将 title
属性暴露给了用户,但是逻辑控制还是通过组件来完成。
由于自定义样式接收的是虚拟 DOM 结构,所以没法像默认控件那样通过改变对象的属性来展开/收起子节点,所以需要一个 keys
集合。
初始时将第一层节点的 key
添加到 keys
,然后通过点击向上/向下箭头,分别添加/过滤子节点 key
,子节点根据 key
是否存在于 keys
来判断是否展开/收起。