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