环境:Ant design pro v4 js
antV x6:1.21.7
实现以下功能:节点划线、自动布局、右键菜单
代码可能存在bug,抛砖引玉吧
参考官网demo:业务实践 | X6
右键菜单demo:https://x6.antv.vision/zh/examples/edge/tool#context-menu
import React, { useEffect, useState, useRef } from 'react'
import { Graph, Node, ToolsView } from '@antv/x6'
import dagre from 'dagre'
import { Menu, Modal, message, Dropdown } from 'antd';
import './shape';
import styles from './canvas-content.less'
import { FileAddOutlined, DeleteOutlined, DownSquareOutlined, UpSquareOutlined } from '@ant-design/icons';
/**
* 参考案例
* https://x6.antv.vision/zh/examples/showcase/practices#orgchart
*/
const CanvasContent = (props) => {
const {
data,
setKnowledgeValues
} = props;
let graph = {}
useEffect(() => {
console.log('useEffect 2', graph)
graph = new Graph({
container: document.getElementById('mind-content'),
grid: true,
interacting: {
nodeMovable: false, //节点禁止拖动
edgeMovable: false //边禁止拖动
},
selecting: {
enabled: true,//开启点击
multiple: false,//禁止多选
rubberband: false,//禁用框选
movable: false,//禁止连带移动
showNodeSelectionBox: true,//开启选择后效果
},
})
graph.zoom(-0.1)//缩放比例
data.map((info, index) => {
// debugger
const parentNode = createNode(info)
forEachChild(info, parentNode)
})
layout();
setup();
}, [data]);
/**
* 递归创建节点
* @param {*} info api data
* @param {*} parentNode X6 node
*/
const forEachChild = (info, parentNode) => {
// debugger
info.children.map((childInfo, childIndex) => {
const childNode = createNode(childInfo)
createEdge(parentNode, childNode)
forEachChild(childInfo, childNode)
})
}
// 添加节点
const createNode = (info) => {
return graph.addNode({
shape: 'mind-map-rect',
attrs: {
text: {
textWrap: {
text: info.text,
id: info.id
},
},
}
})
}
// 添加连线
const createEdge = (source, target, vertices) => {
return graph.addEdge({
shape: 'org-edge',
source: { cell: source },
target: { cell: target },
})
}
// 自动布局
const layout = () => {
const nodes = graph.getNodes()
const edges = graph.getEdges()
const g = new dagre.graphlib.Graph()
g.setGraph({ rankdir: 'LR', nodesep: 16, ranksep: 16 })
g.setDefaultEdgeLabel(() => ({}))
const width = 260
const height = 40
// debugger;
nodes.forEach((node) => {
g.setNode(node.id, { width, height })
})
edges.forEach((edge) => {
const source = edge.getSource()
const target = edge.getTarget()
g.setEdge(source.cell, target.cell)
})
dagre.layout(g)
graph.freeze()
g.nodes().forEach((id) => {
const node = graph.getCell(id)
if (node) {
const pos = g.node(id)
node.position(pos.x, pos.y)
}
})
edges.forEach((edge) => {
const source = edge.getSourceNode()
const target = edge.getTargetNode()
const sourceBBox = source.getBBox()
const targetBBox = target.getBBox()
console.log(sourceBBox, targetBBox)
const gap = targetBBox.x - sourceBBox.x - sourceBBox.width
const fix = sourceBBox.width
const x = sourceBBox.x + fix + gap / 2
edge.setVertices([
{ x, y: sourceBBox.center.y },
{ x, y: targetBBox.center.y },
])
})
graph.unfreeze()
}
// 监听自定义事件
const setup = () => {
//双击事件
graph.on('node:dblclick', ({ cell, view }) => {
const oldText = cell.attr('text/textWrap/text')
const elem = view.container.querySelector('.x6-edit-text')
if (elem == null) { return }
cell.attr('text/style/display', 'none')
if (elem) {
elem.style.display = ''
elem.contentEditable = 'true'
elem.innerText = oldText
elem.focus()
}
const onBlur = () => {
cell.attr('text/textWrap/text', elem.innerText)
cell.attr('text/style/display', '')
elem.style.display = 'none'
elem.contentEditable = 'false'
console.log('onBlur = ', elem.innerText)
}
elem.addEventListener('blur', () => {
onBlur()
elem.removeEventListener('blur', onBlur)
})
})
//右键菜单
graph.on('node:contextmenu', ({ cell, e }) => {
console.log('cell ', cell.attr('text/textWrap/text'), cell)
//default menu status
var delFlag = false, topFlag = false, downFlag = false;
if (graph.isRootNode(cell)) {
//is root node
delFlag = true, topFlag = true, downFlag = true
} else {
//查找同层的集合
const id = cell.attr('text/textWrap/id')
const sameLevelList = getSameLevel(data[0].children, id);
if (sameLevelList.length === 1) {
//同层只有1个元素,禁用上下
topFlag = true, downFlag = true
} else {
const index = sameLevelList.findIndex(info => info.id === id);
if (index === 0) topFlag = true //最上层禁用上移
else if (index === sameLevelList.length - 1) downFlag = true //最下层禁用下移
}
}
const p = graph.clientToGraph(e.clientX, e.clientY)
const menu = (
<Menu className={styles.x6Menu} onClick={(e) => handleMenuClick(e, cell)}>
<Menu.Item key="1" icon={<FileAddOutlined />}>添加</Menu.Item>
<Menu.Item key="2" icon={<DeleteOutlined />} disabled={delFlag}>删除</Menu.Item>
<Menu.Item key="3" icon={<UpSquareOutlined />} disabled={topFlag}>上移</Menu.Item>
<Menu.Item key="4" icon={<DownSquareOutlined />} disabled={downFlag}>下移</Menu.Item>
</Menu>
)
// debugger
cell.addTools([
{
name: 'contextmenu',
args: {
menu,
x: p.x,
y: p.y,
onHide() {
// this.cell.removeTools()
cell.removeTools()
},
},
},
])
})
}
/**
* 递归根据id查询当前对象
* @param {*} list
* @param {*} id
* @returns
*/
const getKnowLedgeInfo = (list, id) => {
let result = false;
list.map((item, index) => {
if (result) return;
if (item.id === id) {
list.splice(index, 1)
result = true;
return;
}
return getKnowLedgeInfo(item.children, id)
})
}
/**
* 菜单点击事件
* @param {*} event 点击事件
* @param {*} node 选中当前节点
*/
const handleMenuClick = (event, node) => {
const { key } = event;
const id = node.attr('text/textWrap/id');
if (key === '1') {
//添加节点
const newNode = createNode('New Node')
graph.freeze()
graph.addCell([newNode, createEdge(node, newNode)])
layout()
} else if (key === '2') {
//删除节点
Modal.confirm({
title: '删除知识点',
content: `确定删除 “${node.attr('text/textWrap/text')}” 和 “子节点” 吗?`,
okText: '确认',
cancelText: '取消',
onOk: () => {
graph.freeze()
const cells = graph.getSuccessors(node);//获取后续节点
graph.removeCells(cells) //删除子节点
graph.removeCell(node) //删除当前节点
layout()
},
});
} else if (key === '3') {
//上移节点
//查找同级别元素
const sameLevelList = getSameLevel(data[0].children, id);
//获取选中元素下表
const index = sameLevelList.findIndex(info => info.id === id);
//删除top元素,把选中元素添加到top位置,并返回被删除的top元素
const topElement = sameLevelList.splice(index - 1, 1, sameLevelList[index])[0]
//将被删除的top元素插入到触发节点位置
sameLevelList.splice(index, 1, topElement) //
console.log('sameLevelList ', sameLevelList)
} else if (key === '4') {
//下移节点
}
}
const getSameLevel = (dataList, id) => {
let sameLevelList = []
//根节点的子类遍历,根节点不允许上下移动
dataList.forEach((info) => {
if (info.id === id) {
sameLevelList = dataList;
return;
}
if (sameLevelList.length > 0) return
sameLevelList = getSameLevel(info.children, id)
})
return sameLevelList;
}
return (
<div className={styles.mindDiv}>
<div className={styles.mindContent} id="mind-content" />
</div>
)
}
export default CanvasContent;