Echarts Tree踩坑之旅

5,209 阅读3分钟

需要展示一个组织结构图,默认展开三层,可以下载文件,具体需求如下图:

image.png 经过选择,最终决定使用 Echarts Tree 来实现需求。实现效果如下:

image.png 下面来说下具体实现方式: 按照 Echarts 官方文档引入组件:

import * as echarts from 'echarts/core';
import { TooltipComponent } from 'echarts/components';
import { TreeChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';

require('echarts/lib/component/toolbox');
echarts.use([TooltipComponent, TreeChart, CanvasRenderer]);

下一步初始化组件:

<div ref="showEChart" :class="className" :style="{height:height,width:width}" :key="treeKey"/>
let showEChart = this.$refs.showEChart;
this.chart = echarts.init(showEChart, 'macarons');

初始化组件数据以及样式,这一步,可以将各节点的公用样式定义在 label 中:

setOptions (data) {
    let option = {
        // 提供数据视图、还原、下载的工具
        toolbox: {
            show: true
            // feature: {
            //     mark: { show: true },
            //     dataView: { show: true, readOnly: false },
            //     restore: { show: true },
            //     saveAsImage: { show: true }
            // }
        },
        series: [
            {
                name: '组织结构图',
                type: 'tree',
                orient: 'TB', // 竖向或水平  TB代表竖向 LR代表水平
                layout: 'orthogonal',
                edgeShape: 'polyline',
                edgeForkPosition: '63%',
                top: '50px',
                bottom: '50px',
                zlevel: 2,
                initialTreeDepth: this.initialTreeDepth, // 树图初始展开的层级(深度)
                // expandAndCollapse: true, // 点击节点时不收起子节点,default: true
                symbolSize: [100, 40], // 图片大小
                itemStyle: {
                    color: 'transparent',
                    padding: [10, 0, 10, 0],
                    borderWidth: 0
                },
                label: {
                    borderRadius: [5, 5, 5, 5],
                    borderColor: '#eeeeee',
                    backgroundColor: '#ffffff',
                    shadowColor: 'rgb(0 0 0 / 15%)',
                    shadowBlur: 1,
                    rich: {
                        first: {
                            align: 'center',
                            height: 30,
                            width: 130,
                            lineHeight: 30,
                            backgroundColor: '#ecf2f6',
                            borderRadius: [5, 5, 0, 0]
                        },
                        second: {
                            color: '#0e8dcb',
                            padding: [5, 10, 5, 10],
                            lineHeight: 26,
                            align: 'left'
                        }
                    }
                },
                lineStyle: {
                    color: '#D5D5D5',
                    width: 1,
                    curveness: 1
                },
                data: [data]
            }
        ]
    };
    let tree = JSON.parse(JSON.stringify(option));
    tree.series[0].zlevel = 1;
    tree.series[0].expandAndCollapse = false;
    this.chart.setOption(option);
},

动态设置生成图片的宽高:

adjustChart (chart, el) {
    let allNodes = chart._chartsViews[0]._data._graphicEls;
    let nodeNums = allNodes.filter(node => node !== undefined).length;
    const height = window.innerHeight * 0.3;
    const width = window.innerWidth * 0.5;
    const currentHeight = 200 * this.initialTreeDepth;
    const currentWidth = 150 * nodeNums;
    const newHeight = Math.max(currentHeight, height);
    const newWidth = Math.max(currentWidth, width);
    this.$nextTick(() => {
        this.height = newHeight + 'px';
        this.width = newWidth + 'px';
        chart.resize({ width: newWidth, height: newHeight });
    });
},

注:在设置完宽高之后,必须调用 resize 方法,刷新组件宽高,否则会出现 height已重新赋值,但是图片的高度用的是上一次计算的高度

下载图片:

downLoad (type, fileName) {
    let picInfo = this.chart.getDataURL({
        type,
        pixelRatio: 1.5, // 放大两倍下载,之后压缩到同等大小展示。解决生成图片在移动端模糊问题
        backgroundColor: '#fff'
    });// 获取到的是一串base64信息
    const elink = document.createElement('a');
    elink.download = fileName + '.' + type;
    elink.style.display = 'none';
    elink.href = picInfo;
    document.body.appendChild(elink);
    elink.click();
    document.body.removeChild(elink);
},

切换图片默认展开层级,但此方法可能出现节点隐藏,边未隐藏的情况,尚未找到解决方法:

 /**
* 层级
* @param depth
*/
   settingOption (depth) {
       let info = this.chart.getOption();
       info.series[0].data = this.formatOptions(info.series[0].data, depth);
       this.chart.setOption(info);
       this.adjustChart(this.chart, 'showEChart');
   },
   formatOptions (info, depth) {
       return info.map(el => {
           this.$set(el, 'collapsed', el.level > depth);
           if (el.level <= depth && el.children && el.children.length > 0) {
               this.formatOptions(el.children, depth);
           }
           return el;
       });
   }

也可以通过引用组件时,传入的默认展开层级实现

因为图片下载要求下载完整的组织结构图,所以在此又初始化了一个数,其方法大致与以上相似,只是展示完整的组织结构图用于下载。

完整组件代码如下:

<template>
  <div>
    <div ref="showEChart" :class="className" :style="{height:height,width:width}" :key="treeKey"/>
    <div ref="photoTree" :class="className" style="position: fixed;top: -99999px" :style="{height:treeHeight,width:width}"/>
  </div>
</template>

<script>
import debounce from 'lodash.debounce';
import * as echarts from 'echarts/core';
import { TooltipComponent } from 'echarts/components';
import { TreeChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';

require('echarts/lib/component/toolbox');
echarts.use([TooltipComponent, TreeChart, CanvasRenderer]);

export default {
    props: {
        // 初始展开层级,默认为10
        initialTreeDepth: {
            type: Number,
            default: 10
        },
        className: {
            type: String,
            default: 'chart'
        },
        // 默认展示宽度
        width: {
            type: String,
            default: '100%'
        },
        // 默认展示高度
        height: {
            type: String,
            default: '450px'
        },
        // 数据
        chartData: {
            type: Object,
            required: true
        }
    },
    data () {
        return {
            chart: null,
            treeHeight: '450px',
            photoTree: null,
            treeKey: 1
        };
    },
    watch: {
        chartData: {
            deep: true,
            handler (val) {
                this.setOptions(val);
            }
        }
    },
    mounted () {
        this.initChart();
        // 是否需要自适应-加了防抖函数
        this.__resizeHandler = debounce(() => {
            if (this.chart) {
                this.chart.resize();
            }
            if (this.photoTree) {
                this.photoTree.resize();
            }
        }, 100);
        window.addEventListener('resize', this.__resizeHandler);
        // 监听侧边栏的变化以实现自适应缩放
        const sidebarElm = document.getElementsByClassName('sidebar-container')[0];
        sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler);
    },
    beforeDestroy () {
        if (!this.chart && !this.photoTree) {
            return;
        }
        window.removeEventListener('resize', this.__resizeHandler);
        this.chart.dispose();
        this.photoTree.dispose();
        this.chart = null;
        this.photoTree = null;

        const sidebarElm = document.getElementsByClassName('sidebar-container')[0];
        sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler);
    },
    methods: {
        initChart () {
            let showEChart = this.$refs.showEChart;
            let photoTree = this.$refs.photoTree;
            this.chart = echarts.init(showEChart, 'macarons');
            this.photoTree = echarts.init(photoTree, 'macarons');
            this.setOptions(this.chartData);
            this.adjustChart(this.chart, 'showEChart');
            this.adjustChart(this.photoTree, 'photoTree');
            this.chart.on('click', () => {
                this.adjustChart(this.chart, 'showEChart');
            }); // 节点点击事件
        },
        /**
     * 节点调整
     */
        adjustChart (chart, el) {
            let allNodes = this.photoTree._chartsViews[0]._data._graphicEls;
            let nodeNums = allNodes.filter(node => node !== undefined).length;
            const height = window.innerHeight * 0.3;
            const width = window.innerWidth * 0.5;
            const currentHeight = 200 * this.initialTreeDepth;
            const currentWidth = 150 * nodeNums;
            const newHeight = Math.max(currentHeight, height);
            const newWidth = Math.max(currentWidth, width);
            this.$nextTick(() => {
                if (el === 'photoTree') {
                    this.treeHeight = newHeight + 'px'; // 设置高度自适应
                } else {
                    this.height = newHeight + 'px';
                }
                this.width = newWidth + 'px';
                chart.resize({ width: newWidth, height: newHeight });
            });
            return true;
        },
        setOptions (data) {
            let option = {
                // 提供数据视图、还原、下载的工具
                toolbox: {
                    show: true
                    // feature: {
                    //     mark: { show: true },
                    //     dataView: { show: true, readOnly: false },
                    //     restore: { show: true },
                    //     saveAsImage: { show: true }
                    // }
                },
                series: [
                    {
                        name: '组织结构图',
                        type: 'tree',
                        orient: 'TB', // 竖向或水平  TB代表竖向 LR代表水平
                        layout: 'orthogonal',
                        edgeShape: 'polyline',
                        edgeForkPosition: '63%',
                        top: '50px',
                        bottom: '50px',
                        zlevel: 2,
                        initialTreeDepth: this.initialTreeDepth, // 树图初始展开的层级(深度)
                        // expandAndCollapse: true, // 点击节点时不收起子节点,default: true
                        symbolSize: [100, 40], // 图片大小
                        itemStyle: {
                            color: 'transparent',
                            padding: [10, 0, 10, 0],
                            borderWidth: 0
                        },
                        label: {
                            borderRadius: [5, 5, 5, 5],
                            borderColor: '#eeeeee',
                            backgroundColor: '#ffffff',
                            shadowColor: 'rgb(0 0 0 / 15%)',
                            shadowBlur: 1,
                            rich: {
                                first: {
                                    align: 'center',
                                    height: 30,
                                    width: 130,
                                    lineHeight: 30,
                                    backgroundColor: '#ecf2f6',
                                    borderRadius: [5, 5, 0, 0]
                                },
                                second: {
                                    color: '#0e8dcb',
                                    padding: [5, 10, 5, 10],
                                    lineHeight: 26,
                                    align: 'left'
                                }
                            }
                        },
                        lineStyle: {
                            color: '#D5D5D5',
                            width: 1,
                            curveness: 1
                        },
                        data: [data]
                    }
                ]
            };
            let tree = JSON.parse(JSON.stringify(option));
            tree.series[0].zlevel = 1;
            tree.series[0].expandAndCollapse = false;
            this.photoTree.setOption(tree);
            this.chart.setOption(option);
        },
        sidebarResizeHandler (e) {
            if (e.propertyName === 'width') {
                this.__resizeHandler();
            }
        },
        /**
     * 下载
     * @param type
     * @param fileName
     */
        downLoad (type, fileName) {
            let picInfo = this.photoTree.getDataURL({
                type,
                pixelRatio: 1.5, // 放大两倍下载,之后压缩到同等大小展示。解决生成图片在移动端模糊问题
                backgroundColor: '#fff'
            });// 获取到的是一串base64信息
            const elink = document.createElement('a');
            elink.download = fileName + '.' + type;
            elink.style.display = 'none';
            elink.href = picInfo;
            document.body.appendChild(elink);
            elink.click();
            document.body.removeChild(elink);
        },
        /**
     * 层级
     * @param depth
     */
        settingOption (depth) {
            let info = this.chart.getOption();
            info.series[0].data = this.formatOptions(info.series[0].data, depth);
            this.chart.setOption(info);
            this.adjustChart(this.chart, 'showEChart');
        },
        formatOptions (info, depth) {
            return info.map(el => {
                this.$set(el, 'collapsed', el.level > depth);
                if (el.level <= depth && el.children && el.children.length > 0) {
                    this.formatOptions(el.children, depth);
                }
                return el;
            });
        }
    }
};
</script>

