在vue中,使用JointJS 3.x创建创建树状结构 并且 节点可以自定义HTML内容

159 阅读2分钟

这个示例是一个vue组件,在vue项目中安装JointJS 3.x 和 JQuery 过后就可以在vue项目直接使用这个示例。

我在分享的代码中添加了注释,对我分享的这段代码有什么修改意见或者有什么疑问请发表至评论区。

谢谢<.(=^_^=).>

微信图片_20231127114532.png

在vue环境下安装 jointjs 和 jquery

npm install jquery --save 
npm install jointjs

这个示例中使用的模拟数据是 ‘arr’

通过数据 id和parentBox 来手动算出节点的层级关系

parentBox:-1 是第一层, 第二层的 parentBox等于的一层的 id

每个节点的位置也是手动算出来的(可以不用手动算,有相关的自动布局的方法)

节点大小也是根据视窗大小手动算出的

<!-- 这是一个完整的 JointJS 自定义HTML节点的一个demo -->
<template>
    <div id="paper-html-elements" ref="canvas"></div>
</template>

<script>
import $ from 'jquery';
import * as joint from 'jointjs';
window.joint = joint;

export default {
    data() {
        return {
            graph: null,
            paper: null, // jointjs 画布(jointjs创建的内容都在这个画布上显示)
            customDivContent: '', // 新增 customDivContent 数据
            arr: [], // 存入加载的数据
            allNode: [], // 存入节点
            allLink: [], // 存入link连接线
        }
    },
    mounted() {
        this.init();
    },
    methods: {
        init() { // 初始加载
            let namespace = joint.shapes;
            // graph 创建形状
            this.graph = new joint.dia.Graph({}, { cellNamespace: namespace });
            // paper 创建纸张
            this.paper = new joint.dia.Paper({
                el: this.$refs.canvas, // 绑定放画布容器
                width: 650, // 画布宽度
                height: 400, // 画布高度
                gridSize: 1,
                model: this.graph, 
                cellViewNamespace: namespace,
            });
            // 创建可以自定义html的方法
            joint.shapes.html = {};
            joint.shapes.html.Element = joint.shapes.basic.Rect.extend({
                defaults: joint.util.defaultsDeep(
                    {
                        type: 'html.Element',
                        attrs: {
                            rect: { stroke: 'none', 'fill-opacity': 0 },
                        },
                    },
                    joint.shapes.basic.Rect.prototype.defaults
                ),
            });
      
            joint.shapes.html.ElementView = joint.dia.ElementView.extend({
                template: `<div class="html-element"><div class="custom-div" v-html="customDivContent"></div></div>`,

                initialize() {
                    joint.dia.ElementView.prototype.initialize.apply(this, arguments);
                    this.$box = $(this.template); // 使用 jQuery 直接创建节点
                },
                render: function () {
                    joint.dia.ElementView.prototype.render.apply(this, arguments);
                    this.paper.$el.prepend(this.$box);
                    this.updateBox();
                    return this;
                },
                updateBox: function () {
                    var bbox = this.model.getBBox();
                    this.$box.find('.custom-div').html(this.model.get('customDivContent')); // 更新 customDiv 的内容
                    this.$box.css({
                        width: bbox.width,
                        height: bbox.height,
                        left: bbox.x,
                        top: bbox.y,
                        transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)',
                    });
                },
                removeBox: function (evt) {
                    this.$box.remove();
                },
            });

            this.nodeData() // 加载数据
            this.createNode() // 创建节点
            this.createLink() // 创建连接线
            // this.showNode() // 把节点和连接线放到画布中

        },
        nodeData() { // 加载的数据
            this.arr = [ // 模仿api传过来的数据
                {parentBox: -1, id: 1},
                {parentBox: 1, id: 2},
                {parentBox: 1, id: 3},
                {parentBox: 2, id: 4},
                {parentBox: 2, id: 5},
                {parentBox: 3, id: 6},
                {parentBox: 4, id: 7},
                {parentBox: -1, id: 8},
                {parentBox: 8, id: 9},
                {parentBox: 9, id: 10},
                {parentBox: 10, id: 11},
                {parentBox: 10, id: 12}
            ]
            // 此处手动算出层级关系
            this.arr.forEach(count => {
                count.ceng = 1
                this.arr.forEach(item => {
                    if(count.parentBox == item.id) {
                        count.ceng = item.ceng + 1

                    }
                })
            })
        },
        createNode() { // 创建自定义的节点

            // 获取内容可视区 高 宽
            let pageWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
            let pageHeight = window.innerHeight || document.documentElement.clientWidth || document.body.clientHeight;
            let xAxis = pageWidth * 0.05;
            let yAxis = 0;
            let rect = ''
            this.arr.forEach((item,i) => {
                
                // 此处手动算出每个节点位置 =^_^=
                if(item.ceng == 1 && item.parentBox == -1) {
                    xAxis = 0.05 * pageWidth
                    if(yAxis == 0) {
                        yAxis += pageHeight * 0.04
                    } else {
                        yAxis += pageHeight * 0.16
                    }  
                } else if(item.ceng-this.arr[i-1].ceng == 1) {
                    xAxis = 0.05 * pageWidth
                    yAxis += pageHeight * 0.14
                } else if(item.ceng-this.arr[i-1].ceng == 0) {
                    xAxis += pageWidth * 0.18
                };

                // 创建节点
                rect = new joint.shapes.html.Element({
                    position: { x: xAxis, y: yAxis },
                    size: { width: pageWidth * 0.14, height: pageHeight * 0.1 },   
                });

                
                // 创建节点内自定义的html内容
                // 设置 customDivContent 的值
                this.customDivContent = `<div class="cusNode" data-id="${item.id}">
                    <div class="html-content">${item.ceng},${item.id},${item.parentBox}</div>
                    </div>`;
                rect.set('customDivContent', this.customDivContent);
                this.graph.addCells(rect); // 加载节点
                rect.findView(this.paper).updateBox(); // 调用视图对象的 updateBox 方法

                // 传出定义的节点内容
                this.allNode.push({
                    id: item.id,
                    parentBox: item.parentBox,
                    ceng: item.ceng,
                    rect: rect,
                });
                
                // 重置容器
                rect = '';

            })
        },
        createLink() { // 创建Link连接线
            // 给数据排好队,以便创建连接线
            let locat = this.allNode
            let newData = []
            let groupData = []
            locat.forEach((item,i) => {
                if(locat[i+1] != undefined && locat[i+1].parentBox != -1) {
                    groupData.push(item)
                } else if(locat[i+1] == undefined || locat[i+1].parentBox == -1) {
                    groupData.push(item)
                    newData.push(groupData)
                    groupData = []
                }                
            })

            let source = {}
            newData.forEach(count => {
                
                count.forEach((item,i) => {
                    // console.log(item);
                    if(i >= 1) {
                        count.forEach(k => {
                            if(item.ceng == k.ceng+1 && item.parentBox == k.id) {
                                source.id = k.rect.id
                            }
                        })
                        // 传出定义的连接线
                        this.allLink.push((new joint.shapes.standard.Link({
                            source: {id: source.id},
                            target: {id: item.rect.id},
                            router: { // 自定义连接线样式
                                name: 'metro' , //设置连线弯曲样式 manhattan 为直角 metro斜角
                                args: {
                                    startDirections: ['bottom'],
                                    endDirections: ['top'],
                                    step: 18,
                                    
                                },
                            },
                        })).attr({ // 自定义连接线样式
                            line: { // selector for the visible <path> SVGElement
                                stroke: '#0bb2b862', // SVG attribute and value
                                strokeWidth: 5,
                                targetMarker: {
                                    type: 'fill',
                                    'stroke-width': 0,
                                }
                            }
                        })); 
                    }
                })
                // 重置容器
                source = {}
            })

            // 加载Link连接线
            this.graph.addCells(this.allLink)

        },

    }
}
</script>

<style>
#paper-html-elements {
    position: relative;
    border: 1px solid gray;
    display: inline-block;
    background: transparent;
    overflow: hidden;
}
#paper-html-elements svg {
    background: transparent;
}
#paper-html-elements svg .link {
    z-index: 2;
}

.custom-div {
    position: absolute;
    margin: 0px;
    padding: 0px;
    width: 100%;
    height: 100%;
    pointer-events: none;
    -webkit-user-select: none;
    background-color: #ECF0F1;
    border: 1px solid #BDC3C7;
}
.html-element {
    position: absolute;
    width: 100%;
    height: 100%;
    background: #3498DB;
    pointer-events: none;
    -webkit-user-select: none;
    border-radius: 4px;
    border: 2px solid #2980B9;
    box-shadow: inset 0 0 5px black, 2px 2px 1px gray;
    box-sizing: border-box;
    z-index: 2;
}
.html-element div {
    pointer-events: auto;
}
.cusNode {
    width: 100%;
    height: 100%;
}

</style>