1. 启动vue项目,执行以下命令安装dagre、graphlib、jointjs、svg-pan-zoom。
  npm install dagre graphlib jointjs svg-pan-zoom --save
  1. 新建vue文件,为svg准备父节点,以及部分初始化数据。
<template>
    <div class="container">
        <div id="paper"></div>
    </div>
</template>
<script>
  import dagre from "dagre";
  import graphlib from "graphlib";
  import * as joint from "jointjs";
  import '/node_modules/jointjs/dist/joint.css';
  import svgPanZoom from 'svg-pan-zoom';
  export default {
    data(){
        return {
            graph: null,
            paper: null,
            /** 原始数据:节点 */
            nodes: [
                { id: 1, label: 'node1' },
                { id: 2, label: 'node2' },
                { id: 3, label: 'node3' },
            ],
            /** 原始数据:连线 */
            links: [
                { from: 1, to: 2 },
                { from: 1, to: 3 }
            ],
            /** 处理后生成的节点 */
            nodeList: [],
            /** 处理后生成的连线 */
            linkList: []
        }
    },
    methods: {
      /** 页面初始化 */
      init(){
          /** 此处是下面依次写的几个函数执行 */
      }
    },
    mounted(){
        this.init();
    }
</script>
  1. 初始化画布,完成画布的初始化。
/** 初始化画布,按照joint文档来就可以,具体画布的尺寸和颜色自定义 */
initGraph() {
    let paper = document.getElementById('paper');
    this.graph = new joint.dia.Graph();
    this.paper = new joint.dia.Paper({
        dagre: dagre,
        graphlib: graphlib,
        el: paper,
        model: this.graph,
        width: '100%',
        height: 'calc(100vh - 100px)',
        background: {
            color: '#f5f5f5'
        },
        /** 是否需要显示单元格以及单元格大小(px) */
        // drawGrid: true,
        // gridSize: 20,
    });
}

将initGraph方法放入init执行后,页面上应当出现了一个浅灰色的画布( ̄▽ ̄)~*。

  1. 创建完画布之后,在画布上绘制节点。
/** 创建节点 */
createNode(){
    /** 遍历节点原始数据,通过joint.shapes.standard.Rectangle(joint内置shape)创建节点对象。 */
    this.nodes.forEach(ele => {
        let node = new joint.shapes.standard.Rectangle({
            id: ele.id,
            size: {
                width: 100,
                height: 50
            },
            attrs: {
                body: {
                    fill: '#ddd',
                    stroke: 'none'
                },
                text: {
                    text: ele.label
                }
            }
        });
        /** 创建的节点对象放入list */
        this.nodeList.push(node);
    })
    /** 通过graph的addCell方法向画布批量添加一个list */
    this.graph.addCell(this.nodeList);
}

执行完createNode方法,页面上出现了3个矩形覆盖在一起,可以拖动,是吧ಠᴗಠ。

  1. 把之前几个叠罗汉的矩形通过线连接起来。
    遍历links列表,通过joint.shapes.standard.Link创建节点间的连接关系。
/** 创建连线 */
createLink(){
    this.links.forEach(ele => {
        let link = new joint.shapes.standard.Link({
            source: {
                id: ele.from
            },
            target: {
                id: ele.to
            },
            attrs: {
                line: {
                    stroke: '#aaa',
                    strokeWidth: 1
                }
            }
        });
        /** 创建好的连线push进数组 */
        this.linkList.push(link);
    })
    /** 通过graph.addCell向画布批量添加连线 */
    this.graph.addCell(this.linkList);
}

发现执行完之后,页面上节点和连线都缩在一起聊天了(ㅍ_ㅍ)。。。可以拖动分散开,会看到隐藏的连线,不要慌,下面给它布个局分散开就行了~~

注意:必须先创建节点再创建连线,连线的数据可以看出是跟节点息息相关的,没有节点,也就没有从节点a指向节点b这条连线了,页面上会出现找不到节点的报错。

  1. 节点是可以通过position属性指定渲染位置的,例如: position: { x: 100, y: 200 }。连线是根据节点的位置计算来的。
    但是一般得到的数据大概率不会给你每个节点的具体坐标,所以自动布局是很有必要的。布局算法其实是dagre实现的,要是有兴趣可以去查查昂。
/** 画布节点自动布局,通过joint.layout.DirectedGraph.layout实现 */
randomLayout(){
    joint.layout.DirectedGraph.layout(this.graph, {
      dagre: dagre,
      graphlib: graphlib,
        /** 布局方向 TB | BT | LR | RL */
      rankDir: "LR",
        /** 表示列之间间隔的像素数 */
      rankSep: 200,
        /** 相同列中相邻接点之间的间隔的像素数 */
      nodeSep: 80,
        /** 同一列中相临边之间间隔的像素数 */
        edgeSep: 50
    });
}

执行完后,关系图已经是我们想要的样子了。但是图的位置在左上角,并且整个画布不可拖动,不是很灵活。所以使用svg-pan-zoom来优化动作。

  1. svg-pan-zoom实现画布拖拽缩放等操作。
/** svgpanzoom 画布拖拽、缩放 */
svgPanZoom(){
    /** 判断是否有节点需要渲染,否则svg-pan-zoom会报错。 */
    if(this.nodes.length){
        let svgZoom = svgPanZoom('#paper svg', {
            /** 是否可拖拽 */
            panEnabled: true,
            /** 是否可缩放 */
            zoomEnabled: true,
            /** 双击放大 */
            dblClickZoomEnabled: false,
            /** 可缩小至的最小倍数 */
            minZoom: 0.01,
            /** 可放大至的最大倍数 */
            maxZoom: 100,
            /** 是否自适应画布尺寸 */
            fit: true,
            /** 图是否居中 */
            center: true
        })
        /** 手动设置缩放敏感度 */
        svgZoom.setZoomScaleSensitivity(0.5);
    }
}

由于设置了fit:true导致图会自适应画布大小,节点少的话会导致图过分放大,如不需要自适应画布,可以设置为false。也可以在fit:true基础上天添加以下代码进行优化。

/** fit:true 元素数量较少时,会引起元素过度放大,当缩放率大于1时,将图像缩小为1;小于等于1时,为体现出边距更显美观,整体缩放至0.9 */
let {sx, sy} = this.paper.scale();
if(sx > 1){
    svgZoom.zoom(1/sx);
} else {
    svgZoom.zoom(0.9);
}

可以看到图已经非常靠近我们想要的样子了ヽ(゚∀゚)メ(゚∀゚)ノ ,但还是有美中不足的地方,在拖拽节点时,会发现连着画布一起移动了,并且节点还哆哆嗦嗦的,这明显不太行。
没有使用svg-pan-zoom时节点是可以单独拖拽的,使用了之后,svg-pan-zoom影响了jointjs的节点拖拽事件。也就是说svg-pan-zoom影响了jointjs的节点拖拽事件。
解决这种情况,只需要在svg-pan-zoom判断是否拖拽的节点,并不触发相应事件即可。

  1. svg-pan-zoom有beforePan方法的配置:

beforePan will be called with 2 attributes:

  • oldPan
  • newPan

Each of these objects has two attributes (x and y) representing current pan (on X and Y axes).

If beforePan will return false or an object {x: true, y: true} then panning will be halted. If you want to prevent panning only on one axis then return an object of type {x: true, y: false}. You can alter panning on X and Y axes by providing alternative values through return {x: 10, y: 20}.

可以看到在beforePan里返回false 或者 { x: true, y: true } 即可停止拖拽。
ps: 但是我试了{ x: true, y: true }不得行ヽ(ー_ー)ノ,但是{ x: false, y: false }是可以的。

  • 首先确定当前拖拽的是节点, 为paper添加事件,判断当前点击并拖拽的是节点
    /** 给paper添加事件 */
    paperEvent(){
        /** 确认点击的是节点 */
        this.paper.on('element:pointerdown', (cellView, evt, x, y) => {
            this.currCell = cellView;
        })
        /** 在鼠标抬起时恢复currCell为null */
        this.paper.on('cell:pointerup blank:pointerup', (cellView, evt, x, y) => {
            this.currCell = null;
        })
    }
    
  • 同时在svgPanZoom的配置里增加以下属性:
    /** 判断是否是节点的拖拽 */
    beforePan: (oldPan, newPan) => {
        if(this.currCell){
            return false;
        }
    }
    

现在这个效果是我们想要的了,d=====( ̄▽ ̄*)b

  1. 整个页面完整代码如下:
<template>
    <div class="container">
        <div id="paper"></div>
    </div>
</template>
<script>
import dagre from "dagre";
import graphlib from "graphlib";
import * as joint from "jointjs";
import '/node_modules/jointjs/dist/joint.css';
import svgPanZoom from 'svg-pan-zoom';
export default {
    data(){
        return {
            graph: null,
            paper: null,
            /** 原始数据:节点 */
            nodes: [
                { id: 1, label: 'node1' },
                { id: 2, label: 'node2' },
                { id: 3, label: 'node3' }
            ],
            /** 原始数据:连线 */
            links: [
                { from: 1, to: 2 },
                { from: 1, to: 3 }
            ],
            /** 处理后生成的节点 */
            nodeList: [],
            /** 处理后生成的连线 */
            linkList: [],
            /** 当前单元格,joint的拖动和svgpanzoom会冲突造成抖动 */
            currCell: null,
        }
    },
    methods: {
        init(){
            this.initGraph();
            this.createNode();
            this.createLink();
            this.randomLayout();
            this.svgPanZoom();
            this.paperEvent();
        },
        /** 初始化画布 */
        initGraph() {
            this.nodeList = [];
            this.linkList = [];
            let paper = document.getElementById('paper');
            this.graph = new joint.dia.Graph();
            this.paper = new joint.dia.Paper({
                dagre: dagre,
                graphlib: graphlib,
                el: paper,
                model: this.graph,
                width: '100%',
                height: 'calc(100vh - 100px)',
                background: {
                    color: '#f5f5f5'
                },
                // drawGrid: true,
                // gridSize: 20,
            });
        },
        /** 创建节点 */
        createNode(){
            this.nodes.forEach(ele => {
                let node = new joint.shapes.standard.Rectangle({
                    id: ele.id,
                    size: {
                        width: 100,
                        height: 50
                    },
                    attrs: {
                        body: {
                            fill: '#ddd',
                            stroke: 'none'
                        },
                        text: {
                            text: ele.label
                        }
                    }
                });
                this.nodeList.push(node);
            })
            this.graph.addCell(this.nodeList);
        },
        /** 创建连线 */
        createLink(){
            this.links.forEach(ele => {
                let link = new joint.shapes.standard.Link({
                    source: {
                        id: ele.from
                    },
                    target: {
                        id: ele.to
                    },
                    attrs: {
                        line: {
                            stroke: '#aaa',
                            strokeWidth: 1
                        }
                    }
                });
                this.linkList.push(link);
            })
            this.graph.addCell(this.linkList);
        },
        /** 画布节点自动布局 */
        randomLayout(){
            joint.layout.DirectedGraph.layout(this.graph, {
              dagre: dagre,
              graphlib: graphlib,
                /** 布局方向 TB | BT | LR | RL */
              rankDir: "LR",
                /** 表示列之间间隔的像素数 */
              rankSep: 200,
                /** 相同列中相邻接点之间的间隔的像素数 */
              nodeSep: 80,
                /** 同一列中相临边之间间隔的像素数 */
                edgeSep: 50
            });
        },
        /** svgpanzoom 画布拖拽、缩放 */
        svgPanZoom(){
            if(this.nodes.length){
                let svgZoom = svgPanZoom('#paper svg', {
                    /** 是否可拖拽 */
                    panEnabled: true,
                    /** 是否可缩放 */
                    zoomEnabled: true,
                    /** 双击放大 */
                    dblClickZoomEnabled: false,
                    /** 可缩小至的最小倍数 */
                    minZoom: 0.01,
                    /** 可放大至的最大倍数 */
                    maxZoom: 100,
                    /** 是否自适应画布尺寸 */
                    fit: true,
                    /** 图是否居中 */
                    center: true,
                    /** 判断是否是节点的拖拽 */
                    beforePan: (oldPan, newPan) => {
                        if(this.currCell){
                            return false;
                        }
                    }
                })
                svgZoom.setZoomScaleSensitivity(0.5);
                /** fit:true 元素数量较少时,会引起元素过度放大,当缩放率大于1时,将图像缩小为1;小于等于1时,为体现出边距更显美观,整体缩放至0.9 */
                let {sx, sy} = this.paper.scale();
                if(sx > 1){
                    svgZoom.zoom(1/sx);
                } else {
                    svgZoom.zoom(0.9);
                }
            }
        },
        paperEvent(){
            this.paper.on('element:pointerdown', (cellView, evt, x, y) => {
                this.currCell = cellView;
            })
            this.paper.on('cell:pointerup blank:pointerup', (cellView, evt, x, y) => {
                this.currCell = null;
            })
        },
    },
    mounted(){
        this.init();
    }
}
</script>

Copyright © 2024 小小憨批儿
Powered by .NET 8.0 on Kubernetes