代码:
<template><div class="graph-wrap" @click.stop="hideFn"><Toobar :graph="graph"></Toobar><!-- 小地图 --><div id="minimap" class="mini-map-container"></div><!-- 画布 --><div id="container" /><!-- 右侧节点配置 --><ConfigPanelclass="right-config":nodeData="nodeData":saveType="saveType"></ConfigPanel><!-- 右键 --><Contextmenuv-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() {// 初始化 graphthis.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, // 网格大小 10pxvisible: 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><divclass="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="" /><imgv-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>