vue使用antv-x6 绘制流程图DAG图(二)

代码:

<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>

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值