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

news2024/9/21 16:48:43

代码:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Security ❀ TCP异常报文详解

文章目录 1. TCP Out-Of-Order2. TCP Previous Segment Lost3. TCP Retransmission4. TCP Dup Ack XXX#X5. TCP Windows Update6. TCP Previous segment not captured7. 异常案例分析 TCP协议中seq和ack seq的联系&#xff1a; id4的http请求报文由客户端发向服务器&#xff0…

C++ 数论相关题目 台阶-Nim游戏

现在&#xff0c;有一个 n 级台阶的楼梯&#xff0c;每级台阶上都有若干个石子&#xff0c;其中第 i 级台阶上有 ai 个石子(i≥1 )。 两位玩家轮流操作&#xff0c;每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中&#xff08;不能不拿&#xff09;。 已经拿到地面…

JAVA处理类似饼状图占比和100%问题,采用最大余额法

前言&#xff1a; 在做数据统计报表的时候&#xff0c;有两种方式解决占比总和达不到100%或者超过100%问题。 第一种方式是前端echart图自带的算分框架。 第二种方式是java后端取处理这个问题。 现存问题&#xff1a; 前端通过饼状图的方式去展示各个分类的占比累加和为100%问题…

vue前端页面时间显示问题解决方法

解决方法&#xff0c; <template slot-scope"scope"><span>{{ parseTime(scope.row.boxClosingOnlineTime, {y}-{m}-{d} {h}:{i}:{s}) }}</span> </template> 刷新页面&#xff1a; 此外&#xff0c;使用JsonFormat(pattern "yyyy-M…

Kaggle竞赛系列_SpaceshipTitanic金牌方案分析_数据分析

文章目录 【文章系列】【前言】【比赛简介】【正文】&#xff08;一&#xff09;数据获取&#xff08;二&#xff09;数据分析1. 缺失值2. 重复值3. 属性类型分析4. 类别分析5. 分析目标数值占比 &#xff08;三&#xff09;属性分析1. 对年龄Age分析&#xff08;1&#xff09;…

OpenHarmony—环境准备

JS SDK安装失败处理指导 问题现象 下载JS SDK时&#xff0c;下载失败&#xff0c;提示“Install Js dependencies failed”。解决措施 JS SDK下载失败&#xff0c;一般情况下&#xff0c;主要是由于npm代理配置问题&#xff0c;或未清理npm缓存信息导致&#xff0c;可按照如…

如何更新github上fork的项目(需要一定git基础)

如何更新Fork的项目(需要一定git基础) 前言&#xff1a;本文记录一下自己在github上fork了大佬的开源博客项目https://github.com/tangly1024/NotionNext&#xff0c;如何使用git克隆以及自定义开发和同步合并原项目更新迭代内容的的步骤 如何更新fork的项目(进阶版) 首先你…

MyBatis 源码系列:MyBatis 解析配置文件、二级缓存、SQL

文章目录 解析全局配置文件二级缓存解析解析二级缓存缓存中的调用过程缓存中使用的设计模式 解析SQL 解析全局配置文件 启动流程分析 String resource "mybatis-config.xml"; //将XML配置文件构建为Configuration配置类 reader Resources.getResourceAsReader(re…

华天动力OA ntkodownload.jsp 任意文件读取漏洞复现

0x01 产品简介 华天动力OA是一款将先进的管理思想、 管理模式和软件技术、网络技术相结合,为用户提供了低成本、 高效能的协同办公和管理平台。 0x02 漏洞概述 华天动力OA ntkodownload.jsp接口处存在任意文件读取漏洞,未经身份认证的攻击者可利用此漏洞获取服务器内部敏感…

条款32:确定你的public继承塑模出 is-a 关系

如果你编写类D(“派生类”)public继承类B(“基类”)&#xff0c;就是在告诉C编译器(以及代码的读者)每个类型D的对象都是类型B的对象&#xff0c;但反之则不然。 class Person {...}; class Student: public Person {...}; void eat(const Person& p); // 素有的Person都…

笔记本从零安装ubuntu server系统+环境配置

