代码:
<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>
configPanel.vue
<template>
<div class="config-wrap" v-if="configShow">
<div class="right-head">
<p>节点配置</p>
<i class="el-icon-close" @click="close"></i>
</div>
<div class="right-content">
<el-form
ref="ruleForm"
:model="ruleForm"
:rules="rules"
:label-position="labelPosition"
label-width="100px"
class="demo-ruleForm"
size="small"
>
<div class="right-basicInfo info-box">
<p class="basic-info" @click="basicShow = !basicShow">
<i v-if="basicShow" class="el-icon-caret-bottom"></i>
<i v-else class="el-icon-caret-top"></i>
<span>基本信息</span>
</p>
<div v-show="basicShow">
<el-form-item label="引擎名称:" prop="">
<div class="name">{{ ruleForm.name }}</div>
</el-form-item>
<el-form-item label="描述:" prop="">
<div class="name">{{ ruleForm.description }}</div>
</el-form-item>
</div>
</div>
<div class="right-basicInfo">
<p class="basic-info" @click="inputShow = !inputShow">
<i v-if="inputShow" class="el-icon-caret-bottom"></i>
<i v-else class="el-icon-caret-top"></i>
<span>输入参数</span>
</p>
<template v-if="ruleForm.inputPreConfig.content">
<div v-show="inputShow" class="search-table">
<el-table
:data="ruleForm.inputPreConfig.content"
style="width: 100%"
>
<el-table-column label="参数名称" prop="name">
</el-table-column>
<el-table-column label="关键词类型" prop="keywordType">
<template slot-scope="scope">
<span>{{ keywordType[scope.row.keywordType] }}</span>
</template>
</el-table-column>
<el-table-column label="过滤条件" prop="attributes">
<template slot-scope="scope">
<span v-if="scope.row.attributes">{{
scope.row.attributes.filter
}}</span>
</template>
</el-table-column>
<el-table-column label="描述" prop="desc" width="100">
<template slot-scope="scope">
<el-tooltip
class="item"
effect="dark"
:content="scope.row.desc"
placement="bottom"
>
<div class="tooltip-box">{{ scope.row.desc }}</div>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
<div class="right-basicInfo">
<p class="basic-info" @click="outputShow = !outputShow">
<i v-if="outputShow" class="el-icon-caret-bottom"></i>
<i v-else class="el-icon-caret-top"></i>
<span>输出参数</span>
</p>
<template v-if="ruleForm.outputReaderConfig.length">
<div v-show="outputShow" class="search-table">
<el-table
:data="ruleForm.outputReaderConfig[0].content"
style="width: 100%"
>
<el-table-column label="参数名称" prop="name">
</el-table-column>
<el-table-column label="关键词类型" prop="keywordType">
<template slot-scope="scope">
<span>{{ keywordType[scope.row.keywordType] }}</span>
</template>
</el-table-column>
<el-table-column label="过滤条件" prop="attributes">
<template slot-scope="scope">
<span v-if="scope.row.attributes">{{
scope.row.attributes.filter
}}</span>
</template>
</el-table-column>
<el-table-column label="描述" prop="desc" width="100">
<template slot-scope="scope">
<el-tooltip
class="item"
effect="dark"
:content="scope.row.desc"
placement="bottom"
>
<div class="tooltip-box">{{ scope.row.desc }}</div>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
<div class="right-basicInfo">
<p class="basic-info" @click="paramsShow = !paramsShow">
<i v-if="paramsShow" class="el-icon-caret-bottom"></i>
<i v-else class="el-icon-caret-top"></i>
<span>执行参数配置</span>
</p>
<div class="basic-content" v-show="paramsShow">
<div
class="params-content"
v-for="(item, index) in ruleForm.dynamicFieldAndValue"
:key="index"
>
<template
v-if="
item.componentConfig.componentType === 'input' &&
item.componentConfig.show
"
>
<el-form-item
:label="item.name"
prop=""
:required="item.componentConfig.required"
>
<el-input
v-model="item.value"
:placeholder="`请填写${item.name}`"
:disabled="isDetail"
></el-input>
</el-form-item>
</template>
<template
v-if="
item.componentConfig.componentType === 'switch' &&
item.componentConfig.show
"
>
<el-form-item
:label="item.name"
prop=""
:required="item.componentConfig.required"
>
<el-switch
v-model="item.value"
:disabled="isDetail"
@change="switchChange($event, item)"
>
</el-switch>
<el-form-item label="" prop="" v-if="item.child">
<div
class="child-box"
v-for="(leve1, index1) in item.child"
:key="index1"
>
<template
v-if="leve1.componentConfig.componentType === 'button'"
>
<el-form-item
label=""
prop=""
:required="leve1.componentConfig.required"
>
<div v-if="item.value">
<el-button
type="primary"
size="mini"
@click="selectPoc"
>
选择POC
</el-button>
<span> 已选择 {{ pocNum }} 个POC </span>
</div>
</el-form-item>
</template>
</div>
</el-form-item>
</el-form-item>
</template>
<template
v-if="
item.componentConfig.componentType === 'select' &&
item.componentConfig.show
"
>
<el-form-item
:label="item.name"
prop=""
:required="item.componentConfig.required"
>
<el-select
v-model="item.value"
:multiple="item.componentConfig.multiple"
placeholder="请选择"
:popper-append-to-body="false"
:disabled="isDetail"
>
<el-option
v-for="option in item.componentConfig.dic"
:key="option.value"
:label="option.label"
:value="option.value"
>
</el-option>
</el-select>
</el-form-item>
</template>
<template
v-if="
item.componentConfig.componentType === 'object' &&
item.componentConfig.show
"
>
<el-form-item :label="item.name" prop="" v-if="item.child">
<div
class="child-box"
v-for="(leve1, index1) in item.child"
:key="index1"
>
<template
v-if="leve1.componentConfig.componentType === 'input'"
>
<el-form-item
:label="leve1.name"
prop=""
:required="leve1.componentConfig.required"
>
<el-input
v-model="leve1.value"
:placeholder="`请填写${leve1.name}`"
:disabled="isDetail"
></el-input>
</el-form-item>
</template>
<template
v-if="leve1.componentConfig.componentType === 'select'"
>
<el-form-item
:label="leve1.name"
prop=""
:required="leve1.componentConfig.required"
>
<el-select
v-model="leve1.value"
:multiple="leve1.componentConfig.multiple"
placeholder="请选择"
:popper-append-to-body="false"
:disabled="isDetail"
>
<el-option
v-for="option in leve1.componentConfig.dic"
:key="option.value"
:label="option.label"
:value="option.value"
>
</el-option>
</el-select>
</el-form-item>
</template>
</div>
</el-form-item>
</template>
<template
v-if="
item.componentConfig.componentType === 'array' &&
item.componentConfig.show
"
>
<el-form-item :label="item.name" prop="" v-if="item.child">
<div
class="child-box"
v-for="(leve1, index1) in item.child"
:key="index1"
>
<template
v-if="leve1.componentConfig.componentType === 'input'"
>
<el-form-item
:label="leve1.name"
prop=""
:required="leve1.componentConfig.required"
>
<div class="array-item">
<el-input
v-model="leve1.value"
:placeholder="`请填写内容`"
:disabled="isDetail"
></el-input>
<i
v-if="item.child.length > 1"
class="el-icon-minus"
@click="delClick(leve1, index)"
></i>
<i
class="el-icon-plus"
@click="addClick(item, index)"
></i>
</div>
</el-form-item>
</template>
</div>
</el-form-item>
</template>
</div>
</div>
</div>
</el-form>
</div>
<div class="right-footer">
<el-button class="cancel" size="small" @click="close">取 消</el-button>
<el-button
v-if="!isDetail"
class="sure"
size="small"
type="primary"
@click="saveStrategy('ruleForm')"
>保 存</el-button
>
</div>
<!-- 选择POC -->
<PocSelect
v-if="pocShow"
:is-show.sync="pocShow"
:selectedIds="pocIds"
@confirm="confirm"
></PocSelect>
</div>
</template>
<script>
export default {
name: 'ConfigPanel',
props: {
nodeData: {
type: Object,
default: () => {
return {}
},
},
graphData: {
type: Array,
default: () => {
return []
},
},
// 保存类型
saveType: {
type: String,
default: () => {
return 'strategy'
},
},
},
computed: {
nodeId() {
return this.nodeData.id
},
isDetail() {
if (
this.$route.path === '/taskCenter/taskStrategy/strategyDetails' ||
this.$route.path === '/taskCenter/taskPlan/planDetails'
) {
return true
} else {
return false
}
},
pocNum() {
return this.pocIds.length
},
},
watch: {
nodeData: {
handler(newVal) {
// console.log(newVal, 777);
if (newVal) {
this.configShow = true
this.ruleForm = newVal.store.data.data.engine
this.ruleForm.dynamicFieldAndValue.forEach((el) => {
if (el.code === 'inputNodeOutput') {
el.componentConfig.dic = this.setFilterData(newVal.id)
const valueList = el.componentConfig.dic.map((item) => item.value)
if (el.value && el.value.length > 0) {
el.value.forEach((item2, index2) => {
if (!valueList.includes(item2)) {
el.value.splice(index2, 1)
}
})
}
}
// 选择poc回显
if (el.code === 'isSelectPoc') {
this.isSelectPoc = el.value;
if (el.child && el.child.length > 0) {
el.child.forEach((item2, index2) => {
if (item2.value) {
this.pocIds = item2.value.split(',')
} else {
this.pocIds = []
}
})
}
}
})
this.ruleForm.dynamicFieldAndValue = this.$tools.convertDataFormat(
this.ruleForm.dynamicFieldAndValue,
'render'
)
console.log(this.ruleForm, 'this.ruleForm')
}
},
// deep: true,
// immediate: true,
},
},
data() {
return {
configShow: false, // 右侧
basicShow: true,
inputShow: true,
outputShow: true,
paramsShow: true,
labelPosition: 'top',
ruleForm: {
id: '',
name: '',
outputReaderConfig: {
outputReaderType: '',
content: [],
},
runnerType: 'runnerTask',
dynamicFieldAndValue: [],
},
rules: {
engineAccount: [
{ required: true, message: '请填写内容', trigger: 'blur' },
{ max: 100, message: '最多填写100个字符', trigger: 'blur' },
],
apiKey: [
{ required: true, message: '请填写API KEY', trigger: 'blur' },
{ max: 100, message: '最多填写100个字符', trigger: 'blur' },
],
description: [
{ required: true, message: '请填写描述', trigger: 'blur' },
],
},
keywordType: {
1: 'IP',
2: '域名',
3: '企业名称',
4: 'URL',
5: 'IPPort',
6: '业务关键词',
7: '自定义查询语法',
8: '根域名',
},
pocShow: false,
pocIds: [], // 传给后端的id
isSelectPoc: false,
}
},
mounted() {},
methods: {
close() {
this.configShow = false
},
confirm(ids) {
console.log(ids)
this.pocIds = ids
},
selectPoc() {
this.pocShow = true
},
// 组装过滤器数据
setFilterData(id) {
let arr = []
const graphData = this.$parent.getGraphData()
const targetEdge = graphData.filter((el) => el?.target?.cell === id) // target 连线数组
const sourceNodeIds = targetEdge.map((el) => el.source.cell) // 源节点数据ID
graphData.forEach((el) => {
if (sourceNodeIds.includes(el.id)) {
if (el.data.engine.outputReaderConfig.length) {
let outputReaderConfig =
el.data.engine.outputReaderConfig[0].content // 上一个节点的输出参数
outputReaderConfig.forEach((item) => {
let label = ''
let filter = ''
if (item?.attributes?.filter) {
filter = item.attributes.filter
}
if (filter) {
label = `${this.keywordType[item.keywordType]}(${filter})`
} else {
label = `${this.keywordType[item.keywordType]}`
}
arr.push({
label: label,
value: JSON.stringify(item),
})
})
}
}
})
// 去重
let newArrId = []
let newArrObj = []
arr.forEach((item) => {
if (!newArrId.includes(item.label)) {
newArrId.push(item.label)
newArrObj.push(item)
}
})
return newArrObj
},
keywordTypeFn(item) {
let str = ''
let name = ''
if (item?.attributes?.filter) {
name = item.attributes.filter
}
if (name) {
if (this.keywordType[item.keywordType] == name) {
str = `${this.keywordType[item.keywordType]}`
} else {
str = `${name}:${this.keywordType[item.keywordType]}`
}
} else {
str = `${this.keywordType[item.keywordType]}`
}
return str
},
// 保存整个画布
saveStrategy(formName) {
// console.log(this.saveType, 4444)
// console.log(this.$store.state.taskStrategy.strategyInfo);
// console.log(this.$parent.getGraphData());
if (this.isSelectPoc) {
if (!this.pocIds.length) {
this.$message.error('至少选择一个poc!')
return
}
}
if (this.saveType === 'strategy') {
this.ruleForm.dynamicFieldAndValue = this.$tools.convertDataFormat(
this.ruleForm.dynamicFieldAndValue,
'save'
)
// 选择poc组装数据
this.ruleForm.dynamicFieldAndValue.forEach((el) => {
if (el.code === 'isSelectPoc') {
if (el.child && el.child.length > 0) {
el.child.forEach((item2, index2) => {
item2.value = this.pocIds.join(',')
})
}
}
})
const { strategyName, strategyDesc, strategyType, id } =
this.$store.state.taskStrategy.strategyInfo
let strategyConfig = this.$parent.getGraphData()
for (let i = 0; i < strategyConfig.length; i++) {
if (strategyConfig[i].id === this.nodeId) {
strategyConfig[i].data.engine = this.ruleForm
}
}
this.$refs[formName].validate(async (valid) => {
if (valid) {
const params = {
strategyName: strategyName,
strategyDesc: strategyDesc,
strategyType: strategyType,
strategyConfig: JSON.stringify(strategyConfig),
id: id,
}
const {
code,
data = {},
message,
} = await this.$store.dispatch('taskStrategy/saveStrategy', params)
if (code !== 0) {
this.$message.error(message)
return
}
this.close()
} else {
console.log('error submit!!')
return false
}
})
} else {
console.log('保存画布数据-不调用接口')
this.ruleForm.dynamicFieldAndValue = this.$tools.convertDataFormat(
this.ruleForm.dynamicFieldAndValue,
'save'
)
// 选择poc组装数据
this.ruleForm.dynamicFieldAndValue.forEach((el) => {
if (el.code === 'isSelectPoc') {
if (el.child && el.child.length > 0) {
el.child.forEach((item2, index2) => {
item2.value = this.pocIds.join(',')
})
}
}
})
let strategyConfig = this.$parent.getGraphData()
for (let i = 0; i < strategyConfig.length; i++) {
if (strategyConfig[i].id === this.nodeId) {
strategyConfig[i].data.engine = this.ruleForm
}
}
this.close()
console.log(this.ruleForm, 'this.ruleForm')
}
},
addClick(item, index) {
this.ruleForm.dynamicFieldAndValue[index].child.push({
type: 'string',
componentConfig: {
componentType: 'input',
required: false,
show: true,
},
})
},
delClick(item, index) {
this.ruleForm.dynamicFieldAndValue[index].child.splice(
this.ruleForm.dynamicFieldAndValue[index].child.indexOf(item),
1
)
},
// poc开关
switchChange(val, item) {
// console.log(val, item)
if (item.code === 'isSelectPoc') {
this.isSelectPoc = val
}
},
},
}
</script>
<style lang="scss" scoped>
.config-wrap {
width: 550px;
// padding: 20px 36px;
background: $-white;
// height: calc(100vh - 235px);
height: 100%;
.right-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid rgb(50, 54, 67);
p {
font-family: PingFangSC-Medium;
font-size: 16px;
font-weight: normal;
line-height: 22px;
letter-spacing: 0px;
// color: rgba(255, 255, 255, 0.9);
}
i {
width: 12px;
height: 12px;
cursor: pointer;
// color: rgba(255, 255, 255, 0.9);
}
}
.right-content {
height: calc(100% - 110px);
overflow-y: auto;
}
.right-basicInfo {
padding: 20px 12px;
margin: 12px;
border-radius: 8px;
opacity: 1;
// background: rgba(49, 52, 65, 0.5);
.basic-info {
font-family: PingFang SC;
font-size: 14px;
font-weight: normal;
// color: #ffffff;
cursor: pointer;
i {
font-size: 10px;
color: #8d93a2;
}
}
.basic-content {
.child-box {
padding-left: 30px;
.array-item {
display: flex;
align-items: center;
i {
font-size: 14px;
cursor: pointer;
margin-left: 8px;
}
.el-icon-minus {
color: #ff4c4c;
}
.el-icon-plus {
color: #0289f7;
}
}
}
}
.search-table {
.tooltip-box {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
> ul {
display: flex;
align-items: center;
flex-flow: wrap;
gap: 8px;
margin-top: 10px;
li {
display: flex;
align-items: center;
background: rgba(58, 206, 255, 0.1);
border: 0.5px solid rgba(58, 206, 255, 0.6);
border-radius: 4px;
height: 26px;
color: rgb(58, 206, 255);
font-size: 12px;
padding: 0px 12px;
}
}
.name {
font-family: PingFangSC-Medium;
font-size: 14px;
font-weight: normal;
line-height: 40px;
letter-spacing: 0em;
// color: rgba(255, 255, 255, 0.9);
}
::v-deep .el-form-item--small.el-form-item {
margin-bottom: 10px;
}
.apiClass {
::v-deep .el-form-item__label {
width: 100%;
text-align: left;
}
.updateClass {
font-family: PingFangSC-Regular;
font-size: 12px;
font-weight: normal;
line-height: 20px;
color: #3a7dff;
}
}
}
.right-footer {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 55px;
border-top: 1px solid rgb(50, 54, 67);
// padding: 18px;
position: absolute;
bottom: 0px;
right: 0;
.sure {
background: #0289f7;
}
}
}
</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>