react echarts tree树图搜索展开功能示例详解
您的楼主邱大帅 人气:0前言
umi+antd-admin 框架中使用类组件+antd结合echarts完成树图数据展示和搜索展开功能
最终效果
版本信息
"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;
总结:
对两种树状图进行功能结合,并且添加搜索结果联动,感觉自己写的功能代码不够简洁,如果有问题欢迎大家积极提出讨论,共同进步~
加载全部内容