引用组件:

<Tree :chartData="treeData" ref="tree" :initialTreeDepth="initialTreeDepth" :key="initialTreeDepth"></Tree>
import Tree from './chartTree';

export default {
    data () {
        return {
            initialTreeDepth: 3,
            treeData: {}
        };
    },
    methods: {
        /**
       * 下载
       */
        downLoad () {
            this.$refs.tree.downLoad(this.search.type, '组织结构图');
        },

        /**
       * 展示
       */
        settingOptions (info) {
            this.initialTreeDepth = info;
        }
    },
    created () {
        this.treeData = {
            level: 1,
            expandAndCollapse: false,
            label: {
                formatter: [
                    '{first|全部}'
                ].join('\n'),
                rich: {
                    first: {
                        align: 'center',
                        borderRadius: [5, 5, 5, 5]
                    }
                }
            },
            children: [{
                'level': 2,
                'label': { 'formatter': '{first|zz01}\n{second|001\n用户:11}' },
                'expandAndCollapse': false,
                'children': [{
                    'level': 3,
                    'label': { 'formatter': '{first|组织02}\n{second|002\n用户:11}' },
                    'expandAndCollapse': true,
                    'children': [{
                        'level': 4,
                        'label': { 'formatter': '{first|组织03}\n{second|003\n用户:8}' },
                        'expandAndCollapse': true
                    }]
                }]
            }, {
                'level': 2,
                'label': { 'formatter': '{first|银河帝国宇宙军}\n{second|0999999999999999\n用户:10}' },
                'expandAndCollapse': false,
                'children': [{
                    'level': 3,
                    'label': { 'formatter': '{first|阿尔法军团}\n{second|0000001\n用户:10}' },
                    'expandAndCollapse': true,
                    'children': [{
                        'level': 4,
                        'label': { 'formatter': '{first|泰坦奥米伽}\n{second|0000045\n用户:10}' },
                        'expandAndCollapse': true,
                        'children': [{
                            'level': 5,
                            'label': { 'formatter': '{first|欧西里斯无畏舰}\n{second|121212312\n用户:10}' },
                            'expandAndCollapse': true,
                            'children': [{
                                'level': 6,
                                'label': { 'formatter': '{first|天穹战列舰}\n{second|0787878\n用户:9}' },
                                'expandAndCollapse': true,
                                'children': [{
                                    'level': 7,
                                    'label': { 'formatter': '{first|狂风导弹巡洋舰}\n{second|123123123\n用户:2}' },
                                    'expandAndCollapse': true
                                }, {
                                    'level': 7,
                                    'label': { 'formatter': '{first|凛冬战略防御部门}\n{second|00099887\n用户:4}' },
                                    'expandAndCollapse': true
                                }]
                            }]
                        }]
                    }]
                }]
            }]
        };
    }
};