G6实现可向下向上扩展的自定义流程图/层级图/架构图

发布时间 2023-09-01 11:07:28作者: 潇可爱❤

参考原文链接:https://www.cnblogs.com/demeter/p/16821514.html

效果:

index.html:cdn引入 

<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.7.1/dist/g6.min.js"></script>

页面代码:

graph
<template>
    <div id="container">
        <div id="chartView"></div>
    </div>
</template>
<script>
import G6 from '@antv/g6'
import { getPathWithBorderRadius } from '@/utils/G6'

export default {
    data() {
        return {
            curTaskNode: {
                id: '-1',
                taskId: '10000',
                name: '这是中心任务abcdefg',
                level: 'base', // base, parent, child
                textColor: '#46BB17',
                hasParent: true, // 默认有父节点
                hasChild: true, // 默认有子节点
                parentCollapse: true, // 父节点是否折叠
                childCollapse: true, // 子节点是否折叠
                childIds: [],
                parentIds: []
            },
            autoIncrementId: 1,// 自增id,这里是因为前端mock数据,所以使用,正常情况下这个id由后端数据提供
            nodesData: [],// 所有的节点集合
            edgesData: [],// 边数据
            graphObj: ''
        }
    },
    mounted() {
        // 初始化节点/边
        this.initGraphData()
        // 绘制
        const container=document.getElementById('chartView')
        const parentContainer=document.getElementById('container')
        const width=parentContainer.offsetWidth-40
        const height=parentContainer.offsetHeight-180 || 500
        const minimap=new G6.Minimap()
        const graph=new G6.Graph({
            container: container,
            width,
            height,
            plugins: [minimap],
            layout: {
                type: 'dagre',
                rankdir: 'LR',
                nodesepFunc: ()=>1,
                ranksepFunc: ()=>1,
                nodesep: 40, // 节点间距
                ranksep: 200, // 层间距
                controlPoints: true,
                preventOverlap: true //防重叠
            },
            defaultNode: {
                type: 'dispatch-rect'
            },
            defaultEdge: { //默认状态下边线样式设计
                type: 'hvh',
                style: {
                    stroke: '#46BB17',
                    endArrow: {
                        path: G6.Arrow.triangle(),
                        fill: '#46BB17'
                    }
                }
            },
            nodeStateStyles: {
                selected: {
                    stroke: '#5394ef',
                    fill: '#5394ef'
                },
            },
            modes: {
                default: [
                    // {default: ['drag-canvas', 'zoom-canvas', 'drag-node'] // 允许拖拽画布、放缩画布、拖拽节点
                    'drag-canvas','zoom-canvas'
                ]
            },
            fitCenter: true
        })
        this.nodesData.push(this.curTaskNode)
        graph.data({nodes: [this.curTaskNode],edges: []})
        graph.render()
        graph.on('node:click',evt=>{
            // 在注册节点的时候每个图形都有个name属性,可以根据name属性确定用户点击的事具体哪个图形,进行相应的操作
            const name=evt.target.get('name')
            if(name==='name-text'){ // 跳转case
                window.open('https://antv-g6.gitee.io/zh','_blank')
            }else if(name==='p-marker' || name==='c-marker'){
                const model=evt.item.getModel()
                this.updateGraph(name,model)
            }
        })
        this.graphObj=graph
        if(typeof window!=='undefined'){
            window.onresize=()=>{
                if(!this.graphObj || this.graphObj.get('destroyed')){
                    return
                }
                if(!container || !container.scrollWidth || !container.scrollHeight){
                    return
                }
                this.graphObj.changeSize(container.scrollWidth,container.scrollHeight)
            }
        }
    },
    methods: {
        // 随机取[1, 4]的正整数
        getRandomIntInclusive(min=1,max=4) {
            min=Math.ceil(min)
            max=Math.floor(max)
            return Math.floor(Math.random()*(max-min+1))+min //含最大值,含最小值
        },
        // 得到目标数组中除了当前数组以外的数组
        getDiffData(originArr,targetArr) {
            const diffArr=[]
            originArr.forEach(item=>{
                if(!targetArr.includes(item)){
                    diffArr.push(item)
                }
            })
            return diffArr
        },
        /**
         * 点击收起/展开的事件
         * @param {*} evtName 节点名称
         * @param {*} model 当前点击的数据模型
         */
        updateGraph(evtName,model) {
            let newNodesData=[] // 新的节点数据
            let newEdgesData=[] // 新的边数据
            if(evtName==='p-marker'){
                if(model.parentCollapse){ // 目前是折叠状态,需要展开
                    newEdgesData=[].concat(this.edgesData)
                    const newAddNodes=[]
                    const newIds=[]
                    for(let i=0; i<5; i++){ // 固定添加两条数据,之后改成请求
                        // newNodesData[curNodeIndex].parentIds.push(String(autoIncrementId));
                        newIds.push(String(this.autoIncrementId))
                        const randomType=this.getRandomIntInclusive()
                        console.log(randomType,'randomType')
                        newAddNodes.push({
                            id: String(this.autoIncrementId),
                            name: `测试新增父name${ this.autoIncrementId }`,
                            taskId: `10000${ this.autoIncrementId }`,
                            parentIds: [],
                            childIds: model.childIds.concat([model.id]),
                            level: 'parent',
                            hasChild: true,
                            hasParent: true,
                            parentCollapse: true,
                            childCollapse: false,
                            textColor: '#fff'
                        })
                        newEdgesData.push({
                            target: model.id,
                            source: String(this.autoIncrementId)
                        })
                        this.autoIncrementId++
                    }
                    const curNodeIndex=this.nodesData.findIndex(item=>item.id===model.id) // 当前点击的节点在节点数据中的下标
                    this.nodesData[curNodeIndex].parentCollapse=false
                    this.nodesData[curNodeIndex].parentIds=this.nodesData[curNodeIndex].parentIds.concat(newIds)
                    this.nodesData.concat(newAddNodes).forEach(node=>{
                        if(model.childIds.includes(node.id)){
                            node.parentIds=node.parentIds.concat(newIds)
                        }
                        newNodesData.push(node)
                    })
                }else{ // 目前是展开状态,需要折叠
                    // 所有以当前的所有父节点为源头的箭头指向的箭头都要去掉
                    newEdgesData=this.edgesData.filter(edge=>!model.parentIds.includes(edge.source))
                    this.nodesData.forEach(node=>{
                        if(!node.childIds.includes(model.id)){ // 所有子节点中有当前节点的节点都要去掉,并且留下的父节点也要去掉要删除的节点数据
                            node.parentIds=this.getDiffData(node.parentIds,model.parentIds)
                            newNodesData.push(node)
                        }
                    })
                    const curNodeIndex=newNodesData.findIndex(item=>item.id===model.id) // 当前点击的节点在节点数据中的下标
                    newNodesData[curNodeIndex].parentCollapse=true
                }
            }else if(evtName==='c-marker'){
                if(model.childCollapse){ // 目前是折叠状态,需要展开
                    newEdgesData=[].concat(this.edgesData) // 边数据
                    const newAddNodes=[] // 新增加的子节点数据
                    const newIds=[]
                    for(let i=0; i<10; i++){ // 固定添加两条数据,之后改成请求
                        newIds.push(String(this.autoIncrementId))
                        newAddNodes.push({
                            id: String(this.autoIncrementId),
                            name: `测试新增子name${ this.autoIncrementId }`,
                            taskId: `10000${ this.autoIncrementId }`,
                            parentIds: model.parentIds.concat([model.id]),
                            childIds: [],
                            level: 'child',
                            hasChild: true,
                            hasParent: true,
                            parentCollapse: false,
                            childCollapse: true,
                            textColor: '#fff'
                        })
                        newEdgesData.push({
                            target: String(this.autoIncrementId),
                            source: model.id
                        })
                        this.autoIncrementId++
                    }
                    const curNodeIndex=this.nodesData.findIndex(item=>item.id===model.id) // 当前点击的节点在节点数据中的下标
                    this.nodesData[curNodeIndex].childCollapse=false
                    this.nodesData[curNodeIndex].childIds=this.nodesData[curNodeIndex].childIds.concat(newIds)
                    this.nodesData.concat(newAddNodes).forEach(node=>{
                        if(model.parentIds.includes(node.id)){
                            node.childIds=node.childIds.concat(newIds)
                        }
                        newNodesData.push(node)
                    })
                }else{ // 目前是展开状态,需要折叠
                    // 去掉所有以当前的所有子节点中任意一个为目标点的箭头
                    newEdgesData=this.edgesData.filter(edge=>!model.childIds.includes(edge.target))
                    this.nodesData.forEach(node=>{
                        if(!node.parentIds.includes(model.id)){
                            node.childIds=this.getDiffData(node.childIds,model.childIds)
                            newNodesData.push(node)
                        }
                    })
                    const curNodeIndex=newNodesData.findIndex(item=>item.id===model.id) // 当前点击的节点在节点数据中的下标
                    newNodesData[curNodeIndex].childCollapse=true
                }
            }
            this.nodesData=newNodesData
            this.edgesData=newEdgesData
            console.log('newNodesData--??',newNodesData)
            console.log('newEdgesData--?️?️',newEdgesData)
            this.graphObj.changeData({nodes: newNodesData,edges: newEdgesData})
            this.graphObj.fitCenter()
        },
        // 初始化节点/边
        initGraphData() {
            G6.registerNode(
                'dispatch-rect',
                {
                    drawShape: (cfg,group)=>{
                        const {
                            name='',
                            taskId='',
                            level='',
                            hasParent=false,
                            hasChild=false,
                            textColor
                        }=cfg
                        console.log('cfg',cfg)
                        // 矩形框
                        const rectConfig={
                            x: -90,
                            y: -30,
                            width: 180,
                            height: 60,
                            lineWidth: 1,
                            fontSize: 12,
                            fill: '#46BB17',
                            radius: 4,
                            opacity: 1,
                            stroke: textColor
                        }
                        const rect=group.addShape('rect',{
                            attrs: {
                                ...rectConfig
                            }
                        })
                        // 当前事件id
                        group.addShape('text',{
                            attrs: {
                                x: -76,
                                y: -8,
                                text: `id:${ taskId }`,
                                fontSize: 12,
                                fill: '#fff',
                                cursor: 'pointer'
                            },
                            name: 'id-text'
                        })
                        // 当前事件名称
                        group.addShape('text',{
                            attrs: {
                                x: -76,
                                y: 10,
                                text: name.length>14 ? `${ name.substring(0,14) }...` : name,
                                fontSize: 14,
                                fill: '#fff',
                                cursor: 'pointer',
                                textBaseline: 'middle'
                            },
                            name: 'name-text'
                        })
                        // 操作上级的marker
                        if((level==='base' || level==='parent') && hasParent){
                            group.addShape('circle',{
                                attrs: {
                                    x: -90,
                                    y: 0,
                                    r: 5,
                                    fill: '#e6df8b',
                                    cursor: 'pointer'
                                },
                                name: 'p-marker'
                            })
                        }
                        // 操作下级的marker
                        if((level==='base' || level==='child') && hasChild){
                            group.addShape('circle',{
                                attrs: {
                                    x: 90,
                                    y: 0,
                                    r: 5,
                                    fill: '#5ce5b7',
                                    cursor: 'pointer'
                                },
                                name: 'c-marker'
                            })
                        }
                        return rect
                    }
                    // update: (cfg, item) => {
                    //   console.log(cfg, 'cfg updated', item);
                    // }

                },
                'rect'
            )
            // 自定义连接线
            G6.registerEdge('hvh',{
                draw(cfg,group) {
                    const startPoint=cfg.startPoint
                    const endPoint=cfg.endPoint
                    const path=getPathWithBorderRadius(startPoint,endPoint)
                    return group.addShape('path',{
                        attrs: {
                            stroke: '#ccc',
                            path,
                            endArrow: {
                                path: G6.Arrow.triangle(6,8,0),
                                d: 0,
                                fill: '#ccc',
                                stroke: '#ccc'
                            }
                        },
                        // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
                        name: 'path-shape'
                    })
                }
            })
        }
    }
}
</script>
<style scoped lang="scss">
#container {
    width: 100%;
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.color-tip-container {
    display: flex;
    padding: 12px;
}

