官方文档写的真的是很简洁啊,有些方法属性好难找,得一边写一边摸索x6.antv.vision/zh/docs/tut…
安装
# npm
$ npm install @antv/x6 --save
# yarn
$ yarn add @antv/x6
安装完成之后,使用 import 或 require 进行引用。
import { Graph } from '@antv/x6';
开始使用
Step 1 创建容器
<div class="content">
<div class="app-stencil" ref="stencilContainer"></div>
<div class="app-content" id="flowContainer" ref="container"></div>
</div>
Step 2 准备数据
在我们真实开发中,初始画布肯定是不需要这些假数据的,我之所以给列在这里,是为了清楚它的数据格式,这样在跟后端调接口的时候,可以一起定义数据格式,方便双方开发。尤其是在做回显时,从接口拿数据进行渲染画布时,直接使用这种格式就可以了。我会在后面列出我开发时是怎么运用的,以下是官方文档提供的事例:
X6 支持 JSON 格式数据,该对象中需要有节点 nodes 和边 edges 字段,分别用数组表示:
const data = {
// 节点
nodes: [
{
id: 'node1', // String,可选,节点的唯一标识
x: 40, // Number,必选,节点位置的 x 值
y: 40, // Number,必选,节点位置的 y 值
width: 80, // Number,可选,节点大小的 width 值
height: 40, // Number,可选,节点大小的 height 值
label: 'hello', // String,节点标签
},
{
id: 'node2', // String,节点的唯一标识
x: 160, // Number,必选,节点位置的 x 值
y: 180, // Number,必选,节点位置的 y 值
width: 80, // Number,可选,节点大小的 width 值
height: 40, // Number,可选,节点大小的 height 值
label: 'world', // String,节点标签
},
],
// 边
edges: [
{
source: 'node1', // String,必须,起始节点 id
target: 'node2', // String,必须,目标节点 id
},
],
};
Step 3 渲染画布
首先,我们需要创建一个 Graph 对象,并为其指定一个页面上的绘图容器,通常也会指定画布的大小。
import { Graph, Shape, Addon, FunctionExt } from '@antv/x6'// 使用 CDN 引入时暴露了 X6 全局变量
// const { Graph } = X6
export default defineComponent({
const imageShapes = [ //左侧拖拽样式 { body: { fill: "#EFF4FF", stroke: "#5F95FF", }, label: { text: state.collectLabel, fill: '#5F95FF', }, image: require('/src/assets/Scheduler/rowCook.svg'), type:'ruleSet', //每一个都自定义一个type }, { // ...... 省略其他 }, ]
let graph = null;
const init= () => {
graph = new Graph({ // 新建画布
container: document.getElementById('flowContainer'),
grid: true,
scroller: {
enabled: true,
pageVisible: false,
pageBreak: false,
},
snapline: {
enabled: true,
sharp: true,
},
mousewheel: {
enabled: true,
modifiers: ["ctrl", "meta"],
minScale: 0.5,
maxScale: 2,
},
// 画布调整
selecting: {
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true,
},
// 连线规则
connecting: {
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
allowMulti: false, // 当设置为 false 时,在起始和终止节点之间只允许创建一条边
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 false。
sourceAnchor: { // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
name: 'bottom',
args: {
dx: 0,
},
},
targetAnchor: { // 当连接到节点时,通过 targetAnchor 来指定目标节点的锚点。
name: 'top',
args: {
dx: 0,
},
},
connectionPoint: 'anchor', // 指定连接点,默认值为 boundary。
connector: 'algo-edge', // 连接器将起点、路由返回的点、终点加工为 元素的 d 属性,决定了边渲染到画布后的样式,默认值为 normal。
createEdge() {
return graph.createEdge({
attrs: {
line: {
strokeDasharray: '5 5',
stroke: '#808080',
strokeWidth: 1,
targetMarker: {
name: 'block',
args: {
size: '6',
},
},
},
},
})
},
validateMagnet({ magnet }) {
return magnet.getAttribute('port-group') !== 'in'
},
validateConnection({
sourceView,
targetView,
sourceMagnet,
targetMagnet
}) {
if (sourceView === targetView) {
return false;
}
if (!sourceMagnet) {
return false;
}
// 只能连接到输入链接桩
if (
!targetMagnet ||
targetMagnet.getAttribute("port-group") !== "in"
) {
return false;
}
return true;
}, // 当停止拖动边的时候根据 validateEdge 返回值来判断边是否生效,如果返回 false, 该边会被清除。
validateEdge({ edge }) {
const { source, target } = edge
return true
}
},
});
// graph.isPannable() // 画布是否可以平移
// graph.enablePanning() // 启用画布平移
graph.centerContent();
/******************************** 左侧模型栏 ****************************/
const stencil = new Stencil({
title: "数据集成",
target: graph,
search: false, // 搜索
collapsable: true,
stencilGraphWidth: 300,
stencilGraphHeight: 600,
groups: [
{
name: "processLibrary",
title: "dataSource",
},
],
layoutOptions: {
dx: 30,
dy: 20,
columns: 1,
columnWidth: 130,
rowHeight: 100,
},
});
proxy.$refs.stencilContainer.appendChild(stencil.container)
// 初始化图形
const ports = {
groups: {
in: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#108ee9',
strokeWidth: 2,
fill: '#fff',
style: {
visibility: "hidden",
},
}
}
},
out: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#31d0c6',
strokeWidth: 2,
fill: '#fff',
style: {
visibility: "hidden",
},
}
}
}
},
items: [ //后缀添加in和out是因为弄了上下两个链接桩,在回显接口返回的数据时,可以正确显示连线的上下连接位置
{
id: state.currentCode + '_in',
group: 'in',
},
{
id: state.currentCode + '_out',
group: 'out',
},
],
}
//设计画布左侧节点样式
Graph.registerNode(
'custom-node',
{
inherit: 'rect',
width: 140,
height: 76,
attrs: {
body: {
strokeWidth: 1,
},
image: {
width: 16,
height: 16,
x: 12,
y: 6,
},
text: {
refX: 40,
refY: 15,
fontSize: 15,
'text-anchor': 'start',
},
label: {
text: 'Please nominate this node',
refX: 10,
refY: 30,
fontSize: 12,
fill: 'rgba(0,0,0,0.6)',
'text-anchor': 'start',
textWrap: {
width: -10, // 宽度减少 10px
height: '70%', // 高度为参照元素高度的一半
ellipsis: true, // 文本超出显示范围时,自动添加省略号
breakWord: true, // 是否截断单词
}
},
},
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'image',
selector: 'image',
},
{
tagName: 'text',
selector: 'text',
},
{
tagName: 'text',
selector: 'label',
},
],
data: {},
relation: {},
ports: { ...ports },
},
true,
)
const imageNodes = imageShapes.map((item) =>
graph.createNode({
shape: 'custom-node',
attrs: {
image: {
'xlink:href': item.image,
},
body: item.body,
text: item.label,
},
}),
)
stencil.load(
imageNodes,
"processLibrary"
);
graph.toJSON()
//绑定事件
graph.on('node:added', ({ node }) => {
state.currentCode = node.id //每个节点都有一个唯一id
})
// 鼠标进入节点-节点显示连接桩
graph.on("node:mouseenter",FunctionExt.debounce(() => {
const ports = container.querySelectorAll(".x6-port-body");
showPorts(ports, true);
}),
500
);
// 鼠标进入节点-节点删除操作
graph.on("node:mouseenter", ({ node }) => {
node.addTools({
name: "button-remove",
args: {
x: 0,
y: 0,
offset: { x: 10, y: 10 },
},
});
});
// 鼠标离开节点
graph.on("node:mouseleave", ({ node }) => {
const ports = container.querySelectorAll(".x6-port-body");
showPorts(ports, false); //隐藏连接桩
node.removeTools(); // 隐藏删除按钮
});
// 鼠标进入线-线删除操作
graph.on("edge:mouseenter", ({ edge }) => {
edge.addTools([
"target-arrowhead",
{
name: "button-remove",
args: {
distance: -30,
},
},
]);
});
//这个方法我没用上,知道的小伙伴也可以告诉我连线的删除
graph.on("edge:removed", ({ edge, options }) => {
// if (!options.ui) {
// return;
// }
// const cellId = edge.getTargetCellId()
// const target = graph.getCellById(cellId)
// target && target.setPortProp(target.id, 'connected', false)
});
// 鼠标离开线-线删除操作
graph.on("edge:mouseleave", ({ edge }) => {
edge.removeTools();
});
graph.on('node:change:data', ({node}) => {
node.data = eachNodeData
})
graph.bindKey("backspace", () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
}
});
//双击节点打开节点配置 --- 最主要用到的方法
graph.on("cell:dblclick", ({ node, cell }) => {
const typeList = graph.getNodes().map(x => ({ //拿到所有节点 type\id\name
code:x.id,
type:x.store.data.attrs.type,
name:x.store.data.attrs.label.text
}))
const relationShip = graph.getEdges().map(x => ({ //获取连线关系
child:x.target.cell,
parent:x.source.cell
}))
//寻找当前节点是否有连线且拿到父节点
let fatherCode = []
relationShip.map((item)=>{
if(item.child==node.id){
fatherCode.push(item.parent)
}
})
// 双重for循环拿到父节点信息
let result = []
for(let i = 0; i < fatherCode.length; i++) {
let tempArr1 = fatherCode[i]
for(let j = 0; j < typeList.length; j++) {
let tempArr2 = typeList[j]
if(tempArr2.code == tempArr1){
result.push(tempArr2)
break;
}
}
}
state.resultShip=result; //父节点集合 这个是因为我们的连线之间存在父子关系,需要用父子关系来控制显示对应的数据,没这个需求的不用加上述操作
state.currentCode = node.id //当前节点id
//打开对应弹框 可以用text判断,也可以用自定义的type去区分
let text = node.getAttrs().text.text
if(text=='规则集'){
state.visibleRule=true;
if(allData.ruleSetList.length>=1){
let idx= allData.ruleSetList.findIndex((itm) => itm.code == node.id)
if(idx > -1){
state.currentRuleSet=allData.ruleSetList[idx]
}else{
state.currentRuleSet={}
}
}
}else if(text=='决策'){
state.visibleTactics=true;
// ............. 省略
}else if(text=='表达式'){
state.visibleExpression=true;
// ............. 省略
}
})
//删除节点及已经保存的弹框数据 --- 最主要用到的方法
graph.on("node:removed", ({ node, options }) => {
if(node.store.data.attrs.type=="expression"){
let idx= allData.expressionList.findIndex((itm) => itm.code== node.id)
if(idx > -1){
allData.expressionList.splice(idx,1)
}
}else if(node.store.data.attrs.type=="ruleSet"){
//删除规则集
// ............. 省略
}else if(node.store.data.attrs.type=="plot"){
//删除策略
// ............. 省略
}
})
// 控制连接桩显示/隐藏
const showPorts = (ports, show) => {
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? "visible" : "hidden";
}
}
})
Step 4 根据接口回显画布
const getData = () =>{
const cells = []
const location = JSON.parse(state.canvasPos)
//节点
location.locationList.map((data)=>{
cells.push(
graph.addNode({
id: data.nodeCode,
x: data.axisX,
y: data.axisY,
shape: 'custom-rect',
attrs: {
label: {
text: data.nodeName,
},
text: {
text: data.paramType === "expression" ? "表达式" : data.paramType === "ruleSet" ? "规则集" : "决策",
fill: '#5F95FF',
},
body: {
fill: "#EFF4FF",
stroke: "#5F95FF",
},
type:data.paramType,
}
}),
)
})
//连线
location.relationList.map((data)=>{
cells.push(
graph.addEdge({
source: {cell:data.parentNodeDTO.parentCode,port:data.parentNodeDTO.parentFlag },
target: {cell:data.childNodeDTO.childCode,port:data.childNodeDTO.childFlag},
attrs: {
line: {
stroke: '#A2B1C3',
strokeWidth: 2,
targetMarker: {
name: 'block',
width: 12,
height: 8,
},
},
},
zIndex: 0,
shape: 'edge',
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
anchor: 'center',
connectionPoint: 'anchor',
}),
)
})
graph.resetCells(cells)
graph.centerContent()
}