代码:
<template>
<div class="graph-wrap" @click.stop="hideFn">
<Toobar :graph="graph"></Toobar>
<!-- 小地图 -->
<div id="minimap" class="mini-map-container"></div>
<!-- 画布 -->
<div id="container" />
<!-- 右侧节点配置 -->
<ConfigPanel
class="right-config"
:nodeData="nodeData"
:saveType="saveType"
></ConfigPanel>
<!-- 右键 -->
<Contextmenu
v-if="showContextMenu"
ref="menuBar"
@callBack="contextMenuFn"
></Contextmenu>
</div>
</template>
<script>
import { Graph, Node, Path, Cell, Addon } from "@antv/x6";
import { register } from "@antv/x6-vue-shape";
import { Dnd } from "@antv/x6-plugin-dnd";
import { MiniMap } from "@antv/x6-plugin-minimap";
import { Scroller } from "@antv/x6-plugin-scroller";
import { Selection } from "@antv/x6-plugin-selection";
import ConfigPanel from "./components/configPanel.vue";
import Contextmenu from "./components/contextmenu.vue";
import DataBase from "./components/nodeTheme/dataBase.vue";
import Toobar from "./components/toobar.vue";
export default {
name: "Graph",
props: {
// 左侧引擎模版数据
stencilData: {
type: Object,
default: () => {
return {};
},
},
graphData: {
type: Array,
default: () => {
return [];
},
},
// 保存类型
saveType: {
type: String,
default: () => {
return "strategy";
},
},
},
watch: {
graphData: {
handler(newVal) {
// console.log(newVal, 5555);
this.nodeStatusList = [];
for (let i = 0; i < newVal.length; i++) {
if (newVal[i].shape === "dag-node") {
if (newVal[i].data.status != null) {
this.nodeStatusList.push({
id: newVal[i].id,
status: newVal[i].data.status,
});
}
}
}
this.startFn(newVal);
},
// deep: true,
// immediate: true,
},
},
components: {
ConfigPanel,
Contextmenu,
Toobar,
},
computed: {
isDetail() {
if (this.$route.path === "/taskCenter/taskPlan/planDetails") {
return true;
} else {
return false;
}
},
},
data() {
return {
graph: "", // 画布
timer: "",
showContextMenu: false, // 右键
dnd: null, // 左侧
nodeData: {}, // 当前节点数据
nodeStatusList: [], // 节点状态
};
},
destroyed() {
clearTimeout(this.timer);
this.timer = null;
this.graph.dispose(); // 销毁画布
},
mounted() {
// 初始化 graph
this.initGraph();
},
methods: {
// 隐藏右键
hideFn() {
this.showContextMenu = false;
},
// 右键事件
contextMenuFn(type, itemData) {
switch (type) {
case "remove":
if (itemData.type === "edge") {
this.graph.removeEdge(itemData.item.id);
} else if (itemData.type === "node") {
this.graph.removeNode(itemData.item.id);
}
break;
}
this.showContextMenu = false;
},
// 注册vue组件节点 2.x 的写法
registerCustomVueNode() {
register({
shape: "dag-node",
width: 185,
height: 40,
component: DataBase,
ports: {
groups: {
top: {
position: "top",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#C2C8D5",
strokeWidth: 1,
fill: "#fff",
},
},
},
bottom: {
position: "bottom",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#C2C8D5",
strokeWidth: 1,
fill: "#fff",
},
},
},
},
},
});
},
// 注册边
registerCustomEdge() {
Graph.registerEdge(
"dag-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "rgba(0, 0, 0, 0.3)",
strokeWidth: 1,
targetMarker: {
name: "block",
width: 12,
height: 8,
},
},
},
},
true
);
},
// 注册连接器
registerConnector() {
Graph.registerConnector(
"algo-connector",
(s, e) => {
const offset = 4;
const deltaY = Math.abs(e.y - s.y);
const control = Math.floor((deltaY / 3) * 2);
const v1 = { x: s.x, y: s.y + offset + control };
const v2 = { x: e.x, y: e.y - offset - control };
return Path.normalize(
`M ${s.x} ${s.y}
L ${s.x} ${s.y + offset}
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
L ${e.x} ${e.y}
`
);
},
true
);
},
initGraph() {
this.registerCustomVueNode();
this.registerCustomEdge();
this.registerConnector();
const graph = new Graph({
container: document.getElementById("container"),
autoResize: true,
// width: 800,
// height: 600,
background: {
color: "rgba(37, 50, 82, 0.1)", // 设置画布背景颜色
},
grid: {
size: 10, // 网格大小 10px
visible: false, // 渲染网格背景
},
// 画布平移, 不要同时使用 scroller 和 panning,因为两种形式在交互上有冲突。
// panning: {
// enabled: true,
// eventTypes: ["leftMouseDown", "mouseWheel"],
// },
// 画布缩放
mousewheel: {
enabled: true,
modifiers: "ctrl",
factor: 1.1,
maxScale: 1.5,
minScale: 0.5,
},
highlighting: {
magnetAdsorbed: {
name: "stroke",
args: {
attrs: {
fill: "#fff",
stroke: "#31d0c6",
strokeWidth: 4,
},
},
},
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
connector: "algo-connector",
connectionPoint: "anchor",
anchor: "center",
validateMagnet({ magnet }) {
return magnet.getAttribute("port-group") !== "top";
},
createEdge() {
return graph.createEdge({
shape: "dag-edge",
attrs: {
line: {
strokeDasharray: "5 5",
},
},
zIndex: -1,
});
},
},
// 点击选中 1.x 版本
// selecting: {
// enabled: true,
// multiple: true,
// rubberEdge: true,
// rubberNode: true,
// modifiers: "shift",
// rubberband: true,
// },
});
// 点击选中 2.x 版本
graph.use(
new Selection({
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: "shift",
rubberband: true,
})
);
this.graph = graph;
this.initAddon(); // 初始化 拖拽
this.graphEvent();
this.initScroller();
this.initMiniMap();
},
// 画布事件
graphEvent() {
const self = this;
// 边连接/取消连接
this.graph.on("edge:connected", ({ edge }) => {
// 目标一端连接桩只允许连接输入
if (/out/.test(edge.target.port) || !edge.target.port) {
this.$message.error("目标一端连接桩只允许连接输入!");
return this.graph.removeEdge(edge.id);
}
edge.attr({
line: {
strokeDasharray: "",
},
});
});
// 改变节点/边的数据时
this.graph.on("node:change:data", ({ node }) => {
const edges = this.graph.getIncomingEdges(node);
const { status } = node.getData();
console.log(status, 77777);
edges?.forEach((edge) => {
if (status === "running") {
edge.attr("line/strokeDasharray", 5);
edge.attr(
"line/style/animation",
"running-line 30s infinite linear"
);
} else {
edge.attr("line/strokeDasharray", "");
edge.attr("line/style/animation", "");
}
});
});
// 节点右键事件
this.graph.on("node:contextmenu", ({ e, x, y, node, view }) => {
this.showContextMenu = true;
this.$nextTick(() => {
this.$refs.menuBar.initFn(e.pageX, e.pageY, {
type: "node",
item: node,
});
});
});
// 边右键事件
this.graph.on("edge:contextmenu", ({ e, x, y, edge, view }) => {
this.showContextMenu = true;
this.$nextTick(() => {
this.$refs.menuBar.initFn(e.pageX, e.pageY, {
type: "edge",
item: edge,
});
});
});
// 节点单击事件
this.graph.on("node:click", ({ e, x, y, node, view }) => {
// console.log(node, 2222);
// console.log(node.store.data.data.engine);
this.$nextTick(() => {
this.nodeData = {
id: node.id,
store: node.store,
};
});
});
// 鼠标抬起
this.graph.on("node:mouseup", ({ e, x, y, node, view }) => {
// self.$emit("saveGraph");
});
//平移画布时触发,tx 和 ty 分别是 X 和 Y 轴的偏移量。
this.graph.on("translate", ({ tx, ty }) => {
self.$emit("saveGraph");
});
// 移动节点后触发
this.graph.on("node:moved", ({ e, x, y, node, view }) => {
self.$emit("saveGraph");
});
// 移动边后触发
this.graph.on("edge:moved", ({ e, x, y, node, view }) => {
self.$emit("saveGraph");
});
},
// 初始化拖拽
initAddon() {
this.dnd = new Dnd({
target: this.graph,
});
},
// 开始拖拽
startDragToGraph() {
const node = this.graph.createNode(this.nodeConfig());
this.dnd.start(node, this.stencilData.e);
},
// 节点配置
nodeConfig() {
const engineItem = this.stencilData.engineItem;
const time = new Date().getTime();
const attrs = {
circle: {
r: 4,
magnet: true,
stroke: "#C2C8D5",
strokeWidth: 1,
fill: "#fff",
},
};
const top = {
position: "top",
attrs,
};
const bottom = {
pposition: "bottom",
attrs,
};
const itemsObj = [
{
id: `in-${time}`,
group: "top", // 指定分组名称
},
{
id: `out-${time}`,
group: "bottom", // 指定分组名称
},
];
// 链接桩3种状态 1、in | 只允许被连 2、out | 只允许输出 3、any | 不限制
let groups = {};
let items = [];
if (engineItem.top) {
groups = {
top,
};
items = [itemsObj[0]];
}
if (engineItem.bottom) {
groups = {
bottom,
};
items = [itemsObj[1]];
}
if (engineItem.top && engineItem.bottom) {
groups = {
top,
bottom,
};
items = itemsObj;
}
let config = {
shape: "dag-node",
width: 185,
height: 40,
attrs: {
body: {
fill: "#1D2035",
stroke: "rgba(255, 255, 255, 0.3)",
},
label: {
text: engineItem.name,
fill: "rgba(255, 255, 255, 0.9)",
},
},
ports: {
groups,
items,
},
data: {
label: engineItem.name,
engine: engineItem,
},
};
// console.log(config, 33333);
return config;
},
// 初始化节点/边
init(data = []) {
const cells = [];
data.forEach((item) => {
if (item.shape === "dag-node") {
cells.push(this.graph.createNode(item));
} else {
cells.push(this.graph.createEdge(item));
}
});
this.graph.resetCells(cells);
},
// 显示节点状态
async showNodeStatus(statusList) {
console.log(statusList, "8888888");
// const status = statusList.shift();
statusList?.forEach((item) => {
const { id, status } = item;
const node = this.graph.getCellById(id);
const data = node.getData();
node.setData({
...data,
status: status,
});
});
this.timer = setTimeout(() => {
this.showNodeStatus(statusList);
}, 3000);
},
startFn(item) {
this.timer && clearTimeout(this.timer);
this.init(item);
// this.showNodeStatus(Object.assign([], this.nodeStatusList));
this.graph.centerContent();
},
// 获取画布数据
getGraphData() {
const { cells = [] } = this.graph.toJSON();
let data = [];
console.log(cells, 333);
for (let i = 0; i < cells.length; i++) {
let item = {};
let cellsItem = cells[i];
if (cellsItem.shape === "dag-node") {
let nodeType = 0; // 节点类型 0-下连接柱, 1-上下连接柱 ,2-上连接柱
if (
cellsItem.ports.items.length === 1 &&
cellsItem.ports.items[0].group === "bottom"
) {
nodeType = 0;
}
if (cellsItem.ports.items.length === 2) {
nodeType = 1;
}
if (
cellsItem.ports.items.length === 1 &&
cellsItem.ports.items[0].group === "top"
) {
nodeType = 2;
}
item = {
id: cellsItem.id,
shape: cellsItem.shape,
x: cellsItem.position.x,
y: cellsItem.position.y,
ports: cellsItem.ports.items,
data: {
...cellsItem.data,
type: "node",
nodeType: nodeType,
},
};
} else {
item = {
id: cellsItem.id,
shape: cellsItem.shape,
source: cellsItem.source,
target: cellsItem.target,
data: {
type: "edge",
},
zIndex: 0,
};
}
data.push(item);
}
return data;
},
initScroller() {
this.graph.use(
new Scroller({
enabled: true,
pageVisible: true,
pageBreak: false,
pannable: true,
})
);
},
// 初始化小地图
initMiniMap() {
this.graph.use(
new MiniMap({
container: document.getElementById("minimap"),
width: 220,
height: 140,
padding: 10,
})
);
},
},
};
</script>
<style lang="scss" scoped>
.graph-wrap {
width: 100%;
height: 100%;
min-height: 600px;
position: relative;
background: #fff;
#container {
width: 100%;
height: 100%;
}
.right-config {
position: absolute;
top: 0px;
right: 0px;
}
}
</style>
<style lang="scss" >
// 小地图
.mini-map-container {
position: absolute;
bottom: 12px;
right: 10px;
width: 220px;
height: 140px;
opacity: 1;
// background: #fff;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.x6-widget-minimap {
background: rgba(37, 50, 82, 0.1) !important;
}
.x6-widget-minimap-viewport {
border: 1px solid #0289f7 !important;
}
.x6-widget-minimap-viewport-zoom {
border: 1px solid #0289f7 !important;
}
.x6-widget-minimap .x6-graph {
box-shadow: none !important;
}
.x6-graph-scroller.x6-graph-scroller-paged .x6-graph {
box-shadow: none !important;
}
// .x6-graph-scroller::-webkit-scrollbar {
// width: 8px;
// height: 8px;
// /**/
// }
// .x6-graph-scroller::-webkit-scrollbar-track {
// background: rgb(239, 239, 239);
// border-radius: 2px;
// }
// .x6-graph-scroller::-webkit-scrollbar-thumb {
// background: #bfbfbf;
// border-radius: 10px;
// }
// .x6-graph-scroller::-webkit-scrollbar-thumb:hover {
// background: #999;
// }
// .x6-graph-scroller::-webkit-scrollbar-corner {
// background: rgb(239, 239, 239);
// }
</style>
toobar.vue
<template>
<div class="toolbar">
<el-button type="text" :disabled="!canUndo">
<el-tooltip effect="dark" content="撤销" placement="right">
<i class="raderfont rader-icon-a-revoke" @click="onUndo"></i>
</el-tooltip>
</el-button>
<el-button type="text" :disabled="!canRedo">
<el-tooltip effect="dark" content="重做" placement="right">
<i class="raderfont rader-icon-next" @click="onRedo"></i>
</el-tooltip>
</el-button>
<el-tooltip effect="dark" content="放大" placement="right">
<i class="raderfont rader-icon-amplify" @click="zoomIn"></i>
</el-tooltip>
<el-tooltip effect="dark" content="缩小" placement="right">
<i class="raderfont rader-icon-reduce" @click="zoomOut"></i>
</el-tooltip>
<el-tooltip effect="dark" content="全屏" placement="right">
<i class="raderfont rader-icon-full-screen" @click="toFullScreen"></i>
</el-tooltip>
</div>
</template>
<script>
import { History } from "@antv/x6-plugin-history";
export default {
name: "Toobar",
props: ["graph"],
data() {
return {
graphObj: null,
canUndo: false,
canRedo: false,
};
},
mounted() {
this.$nextTick(() => {
this.graphObj = this.graph;
this.graphHistory();
});
},
methods: {
// 撤销重做
graphHistory() {
this.graphObj.use(
new History({
enabled: true,
})
);
this.graphObj.on("history:change", () => {
this.canUndo = this.graphObj.canUndo();
this.canRedo = this.graphObj.canRedo();
});
},
// 撤销
onUndo() {
this.graphObj.undo();
},
// 重做
onRedo() {
this.graphObj.redo();
},
// 放大
zoomIn() {
this.graphObj.zoom(0.2);
},
// 缩小
zoomOut() {
this.graphObj.zoom(-0.2);
},
// 全屏
toFullScreen() {
this[document.fullscreenElement ? "exitFullscreen" : "fullScreen"]();
},
fullScreen() {
const full = this.$parent.$el;
if (full.RequestFullScreen) {
full.RequestFullScreen();
// 兼容Firefox
} else if (full.mozRequestFullScreen) {
full.mozRequestFullScreen();
// 兼容Chrome, Safari and Opera等
} else if (full.webkitRequestFullScreen) {
full.webkitRequestFullScreen();
// 兼容IE/Edge
} else if (full.msRequestFullscreen) {
full.msRequestFullscreen();
}
},
exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
// 兼容Firefox
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
// 兼容Chrome, Safari and Opera等
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
// 兼容IE/Edge
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
},
},
};
</script>
<style lang="scss" scoped>
.toolbar {
z-index: 100;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.06);
.el-button + .el-button {
margin-left: 0px;
}
.el-button {
margin: 5px 0px;
}
i {
font-size: 18px;
margin: 5px 8px;
// color: rgba(255, 255, 255, 0.8);
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.layout-opts {
list-style: none;
padding: 0;
text-align: center;
li {
cursor: pointer;
font-size: 14px;
line-height: 22px;
color: #3c5471;
&:hover {
color: #1890ff;
}
}
}
}
</style>
dataBase.vue
<template>
<div
class="node"
:class="[
status === 0 ? 'running' : '',
status === 1 ? 'progress' : '',
status === 2 ? 'success' : '',
status === 3 ? 'failed' : '',
status === 4 ? 'stop' : '',
]"
>
<span class="left" :class="[labelList.includes(label) ? 'common' : '']">
<img v-if="labelList.includes(label)" :src="leftImg[label]" alt="" />
<img
v-if="!labelList.includes(label)"
src="@/static/images/detection.png"
alt=""
/>
</span>
<span class="right">
<span class="label" :title="label">{{ label }}</span>
<span class="status">
<img :src="imgCot[status]" alt="" />
</span>
</span>
</div>
</template>
<script>
export default {
name: "DataBase",
inject: ["getNode"],
data() {
return {
status: 0,
label: "",
labelList: ["开始", "结束", "过滤器", "选择器"],
imgCot: {
0: require("@/static/images/wait-status.png"),
1: require("@/static/images/progress-status.png"),
2: require("@/static/images/success-status.png"),
3: require("@/static/images/fail-status.png"),
4: require("@/static/images/stop-status.png"),
5: require("@/static/images/pause-status.png"),
},
leftImg: {
开始: require("@/static/images/start-inside.png"),
结束: require("@/static/images/stop-inside.png"),
过滤器: require("@/static/images/filter-inside.png"),
选择器: require("@/static/images/selector-inside.png"),
},
};
},
computed: {
showStatus() {
if (typeof this.status === "undefined") {
return false;
}
return true;
},
},
mounted() {
const self = this;
const node = this.getNode();
this.label = node.data.label;
this.status = node.data.status || 0;
// console.log(node, 11111);
// 监听数据改变事件
node.on("change:data", ({ current }) => {
console.log(current, 22222);
self.label = current.label;
self.status = current.status;
});
},
methods: {},
};
</script>
<style lang="scss" scoped>
.node {
display: flex;
align-items: center;
width: 100%;
height: 100%;
background-color: #fff;
// border: 1px solid rgba(255, 255, 255, 0.3);
// border-left: 4px solid #5f95ff;
border-radius: 8px;
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
.left {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 8px 0px 0px 8px;
box-sizing: border-box;
border: 1px solid rgba(220, 223, 230);
// background: rgba(42, 230, 255, 0.15);
&.common {
// background: rgba(168, 237, 113, 0.149);
}
img {
width: 22px;
height: 22px;
}
}
.right {
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(220, 223, 230);
border-radius: 0px 8px 8px 0px;
border-left: 0;
padding: 0px 5px;
.label {
flex: 1;
display: inline-block;
flex-shrink: 0;
// color: rgba(255, 255, 255, 0.9);
color: #666;
font-size: 12px;
overflow: hidden; //超出文本隐藏
text-overflow: ellipsis; ///超出部分省略号显示
display: -webkit-box; //弹性盒模型
-webkit-box-orient: vertical; //上下垂直
-webkit-line-clamp: 2; //自定义行数
}
.status {
width: 18px;
height: 18px;
flex-shrink: 0;
margin-left: 5px;
img {
width: 18px;
height: 18px;
}
}
}
}
.node.success {
// border-left: 4px solid #52c41a;
}
.node.failed {
// border-left: 4px solid #ff4d4f;
}
.node.progress .status img {
animation: spin 1s linear infinite;
}
.x6-node-selected .node {
border-color: #2ae6ff;
border-radius: 8px;
box-shadow: 0 0 0 3px #d4e8fe;
}
.x6-node-selected .node.running {
border-color: #2ae6ff;
border-radius: 8px;
// box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.success {
border-color: #52c41a;
border-radius: 8px;
// box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
border-color: #ff4d4f;
border-radius: 8px;
// box-shadow: 0 0 0 4px #fedcdc;
}
.x6-edge:hover path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1px;
}
.x6-edge-selected path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1.5px !important;
}
@keyframes running-line {
to {
stroke-dashoffset: -1000;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
contextmenu.vue
<template>
<ul class="contextmenu-wrap" :style="{ left: x + 'px', top: y + 'px' }">
<li @click.stop="callBack('remove')">删除</li>
</ul>
</template>
<script>
export default {
name: "Contextmenu",
data() {
return {
x: "",
y: "",
item: {}, // 节点或者边的数据
};
},
mounted() {},
methods: {
initFn(x, y, item) {
this.x = parseInt(x) + "";
this.y = parseInt(y) + "";
if (item) {
this.item = item;
}
},
callBack(type) {
this.$emit("callBack", type, this.item);
},
},
};
</script>
<style lang="scss" scoped>
.contextmenu-wrap {
width: 150px;
position: fixed;
z-index: 999;
// border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
font-size: 12px;
color: #545454;
background: #1d2035;
padding: 10px 8px;
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
> li {
color: #ffffff;
cursor: pointer;
text-align: center;
// background: rgba(37, 50, 82, 0.2);
}
}
</style>