.color-tip-item {
    margin-right: 8px;
    padding: 4px 10px;
    border-radius: 4px;
}
</style>

自定义连接线:

function getDistance(p1, p2) {
    return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
}
/** 获取圆角控制点,用来绘制圆角 */
function getBorderRadiusPoints(p0, p1, p2, r) {
    const d0 = getDistance(p0, p1)
    const d1 = getDistance(p2, p1)

    if (d0 < r) {
        r = d0
    }

    if (d1 < r) {
        r = d1
    }

    const ps = {
        x: p1.x - (r / d0) * (p1.x - p0.x),
        y: p1.y - (r / d0) * (p1.y - p0.y)
    }
    const pt = {
        x: p1.x - (r / d1) * (p1.x - p2.x),
        y: p1.y - (r / d1) * (p1.y - p2.y)
    }
    return [ps, pt]
}
/** 获取附带圆角的完整path */
function getPathWithBorderRadius(startPoint, endPoint) {
    const controlPoints = [
        startPoint,
        {
            x: endPoint.x / 3 + (2 / 3) * startPoint.x,
            y: startPoint.y
        },
        {
            x: endPoint.x / 3 + (2 / 3) * startPoint.x,
            y: endPoint.y
        },
        endPoint
    ]
    let path = [['M', startPoint.x, startPoint.y]]
    controlPoints.forEach((currentPoint, idx) => {
        const p1 = controlPoints[idx + 1]
        const p2 = controlPoints[idx + 2]
        if (!p1 || !p2) return
        // 在中点增加一个矩形,注意矩形的原点在其左上角
        const radiusPoint = getBorderRadiusPoints(currentPoint, p1, p2, 10)
        const [ps, pt] = radiusPoint
        path = path.concat([
            ['L', ps.x, ps.y], // 三分之一处
            ['Q', p1.x, p1.y, pt.x, pt.y], // 三分之一处
            ['L', pt.x, pt.y]
        ])
    })
    path.push(['L', endPoint.x, endPoint.y])
    return path
}

export {getPathWithBorderRadius}