G6实现可向下向上扩展的自定义流程图/层级图/架构图
效果:
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}