前言

umi+antd-admin 框架中使用类组件+antd结合echarts完成树图数据展示和搜索展开功能

最终效果




echarts组织结构图treemap_javascript


版本信息

"antd": "3.24.2",

"umi": "^2.7.7",

"echarts": "^4.4.0",

"echarts-for-react": "^2.0.15-beta.1",

核心功能:
  1. 左右树图数据展示,并且基本的展开功能
  2. 左树图的搜索功能,搜索后结果自动展开默认显示上级
  3. 点击左树图,右边树图对应节点展开
  4. 右树图背景水印,全屏功能
  5. 右树图定制节点样式
关键思路:

点击左边树状结构时,首先通过递归找到具体哪一个分支中包含对应结果,然后再通过递归一层一层的添加属性collapsed,如果改分支下面包含结果添加collapsed:false,其他分支添加collapsed:true

重新渲染时echarts tree的options中initialTreeDepth属性需要设置一下,为结果点所在层级

代码附上:

数据data.js

export const lineLabel = { //树图中部分节点展示样式为线上样式
  fontSize: 14,
  color: '#333333',
  offset: [ 5, -15],
  borderRadius: 0,
  borderColor: 'transparent',
  backgroundColor: 'transparent',
  position: 'left',
  verticalAlign: 'middle',
  align: 'right',
}

// 数据
export const data = [
  { yybid: '1', fid: '0', grade: '0', yybmc: '课程',itemStyle: { color: "#DE4A3C" } },
  { yybid: '101', fid: '1', grade: '1', yybmc: '语文',itemStyle: { color: "#DE4A3C" } },
  { yybid: '1011', fid: '101', grade: '2', yybmc: '听写',itemStyle: { color: "#DE4A3C" } },
  { yybid: '10111', fid: '1011', grade: '3', yybmc: '生字',itemStyle: { color: "#DE4A3C" }},

  { yybid: '10111-1', fid: '10111', grade: '4', yybmc: '文字',itemStyle: { color: "#DE4A3C" },label:lineLabel }, // 比如这个就是显示在线上的意思,可以定制节点样式
  { yybid: '10111-1-1', fid: '10111-1', grade: '5', yybmc: '同音字',itemStyle: { color: "#DE4A3C" } },
  { yybid: '10111-1-1-1', fid: '10111-1-1', grade: '6', yybmc: '...',itemStyle: { color: "#FFFFFF" } },
  { yybid: '10111-1-1-2', fid: '10111-1-1', grade: '6', yybmc: '...',itemStyle: { color: "#FFFFFF" } },
  { yybid: '10111-1-1-3', fid: '10111-1-1', grade: '6', yybmc: '...',itemStyle: { color: "#FFFFFF" } },
  { yybid: '10111-1-1-4', fid: '10111-1-1', grade: '6', yybmc: '...',itemStyle: { color: "#FFFFFF" } },
  { yybid: '10111-1-2', fid: '10111-1', grade: '5', yybmc: '多音字',itemStyle: { color: "#DE4A3C" } },
    { yybid: '102', fid: '1', grade: '1', yybmc: '数学',itemStyle: { color: "#DE4A3C" } },
    .....
]

功能:

import React from 'react';
import { Row, Col, Input, Tree, Button, Icon } from 'antd';
import TreeUtils from '../../../../../utils/treeUtils';
import ReactEcharts from 'echarts-for-react'
import styles from './index.less';
import { data } from './data'

const { Search } = Input;

