因为官网的演示案例是react+antd,而我的是vue3+element,实现效果如下图
具体实现代码如下,在实例化X6的时候通过onPortRendered这个钩子函数里面使用createVNode去给每一个连接桩节点添加自定义的dom,在结合element UI的组件去使用
1首先引入需要用到的函数
import { ref, onMounted, createVNode, render } from "vue";
import { Graph, Shape, Markup } from "@antv/x6";
import { ElTooltip } from "element-plus";
2在创建节点的时候,使用 portMarkup: [Markup.getForeignObjectMarkup()],portMarkup
是 AntV X6 中用于定义连接桩(ports)外观的属性。使用 Markup.getForeignObjectMarkup()
可以将连接桩定义为 foreignObject
,这样可以在连接桩中嵌入任意的 HTML 或 SVG 内容
const srcList = [
"https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg",
"https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg",
"https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg",
"https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg",
"https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg",
"https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg",
"https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg",
];
const imgNodes = srcList.map((item) => {
return graph.value.createNode({
shape: "image", //可选值:Rect Circle Ellipse Polygon Polyline Path Image HTML TextBlock BorderedImage EmbeddedImage InscribedImage Cylinder
imageUrl: item,
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
strokeWidth: 1,
},
},
width: 100,
height: 60,
portMarkup: [Markup.getForeignObjectMarkup()],
ports: { ...customPorts },
});
});
3在配置连接桩时不要使用circle,要使用fo,具体配置如图
4 在onPortRendered钩子函数里添加如下代码
onPortRendered(args) {
const selectors = args.contentSelectors;
const container = selectors && selectors.foContent;
if (container) {
const portName = args.port.label && args.port.label.text;
const dom = createVNode(
ElTooltip,
{
effect: "dark",
content: portName,
placement: "top-start",
},
[
createVNode("div", {
class: "createVNode",
}),
]
);
render(dom, container);
}
//记得添加样式
::v-deep .createVNode {
width: 100%;
height: 100%;
border: 2px solid #31d0c6;
border-radius: 100%;
background: #fff;
box-sizing: border-box;
}
完结
完整代码如下
<template>
<div class="home">
<div class="left" id="stencil"></div>
<div class="right" id="container"></div>
<div class="tool"></div>
</div>
</template>
<script setup>
import { ref, onMounted, createVNode, render } from "vue";
import { Graph, Shape, Markup } from "@antv/x6";
import { Stencil } from "@antv/x6-plugin-stencil";
import { Snapline } from "@antv/x6-plugin-snapline";
import { basicPorts, customPorts } from "./ports";
import { NodeGroup } from "./shape";
import { ElMessage, ElTooltip } from "element-plus";
const stencil = ref(null);
const graph = ref(null);
onMounted(() => {
initGraph();
});
const initGraph = () => {
const container = document.getElementById("container");
graph.value = new Graph({
container: container,
width: container.offsetWidth,
height: container.offsetHeight,
autoResize: true,
background: {
color: "#fff", // 设置画布背景颜色
},
grid: {
size: 10, // 网格大小
visible: true, // 是否显示网格
type: "doubleMesh", // 网格类型,双网格
args: [
{
color: "#cccccc", // 网格颜色
thickness: 1, // 网格线条宽度
},
],
},
scroller: {
enabled: false, // 是否启用滚动
pageVisible: false, // 是否显示分页
pageBreak: false, // 是否显示分页断开线
pannable: false, // 是否允许拖动画布
},
mousewheel: {
enabled: true, // 是否启用鼠标滚轮缩放
modifiers: ["ctrl", "meta"], // 按下哪些键可以使用滚轮缩放
minScale: 0.5, // 最小缩放比例
maxScale: 2, // 最大缩放比例
},
connecting: {
anchor: "center", // 连接点位置
connectionPoint: "anchor", // 连接点类型
allowBlank: true, // 允许连接到空白处
highlight: true, // 高亮可连接的连接点
snap: true, // 是否自动吸附
allowMulti: true, // 是否允许相同起始和终止节点之间创建多条边
allowNode: false, // 是否允许连接到节点
allowBlank: false, // 是否允许连接到空白点
allowLoop: false, // 是否允许创建循环连线
allowEdge: false, // 是否允许连接到另一个边
highlight: true, // 是否高亮显示可用连接点或节点
connectionPoint: "anchor", // 连接点类型
anchor: "center", // 锚点位置
createEdge() {
// X6 的 Shape 命名空间中内置 Edge、DoubleEdge、ShadowEdge 三种边
return new Shape.Edge({
attrs: {
line: {
stroke: "#000",
strokeWidth: 2,
strokeDasharray: null,
targetMarker: null,
},
// outline: {
// stroke: "#73d13d",
// strokeWidth: 15,
// },
},
router: {
name: "manhattan",
},
});
},
validateConnection({
sourceView,
targetView,
sourceMagnet,
targetMagnet,
}) {
if (sourceView === targetView) {
return false; // 禁止自身连接
}
if (!sourceMagnet) {
return false; // 禁止无连接点的起始连接
}
if (!targetMagnet) {
return false; // 禁止无连接点的终止连接
}
return true;
},
},
onPortRendered(args) {
const selectors = args.contentSelectors;
const container = selectors && selectors.foContent;
if (container) {
const portName = args.port.label && args.port.label.text;
const dom = createVNode(
ElTooltip,
{
effect: "dark",
content: portName,
placement: "top-start",
},
[
createVNode("div", {
class: "createVNode",
}),
]
);
render(dom, container);
}
console.log(container);
},
panning: {
enabled: true, // 是否启用画布拖拽平移
modifiers: "shift", // 按住 shift 键才能平移画布
},
resizing: true, // 是否启用节点缩放
rotating: true, // 是否启用节点旋转
selecting: {
enabled: true, // 是否启用选择功能
multiple: true, // 是否允许多选
rubberband: true, // 是否启用框选
movable: true, // 是否允许移动
showNodeSelectionBox: true, // 是否显示节点选择框
},
snapline: true, // 是否启用对齐线
history: true, // 是否启用历史记录
clipboard: {
enabled: true, // 是否启用剪贴板功能
},
keyboard: {
enabled: true, // 是否启用键盘快捷键
},
});
graph.value.use(
new Snapline({
enabled: true,
})
);
initStencil();
// nodeAddEvent();
};
const initStencil = () => {
const stencilDom = document.getElementById("stencil");
stencil.value = new Stencil({
title: "选择栏",
target: graph.value,
stencilGraphWidth: 280,
collapsable: true,
groups: [
{
name: "basic",
title: "基础节点",
graphHeight: 180,
},
{
name: "custom-image",
title: "系统设计图",
graphHeight: 400,
},
{
name: "dian-image",
title: "KM",
graphHeight: 600,
},
],
container: stencilDom,
});
stencilDom.appendChild(stencil.value.container);
// stencil.value.load([rect1], "custom-image");
initNode();
};
const initNode = () => {
// 基础节点
const r1 = graph.value.createNode({
shape: "flow-chart-rect",
attrs: {
body: {
rx: 24,
ry: 24,
},
text: {
text: "起始节点",
},
},
});
const r2 = graph.value.createNode({
shape: "flow-chart-rect",
attrs: {
text: {
text: "流程节点",
},
},
});
const r3 = graph.value.createNode({
shape: "flow-chart-rect",
width: 52,
height: 52,
angle: 45,
attrs: {
"edit-text": {
style: {
transform: "rotate(-45deg)",
},
},
text: {
text: "判断节点",
transform: "rotate(-45deg)",
},
},
ports: {
groups: {
top: {
position: {
name: "top",
args: {
dx: -26,
},
},
},
right: {
position: {
name: "right",
args: {
dy: -26,
},
},
},
bottom: {
position: {
name: "bottom",
args: {
dx: 26,
},
},
},
left: {
position: {
name: "left",
args: {
dy: 26,
},
},
},
},
},
});
const r4 = graph.value.createNode({
shape: "flow-chart-rect",
width: 70,
height: 70,
attrs: {
body: {
rx: 35,
ry: 35,
},
text: {
text: "链接节点",
},
},
});
const srcList = [
"https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg",
"https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg",
"https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg",
"https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg",
"https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg",
"https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg",
"https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg",
];
const imgNodes = srcList.map((item) => {
return graph.value.createNode({
shape: "image", //可选值:Rect Circle Ellipse Polygon Polyline Path Image HTML TextBlock BorderedImage EmbeddedImage InscribedImage Cylinder
imageUrl: item,
attrs: {
body: {
fill: "#f5f5f5",
stroke: "#d9d9d9",
strokeWidth: 1,
},
},
width: 100,
height: 60,
portMarkup: [Markup.getForeignObjectMarkup()],
ports: { ...customPorts },
});
});
stencil.value.load([r1, r2, r3, r4], "basic");
stencil.value.load(imgNodes, "custom-image");
// 节点被添加到画布显示连接桩
graph.value.on("node:added", ({ node }) => {
const ports = node.getPorts();
ports.forEach((port) => {
node.portProp(port.id, "attrs/circle/style/visibility", "visible");
});
});
};
const edgeCrlorDom = ref(null);
const curSelectNode = ref(null);
const nodeAddEvent = () => {
graph.value.on("node:port:click", ({ e, node, view }) => {
e.stopPropagation();
});
graph.value.on("node:click", ({ e, x, y, node, view }) => {
if (e.target.classList.contains("x6-port-body")) {
return;
}
console.log("点击!!!", node);
// 判断是否有选中过节点
if (curSelectNode.value) {
// 移除选中状态
curSelectNode.value.removeTools();
// 判断两次选中节点是否相同
if (curSelectNode.value !== node) {
node.addTools([
{
name: "boundary",
args: {
attrs: {
fill: "#16B8AA",
stroke: "#2F80EB",
strokeWidth: 1,
fillOpacity: 0.1,
},
},
},
{
name: "button-remove",
args: {
x: "100%",
y: 0,
offset: {
x: 0,
y: 0,
},
},
},
]);
curSelectNode.value = node;
} else {
curSelectNode.value = null;
}
} else {
curSelectNode.value = node;
node.addTools([
{
name: "boundary",
args: {
attrs: {
fill: "#000",
stroke: "#000",
strokeWidth: 1,
fillOpacity: 0.1,
},
},
},
{
name: "button-remove",
args: {
x: "100%",
y: 0,
offset: {
x: 0,
y: 0,
},
},
},
]);
}
});
// 连线绑定悬浮事件显示删除
graph.value.on("cell:mouseenter", ({ cell }) => {
if (cell.shape == "edge") {
cell.addTools([
{
name: "vertices",
// args: {
// attrs: { fill: "#8f8f8f" },
// },
},
{
name: "segments",
// args: {
// snapRadius: 20,
// attrs: { fill: "#8f8f8f" },
// },
},
{
name: "button-remove",
args: {
x: "100%",
y: 0,
offset: { x: 0, y: 0 },
},
},
{
name: "button",
args: {
markup: [
{
tagName: "circle",
selector: "button",
attrs: {
r: 5,
stroke: "#fe854f",
"stroke-width": 2,
fill: "white",
cursor: "pointer",
},
},
{
tagName: "text",
textContent: "点击换色",
selector: "icon",
attrs: {
fill: "#fe854f",
"font-size": 5,
"text-anchor": "middle",
"pointer-events": "none",
y: "0.3em",
},
},
],
distance: -40,
onClick({ view }) {
const edge = view.cell;
edgeCrlorDom.value = null;
edgeCrlorDom.value = edge;
drawer.value = true;
},
},
},
]);
cell.setAttrs({
line: {
stroke: "#409EFF",
},
});
cell.zIndex = 99; // 保证当前悬停的线在最上层,不会被遮挡
}
});
graph.value.on("cell:mouseleave", ({ cell }) => {
if (cell.shape === "edge") {
cell.removeTools();
cell.setAttrs({
line: {
// stroke: radio.value ? `${radio.value}` : "black",
stroke: "black",
},
});
cell.zIndex = 100; // 保证未悬停的线在下层,不会遮挡悬停的线
}
});
};
</script>
<style scoped>
.home {
color: #000;
display: flex;
height: 100%;
}
.left {
width: 280px;
height: 100%;
background-color: aqua;
overflow: auto;
position: relative;
border-right: 1px solid rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
.right {
flex: 1;
height: 100%;
}
.tool {
width: 280px;
height: 100%;
}
::v-deep .createVNode {
width: 100%;
height: 100%;
border: 2px solid #31d0c6;
border-radius: 100%;
background: #fff;
box-sizing: border-box;
}
</style>