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

news/2024/5/18 22:49:16 标签: vue.js, 流程图, 前端

代码:

<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.niftyadmin.cn/n/5354553.html

相关文章

网站分享(实用)

文章目录 网站分享简介素材和资源问题解决学习网站 网站分享 简介 在开发网站或应用程序时&#xff0c;我们经常需要使用各种素材和资源来增强用户体验和界面设计。在这篇文章中&#xff0c;我将分享一些我个人认为非常好用的网站&#xff0c;它们提供了丰富的素材和资源&…

基于Qt 视频播放器mp4/wav/avi/等等(进阶)

​## Qt音视频相关 Qt 多媒体音频模拟按钮发音(音视频启动)基于Qt 音乐播放器mp3(进阶)## 项目工具 工具名

万物简单AIoT 端云一体实战案例学习 之 智能小车

学物联网,来万物简单IoT物联网!! 下图是本案的3步导学,每个步骤中实现的功能请参考图中的说明。 1、简介 1.1、背景 市面上各种遥控的小车很多,小车的性能不同具备的能力也不一样,大概实现的逻辑就是通过遥控器控制小车的前进、后退、左转或者右转。遥控小车具备一定…

ModelArts加速识别,助力新零售电商业务功能的实现

前言 如果说为客户提供最好的商品是产品眼中零售的本质&#xff0c;那么用户的思维是什么呢&#xff1f; 在用户眼中&#xff0c;极致的服务体验与优质的商品同等重要。 企业想要满足上面两项服务&#xff0c;关键在于提升效率&#xff0c;也就是需要有更高效率的零售&#…

openGaussdb5.0单点企业版部署_KylinV10SP1

本文档环境&#xff1a;Kylin-Server-10-SP1 python2.7.16 交互式初始化环境方式 介绍 openGauss是一款开源关系型数据库管理系统&#xff0c;采用木兰宽松许可证v2发行。openGauss内核深度融合华为在数据库领域多年的经验&#xff0c;结合企业级场景需求&#xff0c;持续构建…

18.通过telepresence调试部署在Kubernetes上的微服务

Telepresence简介 在微服务架构中,本地开发和调试往往是一项具有挑战性的任务。Telepresence 是一种强大的工具,使得开发者本地机器上开发微服务时能够与运行在 Kubernetes 集群中的其他服务无缝交互。本文将深入探讨 Telepresence 的架构、运行原理,并通过实际的案例演示其…

关于maven项目构建的解释

在Idea中使用模块化构建项目 项目介绍&#xff1a; sky-server依赖sky-pojo和sky-common&#xff0c;继承sky-take-outsky-pojo继承sky-take-outsky-common继承sky-take-out 由于Idea编译器自动识别引入的模块&#xff0c;所以在Idea中可以运行项目。 在Idea中使用maven打包…

实用篇 | postman在AI模型中输入speech,text,image使用详解

Postman相关直达 实用篇 | 利用FlaskPostman为深度学习模型进行快速测试(超详细)_ CSDN博客 1.基础知识 1.1.postman发送请求(json和urlencoded) 1.1.1.发送 JSON 数据请求&#xff08;form-data&#xff09;&#xff1a; 要求&#xff1a; 请求方法&#xff1a;POST&#…