React 组件封装之 Tree 树形控件

一、Tree 树形结构

组件说明:
实现树形控件,适用于组织架构、文章列表等链表结构的功能。

效果展示:

  1. 默认样式
    在这里插入图片描述

    2.自定义样式
    在这里插入图片描述
    3.用于组织架构的自定义样式

在这里插入图片描述

二、使用案例

  1. 默认样式案例

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']}
            />
        )
    }
}
  1. 自定义样式
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;

五、总结

树形控件分为默认控件和自定义控件。

  1. 默认控件

【用户】
只需要把数据源处理成链表结构即可。

【处理方式】
使用递归将子节点一层层铺开。初始铺开的是第一层节点,然后通过点击向上/向下箭头,分别展开/收起子节点。实现的逻辑是给对象添加一个布尔类型的 isOpenChild 属性,然后通过 setState 更新页面。

  1. 自定义控件

【用户】
用户通过递归 TreeNode 子节点,可自定义 title 属性,实现自定义列表样式。

【处理方式】

自定义控件主要是将 title 属性暴露给了用户,但是逻辑控制还是通过组件来完成。

由于自定义样式接收的是虚拟 DOM 结构,所以没法像默认控件那样通过改变对象的属性来展开/收起子节点,所以需要一个 keys 集合。

初始时将第一层节点的 key 添加到 keys,然后通过点击向上/向下箭头,分别添加/过滤子节点 key,子节点根据 key 是否存在于 keys 来判断是否展开/收起。

猜你喜欢

转载自blog.csdn.net/weixin_44135121/article/details/109309582