文章目录 前言相关链接ubuntu Server 安装教程屏幕自动息屏关上盖子不休眠MobaXterm外网SSH内网穿透IPV6远程 为什么我要笔记本装Linux为什么要换ubuntu Server版能否连接wifi之后Linux 配置清单总结 前言 之前装了个ubuntu desktop 版&#xff0c;发现没有命令行&#xff0c;…

FLUENT Meshing Watertight Geometry工作流入门 - 3 BOI FOI

本视频中学到的内容&#xff1a; 如何使用和设置 “Body Of Influence”&#xff08;BOI&#xff09;、“Face Of Influence”&#xff08;FOI&#xff09;。 视频链接&#xff1a; 【FLUENT Meshing入门教程-局部尺寸控制3-BOI&FOI-哔哩哔哩】 https://b23.tv/UHYY828 在…

【React】前端项目引入阿里图标

【React】前端项目引入阿里图标 1、登录自己的iconfont-阿里巴巴矢量图标库&#xff0c;把需要的图标加入到自己的项目中去&#xff1b;2、加入并进入到项目中去选择Symbol点击复制代码3、安装ant-design/icons4. 新建一个MyIcon.js文件内容如下5、在项目中使用 1、登录自己的i…

【Linux笔记】文件描述符与重定向

一、Linux关于文件操作的一些系统调用 1、open和close 我们在C语言阶段已经学过很多文件操作的函数&#xff0c;今天我们要来看看操作系统中对于文件是怎么操作的。 1.1、open与close的用法 C语言的库函数中有很多关于文件操作的接口&#xff0c;包括fopen、fclose、fprint…

《Docker技术革命:从虚拟机到容器化,全面解析Docker的原理与应用-上篇》

文章目录 Docker为什么会出现总结 Docker的思想Docker历史总结 Docker能干嘛虚拟机技术虚拟机技术的缺点 容器化技术Docker和虚拟机技术的区别 Docker概念Docker的基本组成镜像&#xff08;image)容器&#xff08;container&#xff09;仓科&#xff08;repository&#xff09;…

基于大数据的B站数据分析系统的设计与实现

摘要&#xff1a;随着B站&#xff08;哔哩哔哩网&#xff09;在国内视频分享平台的崛起&#xff0c;用户规模和数据量不断增加。为了更好地理解和利用这些海量的B站数据&#xff0c;设计并实现了一套基于Python的B站数据分析系统。该系统采用了layui作为前端框架、Flask作为后端…

防火墙综合拓扑(NAT、双机热备)

实验需求 拓扑 实验注意点&#xff1a; 先配置双机热备&#xff0c;再来配置安全策略和NAT两台双机热备的防火墙的接口号必须一致如果其中一台防火墙有过配置&#xff0c;最好清空或重启&#xff0c;不然配置会同步失败两台防火墙同步完成后&#xff0c;可以直接在主状态防火墙…

逻辑回归与感知机详解

一逻辑回归 采用log函数作为代价函数 1 用于二分类问题 2 cost成本函数定义 3 求最小值&#xff0c;链式求导法则 4 梯度下降法 5 结构图表示 二 感知机 样本点到超平面距离法 1 线性二分类问题 2 点到直线距离 3 更新w 和b 参数 4 算法流程 5 例子

真机调试,微信小程序,uniapp项目在微信开发者工具中真机调试,手机和电脑要连同一个wifi,先清空缓存,页面从登录页进入,再点真机调试,这样就不会报错了

微信小程序如何本地进行真机调试&#xff1f;_unity生成的微信小程序怎么在电脑上真机测试-CSDN博客 微信小程序 真机调试 注意事项 uniapp项目在微信开发者工具中真机调试&#xff0c;手机和电脑要连同一个wifi&#xff0c;先清空缓存&#xff0c;页面从登录页进入&#xf…

mac系统内存越来越大怎么清理?CleanMyMac X轻松解决

MacBook作为一款优秀的笔记本电脑&#xff0c;自带的macOS操作系统更加稳定、安全、易用。但是&#xff0c;随着安装的应用越来越多&#xff0c;应用的功能越来越强大&#xff0c;占用的内存也越来越多&#xff0c;从而导致电脑变慢。那么&#xff0c;MacBook如何清理内存呢&am…