首先用AntV X6官网的一句简介了解一下什么是X6
X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。
知道了X6是什么,那么我们就要开始使用了
首先得确定框架,其次就要安装X6
由于项目是vue2的,所以选择的框架为vue2,当然自己也在vue3中写了一版,如有需要vue3的请私信
<template>
<div class="container1" style="width=100%; height=100%">
<div id="container"></div>
</div>
<div v-for="(item, index) in draList" :key="index" class="btn" @mousedown="startDrag(item, $event)">
<div class="box">{{ item }}</div>
</div>
</template>
前提安装 npm install @antv/x6 @antv/x6-plugin-snapline @antv/x6-plugin-keyboard
<script>
import { Graph, Shape } from "@antv/x6";
import { Snapline } from "@antv/x6-plugin-snapline";
import { Keyboard } from "@antv/x6-plugin-keyboard";
import { startDragToGraph } from "../../utils/drags";
Graph.registerNode(
"lane",
{
inherit: "rect",
markup: [
{
tagName: "rect",
selector: "body",
},
{
tagName: "rect",
selector: "name-rect",
},
{
tagName: "text",
selector: "name-text",
},
],
attrs: {
body: {
fill: "#F8F8F8",
stroke: "#f8f8f8",
strokeWidth: 0.5,
},
"name-rect": {
width: 100,
height: 30,
fill: "#f8f8f8",
stroke: "#f8f8f8",
strokeWidth: 0.5,
// x: -1,
},
"name-text": {
ref: "name-rect",
refY: 30,
refX: 85,
textAnchor: "middle",
// fontWeight: "bold",
fill: "#000",
fontSize: 17,
},
},
},
true
);
Graph.registerNode(
"lane-rect",
{
inherit: "rect",
width: 100,
height: 60,
attrs: {
body: {
strokeWidth: 1,
stroke: "#5F95FF",
fill: "#EFF4FF",
},
text: {
fontSize: 12,
fill: "#262626",
},
},
},
true
);
Graph.registerNode(
"lane-polygon",
{
inherit: "polygon",
width: 80,
height: 80,
attrs: {
body: {
strokeWidth: 1,
stroke: "#5F95FF",
fill: "#EFF4FF",
refPoints: "0,10 10,0 20,10 10,20",
},
text: {
fontSize: 12,
fill: "#262626",
},
},
},
true
);
Graph.registerEdge(
"lane-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "#A2B1C3",
strokeWidth: 2,
},
},
label: {
attrs: {
label: {
fill: "#A2B1C3",
fontSize: 12,
},
},
},
},
true
);
export default {
data() {
return {
delCell: {},
json: "",
graph: {},
celll: "",
draList: ["xxxx艺", "xxxx运"],
ruleForm: { name: "", count: "" },
dataPlacer: [
{
id: "1",
shape: "lane",
width: 170,
height: 680,
pointerEvents: "none",
position: {
x: 15,
y: 0,
},
label: "主线程",
},
{
id: "2",
shape: "lane",
width: 170,
height: 680,
position: {
x: 230,
y: 0,
},
data: {
disableMove: false,
},
label: "并行路线",
},
{
id: "3",
shape: "lane",
width: 170,
height: 680,
position: {
x: 450,
y: 0,
},
data: {
disableMove: false,
},
label: "并行路线",
},
{
id: "4",
shape: "lane",
width: 170,
height: 680,
position: {
x: 670,
y: 0,
},
data: {
disableMove: false,
},
label: "并行路线",
},
{
id: "4.5",
shape: "lane",
width: 170,
height: 680,
position: {
x: 890,
y: 0,
},
data: {
disableMove: false,
},
label: "并行路线",
},
// 开始预留空位
{
id: "5",
shape: "rect",
width: 140,
height: 42,
position: {
x: 30,
y: 90,
},
data: {
disableMove: false,
},
label: "起点",
fontSize: 20,
attrs: {
magnet: false,
body: {
stroke: "#d9d9d9",
magnet: false,
nodeMovable: false,
strokeDasharray: "5,5",
},
},
parent: "1",
},
{
id: "6",
shape: "rect",
width: 140,
height: 42,
position: {
x: 30,
y: 190,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "1",
},
{
id: "7",
shape: "rect",
width: 140,
height: 42,
position: {
x: 30,
y: 290,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "1",
},
{
id: "8",
shape: "rect",
width: 140,
height: 42,
position: {
x: 30,
y: 390,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "1",
},
// 预留结束
// 第二列预留
{
id: "10",
shape: "rect",
width: 140,
height: 42,
position: {
x: 245,
y: 90,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "2",
},
{
id: "11",
shape: "rect",
width: 140,
height: 42,
position: {
x: 245,
y: 190,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "2",
},
{
id: "12",
shape: "rect",
width: 140,
height: 42,
position: {
x: 245,
y: 290,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "2",
},
{
id: "13",
shape: "rect",
width: 140,
height: 42,
position: {
x: 245,
y: 390,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "2",
},
// 第二列预留结束
// 第三列预留开始
{
id: "15",
shape: "rect",
width: 140,
height: 42,
position: {
x: 465,
y: 90,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "3",
},
{
id: "16",
shape: "rect",
width: 140,
height: 42,
position: {
x: 465,
y: 190,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "3",
},
{
id: "17",
shape: "rect",
width: 140,
height: 42,
position: {
x: 465,
y: 290,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "3",
},
{
id: "18",
shape: "rect",
width: 140,
height: 42,
position: {
x: 465,
y: 390,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "3",
},
// 点三列预留结束
// 第四列预留开始
{
id: "20",
shape: "rect",
width: 140,
height: 42,
position: {
x: 685,
y: 90,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "4",
},
{
id: "21",
shape: "rect",
width: 140,
height: 42,
position: {
x: 685,
y: 190,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "4",
},
{
id: "22",
shape: "rect",
width: 140,
height: 42,
position: {
x: 685,
y: 290,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "4",
},
{
id: "23",
shape: "rect",
width: 140,
height: 42,
position: {
x: 685,
y: 390,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "4",
},
// 第四列预留结束
// 第五列预留开始
{
id: "25",
shape: "rect",
width: 140,
height: 42,
position: {
x: 905,
y: 90,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "5",
},
{
id: "26",
shape: "rect",
width: 140,
height: 42,
position: {
x: 905,
y: 190,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "5",
},
{
id: "27",
shape: "rect",
width: 140,
height: 42,
position: {
x: 905,
y: 290,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "5",
},
{
id: "28",
shape: "rect",
width: 140,
height: 42,
position: {
x: 905,
y: 390,
},
data: {
disableMove: false,
},
label: "空位",
attrs: {
body: {
stroke: "#d9d9d9",
strokeDasharray: "5,5",
},
},
parent: "5",
},
],
};
},
methods: {
init() {
let container = document.getElementById("container");
this.graph = new Graph({
container: container,
autoResize: true,
width: 900,
height: 480,
translating: {
useLocalCoordinates: true,
restrict(cellView) {
//#region
let cell = cellView ? cellView.cell : false;
if (!cell) return false;
let yuliu = cell.label
? cell.label.includes("点") || cell.label.includes("空")
: false;
if (yuliu) {
return false;
}
let temp = cell.label ? cell.label.includes("线") : false;
if (temp) {
return false;
}
//#endregion
if (!cellView || !cellView.model) {
return;
}
if (cellView.model.isLink()) {
// 连线不受限制
return undefined;
}
return cellView.model.getBBox();
},
},
connecting: {
router: "manhattan",
connector: {
name: "normal",
args: {
radius: 8,
},
},
anchor: "center",
connectionPoint: "anchor",
allowBlank: false,
snap: {
radius: 20,
},
createEdge() {
return new Shape.Edge({
connector: "normal",
attrs: {
line: {
stroke: "#2d8cf0",
strokeWidth: 1,
targetMarker: {
name: "classic",
size: 8,
},
},
},
router: {
name: "orth",
},
// zIndex: 0,
});
},
validateConnection({
sourceView,
targetView,
sourceMagnet,
targetMagnet,
}) {
if (sourceView === targetView) {
return false;
}
if (!sourceMagnet) {
return false;
}
if (!targetMagnet) {
return false;
}
return true;
},
},
interacting: function (cellView) {
if (
cellView.cell.getData() != undefined &&
!cellView.cell.getData().disableMove
) {
return { nodeMovable: false };
}
return true;
},
});
// 对齐线
this.graph.use(
new Snapline({
enabled: true,
})
);
// 框选
// this.graph.use(
// new Selection({
// enabled: true,
// showNodeSelectionBox: true,
// })
// );
this.graph.use(
new Keyboard({
enabled: true,
global: true,
})
);
this.graph.on("cell:mouseenter", ({ cell }) => {
let temp = cell.label ? cell.label.includes("线") : false;
if (temp) {
return {
selectable: false,
};
}
let yuliu = cell.label
? cell.label.includes("点") || cell.label.includes("空")
: false;
if (yuliu) {
return {
selectable: false,
};
}
if (cell.isNode()) {
let ports = container.querySelectorAll(".x6-port-body");
let show = true;
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? "visible" : "hidden";
}
cell.addTools([
{
name: "button-remove",
args: {
x: 0,
y: 0,
offset: { x: 20, y: 10 },
},
},
]);
} else {
cell.addTools([
{
name: "vertices",
args: {
stopPropagation: false,
},
},
{ name: "segments" }, //添加线上的平移
{
name: "button",
args: {
markup: [
{
tagName: "circle",
selector: "button",
attrs: {
r: 16,
stroke: "#3d85f2",
"stroke-width": 3,
fill: "#3d85f2",
cursor: "pointer",
},
},
{
tagName: "text",
textContent: "︎",
selector: "icon",
attrs: {
fill: "#fff",
"font-size": 25,
"text-anchor": "middle",
"pointer-events": "none",
y: "0.3em",
},
},
],
distance: 40,
onClick({ view }) {
const node = view.cell;
node.remove();
},
},
},
]);
}
});
this.graph.on("cell:mouseleave", ({ cell }) => {
cell.removeTools();
let ports = container.querySelectorAll(".x6-port-body");
let show = false;
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? "visible" : "hidden";
}
});
this.graph.on("node:click", ({ cell }) => {
let yuliu = cell.label
? cell.label.includes("点") || cell.label.includes("空")
: false;
if (yuliu) {
return false;
}
let temp = cell.label ? cell.label.includes("线") : false;
if (temp) {
return false;
}
this.delCell = cell;
let textClick = cell.store.data.attrs.label;
if (textClick.text.includes("xxxx艺")) {
this.flag1 = true;
} else {
this.flag = true;
}
this.celll = cell;
if (cell.form) {
this.ruleForm.name = cell.form.name;
this.ruleForm.count = cell.form.count;
}
});
this.graph.on("cell:mousedown", ({ cell }) => {
let yuliu = cell.label
? cell.label.includes("点") || cell.label.includes("空")
: false;
if (yuliu) {
return false;
}
let temp = cell.label ? cell.label.includes("线") : false;
if (temp) {
return false;
}
this.delCell = cell;
});
this.graph.bindKey("delete", () => {
if (this.delCell) {
this.graph.removeNode(this.delCell);
}
// const cells = this.graph.getSelectedCells();
return false;
});
this.graph.on("node:mousemove", ({ cell }) => {
//#region 过滤
let temp = cell.label ? cell.label.includes("线") : false;
if (temp) {
return {
selectable: false,
};
}
let yuliu = cell.label
? cell.label.includes("点") || cell.label.includes("空")
: false;
if (yuliu) {
return {
selectable: false,
};
}
//#endregion
//#region 吸附
const snapThreshold = 100;
const snapSegmentLength = 1;
let x1 = 30,x2 = 245,x3 = 465,x4 = 685,x5 = 905;
let y1 = 90,y2 = 190,y3 = 290,y4 = 390;
const targetPoints = [
{ x: x1, y: y1 },
{ x: x1, y: y2 },
{ x: x1, y: y3 },
{ x: x1, y: y4 },
// { x: x1, y: y5 },
{ x: x2, y: y1 },
{ x: x2, y: y2 },
{ x: x2, y: y3 },
{ x: x2, y: y4 },
// { x: x2, y: y5 },
{ x: x3, y: y1 },
{ x: x3, y: y2 },
{ x: x3, y: y3 },
{ x: x3, y: y4 },
// { x: x3, y: y5 },
{ x: x4, y: y1 },
{ x: x4, y: y2 },
{ x: x4, y: y3 },
{ x: x4, y: y4 },
// { x: x4, y: y5 },
{ x: x5, y: y1 },
{ x: x5, y: y2 },
{ x: x5, y: y3 },
{ x: x5, y: y4 },
// { x: x5, y: y5 },
];
let node = cell.position();
for (let i = 0; i < targetPoints.length; i++) {
const point = targetPoints[i];
if (node.x <= point.x &&node.x <= point.y &&
node.y > point.x &&node.y <= point.y)
{
cell.position(point.x, point.y);
}
const distX = Math.abs(point.x - node.x);
const distY = Math.abs(point.y - node.y);
// 如果距离小于吸附区域,则进行吸附
if (distX < snapThreshold && distY < snapThreshold) {
// 计算吸附点,使其对齐到网格
const snapX =
Math.round(point.x / snapSegmentLength) * snapSegmentLength;
const snapY =
Math.round(point.y / snapSegmentLength) * snapSegmentLength;
cell.position(snapX, snapY, cell); // 更新图形位置
return false; // 跳出循环,只对第一个目标位置进行吸附
}
}
//#endregion
});
},
reservePlace() {
this.graph.fromJSON(this.dataPlacer);
// let cells = [];
// this.dataPlacer.forEach((item) => {
// if (item.shape === "lane-edge") {
// cells.push(this.graph.createEdge(item));
// } else {
// cells.push(this.graph.createNode(item));
// }
// this.graph.resetCells(cells);
// this.graph.zoomToFit({ padding: 10, maxScale: 1 });
// });
},
// 拖
startDrag(type, e) {
if (type.includes("xxx艺")) {
type = "艺名称";
} else {
type = "运名称";
}
let temp = this.graph;
let arr = [];
let nam = "";
let tt = temp.container.innerText.split("\n");
if (tt) {
for (let j = 0; j < tt.length; j++) {
const dd = tt[j];
if (dd != "") {
arr.push(dd.trim());
}
}
for (let i = 0; i < arr.length + 1; i++) {
const name = arr[i];
nam = `${type}${arr.length - 25 + 1}`;
// console.log(nam);
if (name == type) {
nam = `${type}${i + 1}`;
arr.push(nam);
}
}
}
// 以上代码可以根据自己的实际情况来写,也可以不写以上代码
// 如果不写上面的代码,把下面括号中的nam换成type
startDragToGraph(this.graph, nam, e);
},
},
mounted() {
this.init();
this.reservePlace();
},
}
</script>
安装npm i @antv/x6-plugin-dnd
import { Dnd } from "@antv/x6-plugin-dnd";
export const startDragToGraph = (graph, type, e) => {
const node = graph.createNode({
shape: "rect",
width: 140,
height: 42,
attrs: {
label: {
text: type,
fill: "#00539a",
textAnchor: "middle",
verticalAnchor: "middle",
fontSize: 20,
ellipsis: true,
breakWord: true,
textWrap: {
width: -10,
height: -10,
ellipsis: true,
},
},
body: {
stroke: "#00539a",
strokeWidth: 2,
fill: "#ffffff",
},
},
tools: [ // 使拖拽的元素具有删除的图标
{
name: "button-remove",
args: {
x: 20,
y: 10,
},
},
],
ports: ports,
});
graph.on('node:added', ({cell}) => {
const snapThreshold = 100;
const snapSegmentLength = 1;
let x1 = 30,x2 = 245,x3 = 465,x4 = 685,x5 = 905;
let y1 = 90,y2 = 190,y3 = 290,y4 = 390;
const targetPoints = [
{ x: x1, y: y1 },
{ x: x1, y: y2 },
{ x: x1, y: y3 },
{ x: x1, y: y4 },
{ x: x2, y: y1 },
{ x: x2, y: y2 },
{ x: x2, y: y3 },
{ x: x2, y: y4 },
{ x: x3, y: y1 },
{ x: x3, y: y2 },
{ x: x3, y: y3 },
{ x: x3, y: y4 },
{ x: x4, y: y1 },
{ x: x4, y: y2 },
{ x: x4, y: y3 },
{ x: x4, y: y4 },
{ x: x5, y: y1 },
{ x: x5, y: y2 },
{ x: x5, y: y3 },
{ x: x5, y: y4 },
];
let node = cell.position();
for (let i = 0; i < targetPoints.length; i++) {
const point = targetPoints[i];
if (node.x <= point.x &&node.x <= point.y &&
node.y > point.x &&node.y <= point.y)
{
cell.position(point.x, point.y);
}
const distX = Math.abs(point.x - node.x);
const distY = Math.abs(point.y - node.y);
// 如果距离小于吸附区域,则进行吸附
if (distX < snapThreshold && distY < snapThreshold) {
// 计算吸附点,使其对齐到网格
const snapX =
Math.round(point.x / snapSegmentLength) * snapSegmentLength;
const snapY =
Math.round(point.y / snapSegmentLength) * snapSegmentLength;
cell.position(snapX, snapY, cell); // 更新图形位置
return false; // 跳出循环,只对第一个目标位置进行吸附
}
}
//#endregion
})
const dnd = new Dnd({
target: graph,
});
dnd.start(node, e);
};
const ports = {
groups: {
top: {
position: "top",
zIndex: 1,
attrs: {
portBody: {
"port-type": "ellipse",
r: 8,
magnet: true,
stroke: "#2D8CF0",
strokeWidth: 2,
fill: "#2D8CF0",
},
portCross: {
"port-type": "decorator",
ref: "portBody",
"ref-x": -4.5,
"ref-y": 0,
"ref-dx": 0,
"ref-dy": 0,
position: {
name: "center",
},
d: "M 0 0 L 10 0 M 5 -5 L 5 5",
stroke: "#fff",
strokeWidth: 1,
fill: "#fff",
magnet: true,
zIndex: 2,
},
},
markup: [
{
tagName: "circle",
selector: "portBody",
},
{
tagName: "path",
selector: "portCross",
},
],
},
bottom: {
position: "bottom",
zIndex: 1,
attrs: {
portBody: {
"port-type": "ellipse",
r: 8,
magnet: true,
stroke: "#2D8CF0",
strokeWidth: 2,
fill: "#2D8CF0",
},
portCross: {
"port-type": "decorator",
ref: "portBody",
"ref-x": -4.5,
"ref-y": 0,
"ref-dx": 0,
"ref-dy": 0,
position: {
name: "center",
},
d: "M 0 0 L 10 0 M 5 -5 L 5 5",
stroke: "#fff",
strokeWidth: 1,
fill: "#fff",
magnet: true,
zIndex: 2,
},
},
markup: [
{
tagName: "circle",
selector: "portBody",
},
{
tagName: "path",
selector: "portCross",
},
],
},
left: {
position: "left",
zIndex: 1,
attrs: {
portBody: {
"port-type": "ellipse",
r: 8,
magnet: true,
stroke: "#2D8CF0",
strokeWidth: 2,
fill: "#2D8CF0",
},
portCross: {
"port-type": "decorator",
ref: "portBody",
"ref-x": -4.5,
"ref-y": 0,
"ref-dx": 0,
"ref-dy": 0,
position: {
name: "center",
},
d: "M 0 0 L 10 0 M 5 -5 L 5 5",
stroke: "#fff",
strokeWidth: 1,
fill: "#fff",
magnet: true,
zIndex: 2,
},
},
markup: [
{
tagName: "circle",
selector: "portBody",
},
{
tagName: "path",
selector: "portCross",
},
],
},
right: {
position: "right",
zIndex: 1,
attrs: {
portBody: {
"port-type": "ellipse",
r: 8,
magnet: true,
stroke: "#2D8CF0",
strokeWidth: 2,
fill: "#2D8CF0",
},
portCross: {
"port-type": "decorator",
ref: "portBody",
"ref-x": -4.5,
"ref-y": 0,
"ref-dx": 0,
"ref-dy": 0,
position: {
name: "center",
},
d: "M 0 0 L 10 0 M 5 -5 L 5 5",
stroke: "#fff",
strokeWidth: 1,
fill: "#fff",
magnet: true,
zIndex: 2,
},
},
markup: [
{
tagName: "circle",
selector: "portBody",
},
{
tagName: "path",
selector: "portCross",
},
],
},
},
items: [
{
id: "port1",
group: "top",
},
{
id: "port2",
group: "bottom",
},
{
id: "port3",
group: "left",
},
{
id: "port4",
group: "right",
},
],
};