class Map extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      cNameTreeData: [],
      cNameList: [],
      expandedKeys: [], // 展开节点key
      autoExpandParent: true,
      cmapOpt:{},
      cmapTreeData:{},
      cmapTreeDataOrigin:{},
      defaultExpandedKeys:[], // 默认展开节点key
      keyWord:'',//搜索关键字
      isFullScreen:false,
    };
  }

  componentDidMount() {
    this.fetchcName();
  }

  getOption =(data,initialTreeDepth)=>{
    // 设置水印
    let user =  { name :'管理员' , loginName :'admin'}
    const waterMarkText = `${user.name} ${user.loginName}`
    const canvas = document.createElement('canvas')
    canvas.width = 200
    canvas.height = 150
    const ctx = canvas.getContext('2d')
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.globalAlpha = 0.09
    ctx.font = '16px sans-serif'
    ctx.translate(70,90)
    ctx.rotate(-Math.PI / 4)
    ctx.fillText(waterMarkText, 0, 0)

    const opt= {
      backgroundColor: {
        image:canvas
      },
      toolbox: { 
          top:30,
          right:30,
          itemSize:24,
          feature: {
        //自定义toolbox,必须以my开头,全屏功能
        myFullScreen:{
          show:true,
          title:this.state.isFullScreen?'退出全屏':'全屏',
          icon:"image://" + require("../../../../../assets/fullScreen.png"),
          emphasis: {
            iconStyle: {
                textFill: '#DE4A3C',  //文本颜色,若未设定,则依次取图标 emphasis 时的填充色、描边色,若都不存在,则为'#000'
                textAlign: 'center',  //文本对齐方式,属性值:left/center/right
            }
          },  
          onclick: (e) => {
              //isFullScreen定义在data中,初始化为false
              this.setState({
                isFullScreen:!this.state.isFullScreen
              },()=>{
                const element = document.getElementById('cmapTree');
                if (element.requestFullScreen) { // HTML W3C 提议
                  element.requestFullScreen();
                } else if (element.msRequestFullscreen) { // IE11
                  element.msRequestFullScreen();
                } else if (element.webkitRequestFullScreen) { // Webkit (works in Safari5.1 and Chrome 15)
                  element.webkitRequestFullScreen();
                } else if (element.mozRequestFullScreen) { // Firefox (works in nightly)
                  element.mozRequestFullScreen();
                }
                // 退出全屏
                if (element.requestFullScreen) {
                  document.exitFullscreen();
                } else if (element.msRequestFullScreen) {
                  document.msExitFullscreen();
                } else if (element.webkitRequestFullScreen) {
                  document.webkitCancelFullScreen();
                } else if (element.mozRequestFullScreen) {
                  document.mozCancelFullScreen();
                }
              })
          }
        }
      } 
      },
      series: [
      {
          type: 'tree',
          // silent:true,
          data: [data],

          top: '10%',
          left: '10%',
          bottom: '10%',
          right: '15%',
          // edgeShape:'polyline',

          symbolSize: 10,
          // symbolSize: [30, 30],

          label: {
            color: '#FFFFFF',
            distance: 0,
            fontSize: 16,
            borderWidth: 0,
            borderRadius: 4,
            borderColor: 'rgba(222, 74, 60, 0.9)',
            backgroundColor: 'rgba(222, 74, 60, 0.9)',
            padding: [6, 10, 6 ,10], // 最开始的样式
            // padding: [6, 10],
            position: 'left',
            verticalAlign: 'middle',
            align: 'right', // 最开始的样式
            // align: 'insideRight',
          },

          leaves: {
              label: {
                  position: 'right', // 最开始的样式
                  // position: 'left',
                  verticalAlign: 'middle',
                  align: 'left',
                  color: '#333333',
                  distance: -15,
                  margin: 0,
                  fontSize: 16,
                  borderWidth: 0,
                  borderColor: 'rgba(222, 74, 60, 0.1)',
                  backgroundColor: 'rgba(222, 74, 60, 0.1)',
                  borderRadius: 4,
                  padding: [6, 10, 6 , 20], // 最开始的样式
                  // padding: [6, 10],
              }
          },
          itemStyle:{
            borderType : 'solid',
            borderWidth : 2,
            borderColor : '#DE4A3C',
          },
          lineStyle:{
            color:'#DE4A3C'
          },

          edgeForkPosition: "72%",
          emphasis: { // 高亮
            focus: 'descendant'
          },
          animationDuration: 300,
          animationDurationUpdate: 300,
          initialTreeDepth:initialTreeDepth, // 树图初始展开层级,树图初始展开的层级(深度)。根节点是第 0 层,然后是第 1 层、第 2 层,... ,直到叶子节点
          roam:true,//鼠标缩放,拖拽整颗树
          expandAndCollapse: true,//无关的子树折叠收起
          // width: "50%"//组件宽度
        }
      ]
    }


    this.setState({
        cmapOpt:opt
    })
    
  }

  // 获取名称
  fetchcName = () => {
    const cName = {};
    

    // 设置树数据
    const datas = TreeUtils.toTreeData(data, { keyName: 'yybid', pKeyName: 'fid', titleName: 'yybmc', normalizeTitleName: 'title', normalizeKeyName: 'key' }, true);
    cName.datas = [];
    datas.forEach((item) => {
      const { children } = item;
      cName.datas.push(...children);
    });
    cName.dataLoaded = true;
    // 设置树形图数据
    const optData = TreeUtils.toTreeMapData(data, { keyName: 'yybid', pKeyName: 'fid', titleName: 'yybmc', normalizeTitleName: 'name', normalizeKeyName: 'value' }, true);
    cName.optDatas = [];
    optData.forEach((item) => {
      const { children } = item;
      cName.optDatas.push(...children);
    });
    // 设置默认展开第一层    
    const expandedKeys = []
    cName.datas.forEach(item =>{
      expandedKeys.push(item.key)
    })
    this.setState({ 
      cNameTreeData: cName.datas, 
      cNameList: data,
      cmapTreeData:cName.optDatas[0],
      cmapTreeDataOrigin:cName.optDatas[0],
      expandedKeys,
    },()=>{
      this.getOption(cName.optDatas[0],1)
    });
  }


  // 关键字搜索
  handleOnkeyWord = (e) => {
    const keyWord = e.target.value;
    this.setState({
      keyWord:keyWord.trim()
    })
  }
  // 搜索功能
  searchTree = () =>{
    if(this.state.keyWord){
      this.getTreeExpand(this.state.keyWord)
    }else {
      // 设置默认展开第一层
      const cName = {};
      const datas = TreeUtils.toTreeData(data, { keyName: 'yybid', pKeyName: 'fid', titleName: 'yybmc', normalizeTitleName: 'title', normalizeKeyName: 'key' }, true);
      cName.datas = [];
      datas.forEach((item) => {
        const { children } = item;
        cName.datas.push(...children);
      });    
      const expandedKeys = []
      cName.datas.forEach(item =>{
        expandedKeys.push(item.key)
      })
      this.setState({ 
        cNameTreeData: cName.datas,
        expandedKeys,
      });
    }
  }
  // 设置左树展开
  getTreeExpand = (keyWord) =>{
      // 筛选数据
      const { cNameList } = this.state;
      const newTreeList = cNameList.filter((item) => {
        if (item.yybmc.indexOf(keyWord) !== -1) {
          return true;
        }
        return false;
      });
  
      const newTreeParent = [];
      const expandedKeys = [];
      // 获取所有子节点的父节点
      newTreeList.forEach((item) => {
        expandedKeys.push(item.yybid);
        // 不是根节点
        newTreeParent.push(item);
        for (let i = item.grade; i > 0; i--) {
          const newParent = this.getByChildId(newTreeParent[newTreeParent.length - 1].fid);
          newTreeParent.push(newParent[0]);
        }
      });
  
      // 合并数组
      const tempNewData = [...newTreeParent, ...newTreeList];
  
      // 数组去重
      let newData = new Set(tempNewData);
      newData = [...newData];
  
      // 构造树形数据
      const newTreeData = [];
      const datas = TreeUtils.toTreeData(newData, { keyName: 'yybid', pKeyName: 'fid', titleName: 'yybmc', normalizeTitleName: 'title', normalizeKeyName: 'key' }, true);
      newTreeData.datas = [];
      datas.forEach((item) => {
        const { children } = item;
        newTreeData.datas.push(...children);
      });
      newTreeData.dataLoaded = true;
  
      this.setState({
        cNameTreeData: newTreeData.datas,
        expandedKeys,
        autoExpandParent: true,
      });
  }

  // 根据子节点找到父节点
  getByChildId(childId) {
    return this.state.cNameList.filter((item) => {
      return item.yybid === childId;
    });
  }

  // 选中树形数据
  onSelect = (selectedKeys) => {
    const select = this.state.cNameList.filter((item) => {
      return item.yybid === selectedKeys[0];
    });
    
    if (select && select.length > 0) {
      this.handleOnExpand(select[0])
    }
  }


  onExpand = (expandedKeys) => {
    this.setState({
      expandedKeys,
      autoExpandParent: false,
    });
  };

  // 处理data中的children添加属性展开显示
  addCollapsed = (children, selectItem) => {
    let {yybid} = selectItem;
    const newChildren = []
    children.forEach(obj =>{
      let newObj = {}
      // newObj = {...obj,collapsed :true}
      if(obj.value === yybid){
        newObj = {...obj,collapsed :false}
      }else if(obj.children && obj.children.length) {
        if(!this.isHaveChildren(obj.children,yybid)){
          let newChildren = {}
          newChildren = this.addCollapsed(obj.children,selectItem)
          newObj = {...obj,collapsed :true,children:newChildren}
        }else{
          let newChildren = {}
          newChildren = this.addCollapsed(obj.children,selectItem)
          newObj = {...obj,collapsed :false,children:newChildren}
        }
      }else {
        newObj = {...obj,collapsed :true}
      }
      newChildren.push({...obj,...newObj})
    })
    return newChildren
  }
  
  // 判断下面是否有子节点
  isHaveChildren = (arr,selectId) =>{
    const res = []
    arr.forEach(item =>{
      if(item.value === selectId){
        res.push('true')
      }else if(item.children && item.children.length) {
        res.push(String(this.isHaveChildren(item.children,selectId)))
      }else {
        res.push('false')
      }
    })
    return res.some(resObj => resObj === 'true')
  }


  // 树状图搜索展开
  handleOnExpand = (selectItem) =>{
    const { grade } = selectItem
    const { children } = this.state.cmapTreeDataOrigin
    let newChildren = []
    if(grade === '0'){
      // 设置树形图数据
      const optData = TreeUtils.toTreeMapData(data, { keyName: 'yybid', pKeyName: 'fid', titleName: 'yybmc', normalizeTitleName: 'name', normalizeKeyName: 'value' }, true);
      let optDatas = [];
      optData.forEach((item) => {
        const { children } = item;
        optDatas.push(...children);
      });
      return this.setState({
        cmapTreeData:optDatas[0]
      },()=>{
        this.getOption(optDatas[0],'1')
      })
    }else if(grade === '1'){
      children.forEach(obj =>{
        let newObj = {}
        if(obj.value === selectItem.yybid){
          newObj = {...obj,collapsed :false}
        }else {
          newObj = {...obj,collapsed :true}
        }
        newChildren.push({...obj,...newObj})
      })
    }else {
      newChildren = this.addCollapsed(children, selectItem)
      // console.log(selectItem)
    }
    this.setState({
      cmapTreeData:{
        ...this.state.cmapTreeData,
        children:newChildren
      }
    },()=>{
      this.getOption(this.state.cmapTreeData,grade)
    })
  }

  
  componentWillUnmount = () => {
    this.setState = (state,callback)=>{
      return;
    };
}

  render() {
    const { cNameTreeData, autoExpandParent, expandedKeys,cmapOpt } = this.state;

    return (
      <React.Fragment>
            <Row style={{height:'88vh'}}>
              <Col xs={5} sm={5} lg={5} xl={5} style={{height:'100%'}}>
                <div style={{width:'100%',display:'flex',height:'100%'}}>
                  <div style={{height:'100%',width:'90%',padding:'16px 20px 0px 20px'}}>
                    {/* 搜索框 */}
                    <div style={{fontSize:'16px',marginBottom:'10px',color:'#666666',fontWeight:'bold'}}>搜索功能</div>
                    <Search 
                      allowClear 
                      style={{ width: '100%',height:'40px', margin: '0px 10px 10px 0px',borderColor:'#999' }} 
                      placeholder="请输入" 
                      onChange={e => this.handleOnkeyWord(e)} 
                      onPressEnter={this.searchTree}
                      onSearch={this.searchTree} />
                    {/* 树形组件 */}
                    <Tree
                      style={{height:'85%',overflowY:'auto',width:'100%'}}
                      className={styles.tree}
                      onSelect={this.onSelect}
                      onExpand={this.onExpand}
                      expandedKeys={expandedKeys}
                      autoExpandParent={autoExpandParent}
                      treeData={cNameTreeData}
                      height={800}
                    />
                  </div>
                </div>
              </Col>
                {/* tree图 */}
              {cmapOpt?
                  <Col xs={19} sm={19} lg={19} xl={19} style={{height:'100%'}}>
                  {/* <div style={{position:'absolute',top:'35px',right:'30px',fontSize:'16px',color:'#DE4A3C'}}>全屏</div> */}
                  <div  id="cmapTree" style={{backgroundColor:'white',height:'100%'}}>
                    <ReactEcharts
                      option={cmapOpt}
                      style={{ width: '100%', height: '100%' }}
                      notMerge />
                  </div>
                  </Col> : ''
              }
            </Row>        
      </React.Fragment>
    );
  }
}
export default cMap;

