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>

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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/366275.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

URL重写

URL重写 URL重写是一种通过修改URL来管理用户会话的会话管理技术。由于URL容易在传输过程中被截取,因此该技术一般在要传输的信息不是很重要时才使用。例如,在线购物门户中,servlet可以修改URL以便包含用户名等用户信息。然后servlet显示该URL。用户单击URL超链接时,信息发…

ES6.8.6 Java客户端发起 增删改查 query (bool)、update、delete

文章目录 环境测试数据增单个新增批量新增 删通过delete by api删除通过delete by query api删除删除索引中指定字段&#xff08;script&#xff09; 改单个修改update by api通过_bulk批量修改批量修改update by query api使用script脚本修改 查完全匹配&#xff08;term&…

【Qt基本功修炼】Qt线程的两种运行模式

1. 前言 QThread是Qt中的线程类&#xff0c;用于实现多线程运行。 QThread有两种工作模式&#xff0c;即 消息循环模式无消息循环模式 两种模式分别适用于不同的场景。下面我们将从多个方面&#xff0c;讲解QThread两种工作模式的区别。 2. 消息循环模式 2.1 实现原理 Q…

nightinage部署

git开源地址 GitHub - ccfos/nightingale: An all-in-one observability solution which aims to combine the advantages of Prometheus and Grafana. It manages alert rules and visualizes metrics, logs, traces in a beautiful web UI. 一、下载源码自己编译运行 二、用…

在本机搭建私有NuGet服务器

写在前面 有时我们不想对外公开源码&#xff0c;同时又想在VisualStudio中通过Nuget包管理器来统一管理这些内部动态库&#xff0c;这时就可以在本地搭建一个NuGet服务器。 操作步骤 1.下载BaGet 这是一个轻量级的NuGet服务器 2.部署BaGet 将下载好的release版本BaGet解…

字符下标计数

下标计数 数组计数&#xff0c;即通过使用一个新的数组&#xff0c;对原来数组里面的项进行计数&#xff0c;统计原来数组中各项出现的次数&#xff0c;如下图所示&#xff1a; 数组计数可以方便快速地统计出一个各项都比较小的数组中&#xff0c;数值相同的数的个数。 数组计数…

代码随想录算法训练营DAY10 | 栈与队列 (1)

理论基础及Java实现参考文章&#xff1a;栈和队列 一、LeetCode 232 用栈实现队列 题目链接&#xff1a;232.用栈实现队列https://leetcode.cn/problems/implement-queue-using-stacks/ 思路&#xff1a;使用两个栈stack1、stack2实现队列&#xff1b;stack1用来存储入队元素&…

【Servlet】——Servlet API 详解

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Servlet】 本专栏旨在分享学习Servlet的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 一、HttpServlet二、Htt…

图像处理之《可逆重缩放网络及其扩展》论文精读

一、文章摘要 图像重缩放是一种常用的双向操作&#xff0c;它首先将高分辨率图像缩小以适应各种显示器或存储和带宽友好&#xff0c;然后将相应的低分辨率图像放大以恢复原始分辨率或放大图像中的细节。然而&#xff0c;非单射下采样映射丢弃了高频内容&#xff0c;导致逆恢复…

Flink的SQL开发

概叙 Flink有关FlinkSQL的官网: https://nightlies.apache.org/flink/flink-docs-release-1.13/zh/docs/dev/table/sql/overview/ 阿里云有关FlinkSQL的官网: https://help.aliyun.com/zh/flink/developer-reference/overview-5?spma2c4g.11186623.0.0.3f55bbc6H3LVyo Ta…

Mac 下文件编码转换的方法

Windows文件传输到Mac,在Windows上打开是可以看的,但是在Mac上打开是乱码,这是因为Windows默认是GBK编码,而Mac使用的是UTF-8编码,这时候需要对文件编码进行转换,以方便在Mac上查看和使用 iconv macOS 系统中&#xff0c;iconv 命令是一个用于转换文件或文本流的字符编码的实用…

Django模型(八)

一、修改数据 先获取对象,通过对象属性更新数据,再保存 (更新单一数据)通过QuerySet的update函数更新数据 (更新多条数据) #单条记录修改 save c = Cook.objects.get(pk=1) c.name = 安妮 c.save()# 更新多个值 update Cook.objects.filter(sect=粤菜).update(level=5)1.1、…

C# 根据USB设备VID和PID 获取设备总线已报告设备描述

总线已报告设备描述 DEVPKEY_Device_BusReportedDeviceDesc 模式 winform 语言 c# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Window…

IntelliJ IDEA 2023和Java的JDK详细安装教程

一、软件下载 网盘链接&#xff1a;IntelliJ IDEA 2023 提取码&#xff1a;2syx 二、详细安装教程 1.鼠标右击【JetBrains2023】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;选择【解压到 JetBrains2023】&#xff1b;打开解压后的文件夹&#x…

银行数据仓库体系实践(15)--数据应用之巴塞尔新资本协议

巴塞尔新资本协议介绍 在银行管理中经常会听到巴3、新资本协议等专用词&#xff0c;那这都是指《巴塞尔资本协议》&#xff0c;全称《关于统一国际银行资本衡量和资本标准的协议》。新资本协议的五大目标是&#xff1a;促进金融体系的安全性和稳健性&#xff08;保持总体资本水…

c++阶梯之auto关键字与范围for

auto关键字&#xff08;c11&#xff09; 1. auto关键字的诞生背景 随着程序的逐渐复杂&#xff0c;程序代码中用到的类型也越来越复杂。譬如&#xff1a; 类型难以拼写&#xff1b;含义不明确容易出错。 比如下面一段代码&#xff1a; #include <string> #include &…

(学习日记)2024.02.01:引用变量 / 默认实参 / 一元作用域运算符 / 函数重载

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

踩坑STM32CubeMX生成Makefile工程无法使用printf(“%f“)

过去一年偶有接触STM32开发时都是使用STM32CubeMX生成Makefile的工程&#xff0c;具体开发环境见配置Clion用于STM32开发&#xff08;Makefile&#xff09;&#xff0c;但没想到今天在使用printf打印输出浮点数时无法正常输出&#xff0c;不仅printf无法使用&#xff0c;其他涉…

MongoDB从入门到实战之MongoDB快速入门

前言 上一章节主要概述了MongoDB的优劣势、应用场景和发展史。这一章节将快速的概述一下MongoDB的基本概念&#xff0c;带领大家快速入门MongoDB这个文档型的NoSQL数据库。 MongoDB从入门到实战的相关教程 MongoDB从入门到实战之MongoDB简介&#x1f449; MongoDB从入门到实战…

白皮书发布,石油石化数字孪生加速

近日&#xff0c;《数字石化 孪生智造——石油石化数字孪生白皮书》发布。白皮书聚焦石油石化行业发展机遇&#xff0c;剖析数字孪生技术在行业中的案例实践与应用场景&#xff0c;展望石油石化企业未来孪生发展新态势。 当前&#xff0c;国家大力推动减污降碳协同增效&#x…