目录
  • 前言
    • 最终效果
    • 版本信息
    • 核心功能:
    • 关键思路:
  • 附上代码
    • 数据data.js
    • 功能:
    • TreeUtils
  • 总结:

    前言

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

    最终效果

    react echarts tree树图搜索展开功能示例详解

    版本信息

    "antd": "3.24.2",

    "umi": "^2.7.7",

    "echarts": "^4.4.0",

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

    核心功能:

    • 左右树图数据展示,并且基本的展开功能
    • 左树图的搜索功能,搜索后结果自动展开默认显示上级
    • 点击左树图,右边树图对应节点展开
    • 右树图背景水印,全屏功能
    • 右树图定制节点样式

    关键思路:

    点击左边树状结构时,首先通过递归找到具体哪一个分支中包含对应结果,然后再通过递归一层一层的添加属性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;
    

    总结:

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

    以上就是react echarts tree树图搜索展开功能示例详解的详细内容,更多关于react echarts tree树图搜索的资料请关注其它相关文章!

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。