TreeUtils

/**
 * Object对象相关的自定义处理函数
 */
const TreeUtils = {
  toTreeData(datas, { keyName = 'id', pKeyName = 'pid', titleName = 'name', normalizeTitleName = 'title', normalizeKeyName = 'key', parentName = 'fid' }, normalize = true, persistPrimaryData = false) { // 将普通数据转成树状结构
    // persistPrimaryData:保留原来的数据
    const tree = [];
    const noParentTemp = []; // 临时存放所有父节点,一旦改父节点是另一个节点的子节点,那么就删掉,最后剩下的就是没有完整父节点的节点了
    const isChildTemp = []; // 存放所有曾为子节点的节点
    const relation = {}; // 存放节点数据及其之间的关系
    // 遍历数据
    datas.forEach((data) => {
      const key = data[keyName];
      const pKey = data[pKeyName];
      const title = data[titleName];
      // 记录所有的子节点信息
      isChildTemp.push(key);
      // 记录暂时还没有发现父节点的项
      if (!noParentTemp.includes(pKey)) {
        noParentTemp.push(pKey);
      }
      // 如果发现该项在"暂时没有完整父节点"数组中,那么就从数组中删除掉
      if (noParentTemp.includes(key)) {
        noParentTemp.splice(noParentTemp.indexOf(key), 1);
      }

      // 将当前项的数据存在relation中
      const itemTemp = normalize ? { [normalizeKeyName]: key, [normalizeTitleName]: title, [parentName]: pKey } : { ...data };
      if (persistPrimaryData) {
        Object.assign(itemTemp, { primaryData: data });
      }
      if (!relation[key]) {
        relation[key] = {};
      }
      Object.assign(relation[key], itemTemp);

      // 将当前项的父节点数据也存在relation中
      if (!relation[pKey]) {
        relation[pKey] = normalize ? { [normalizeKeyName]: pKey } : { [keyName]: pKey };
      }

      // 如果作为父节点,没有children.那么就加上
      if (!relation[pKey].children) {
        relation[pKey].children = [];
      }

      // 将父子节点通过children关联起来,形成父子关系
      relation[pKey].children.push(relation[key]);
    });

    // 将没有完整父节点的节点过滤一下,剩下的就是没有父节点的节点了(如果只有一个,那就是根节点根节点)
    noParentTemp.forEach((key) => {
      if (!isChildTemp.includes(key)) {
        tree.push(relation[key]);
      }
    });
    return tree;
  },
  toTreeMapData(datas, { keyName = 'id', pKeyName = 'pid', titleName = 'name', normalizeTitleName = 'title', normalizeKeyName = 'key', parentName = 'fid' }, normalize = true, persistPrimaryData = false) { // 将普通数据转成树状结构
    // persistPrimaryData:保留原来的数据
    const tree = [];
    const noParentTemp = []; // 临时存放所有父节点,一旦改父节点是另一个节点的子节点,那么就删掉,最后剩下的就是没有完整父节点的节点了
    const isChildTemp = []; // 存放所有曾为子节点的节点
    const relation = {}; // 存放节点数据及其之间的关系
    // 遍历数据
    datas.forEach((data) => {
      const key = data[keyName];
      const pKey = data[pKeyName];
      const title = data[titleName];
      const itemStyle = data['itemStyle']
      const label = data['label']
      // 记录所有的子节点信息
      isChildTemp.push(key);
      // 记录暂时还没有发现父节点的项
      if (!noParentTemp.includes(pKey)) {
        noParentTemp.push(pKey);
      }
      // 如果发现该项在"暂时没有完整父节点"数组中,那么就从数组中删除掉
      if (noParentTemp.includes(key)) {
        noParentTemp.splice(noParentTemp.indexOf(key), 1);
      }

      // 将当前项的数据存在relation中
      const itemTemp = normalize ? { [normalizeKeyName]: key, [normalizeTitleName]: title, [parentName]: pKey ,'itemStyle':itemStyle,'label':label} : { ...data };
      if (persistPrimaryData) {
        Object.assign(itemTemp, { primaryData: data });
      }
      if (!relation[key]) {
        relation[key] = {};
      }
      Object.assign(relation[key], itemTemp);

      // 将当前项的父节点数据也存在relation中
      if (!relation[pKey]) {
        relation[pKey] = normalize ? { [normalizeKeyName]: pKey } : { [keyName]: pKey };
      }

      // 如果作为父节点,没有children.那么就加上
      if (!relation[pKey].children) {
        relation[pKey].children = [];
      }

      // 将父子节点通过children关联起来,形成父子关系
      relation[pKey].children.push(relation[key]);
    });

    // 将没有完整父节点的节点过滤一下,剩下的就是没有父节点的节点了(如果只有一个,那就是根节点根节点)
    noParentTemp.forEach((key) => {
      if (!isChildTemp.includes(key)) {
        tree.push(relation[key]);
      }
    });
    return tree;
  },
};

export default TreeUtils;
总结:

对两种树状图进行功能结合,并且添加搜索结果联动,感觉自己写的功能代码不够简洁,如果有问题欢迎大家积极提出讨论,共同进步~