vue2+ant x6 实现 数据加工 DAG 图


npm install @antv/x6-plugin-selection@2.1.7 --save 
 npm install @antv/x6-vue-shape@2.0.11 --save 
npm install @antv/x6@2.9.7 --save 

npm i --save @vue/composition-api@1.7.2

index.vue

<template>
  <!-- <div>
    <div id="oneNode"></div>
  </div> -->
  <div id="coverCot"  style="width: 100%; height: 500px;">
    <div class="section-cot" style="width: 100%; height: 100%">
      <div id="container" @click.stop="hideFn">
        <MenuBar
          v-if="showContextMenu"
          ref="menuBar"
          @callBack="contextMenuFn"
        />
        <header>
          <el-tooltip
            class="item"
            effect="dark"
            content="放大"
            placement="bottom"
          >
            <i class="el-icon-zoom-in" @click="zoomFn(0.2)" />
          </el-tooltip>
          <el-tooltip
            class="item"
            effect="dark"
            content="缩小"
            placement="bottom"
          >
            <i class="el-icon-zoom-out" @click="zoomFn(-0.2)" />
          </el-tooltip>
          <el-tooltip
            class="item"
            effect="dark"
            content="适应屏幕"
            placement="bottom"
          >
            <i class="el-icon-full-screen" @click="centerFn" />
          </el-tooltip>
          <el-tooltip
            class="item"
            effect="dark"
            content="执行"
            placement="bottom"
          >
            <i class="el-icon-video-play" @click="startFn()" />
          </el-tooltip>
          <el-tooltip
            class="item"
            effect="dark"
            content="保存"
            placement="bottom"
          >
            <i class="el-icon-upload" @click="saveFn()" />
          </el-tooltip>
          <el-tooltip
            class="item"
            effect="dark"
            content="加载保存页面"
            placement="bottom"
          >
            <i class="el-icon-link" @click="loadFn()" />
          </el-tooltip>
        </header>
        <div id="oneNode" />
      </div>
    </div>
    <!-- <DialogCondition ref="dialogCondition"></DialogCondition> -->
    <DialogMysql ref="dialogMysql"></DialogMysql>
  </div>
</template>
 
<script>
// import { Graph } from '@antv/x6';
import { register } from '@antv/x6-vue-shape';
import { Graph, Node, Path, Edge, Platform, StringExt } from '@antv/x6'
import { Selection } from '@antv/x6-plugin-selection'
import DagNode from './components/dag';
import MenuBar from "./components/menuBar";
// import DialogCondition from "./components/dialog/condition.vue";
import DialogMysql from "./components/dialog/mysql.vue";
 let graph = null;
export default {
  components: { MenuBar, DialogMysql },
  data() {
    return {
      showContextMenu: false,
      nodeData:{
        "nodes": [
          {
            "id": "node-0",
            "shape": "data-processing-dag-node",
            "x": 0,
            "y": 100,
            "ports": [
              {
                "id": "node-0-out",
                "group": "out"
              }
            ],
            "data": {
              "name": "数据输入_1",
              "type": "INPUT",
              "checkStatus": "sucess"
            }
          },
          {
            "id": "node-1",
            "shape": "data-processing-dag-node",
            "x": 250,
            "y": 100,
            "ports": [
              {
                "id": "node-1-in",
                "group": "in"
              },
              {
                "id": "node-1-out",
                "group": "out"
              }
            ],
            "data": {
              "name": "数据筛选_1",
              "type": "FILTER"
            }
          },
          {
            "id": "node-2",
            "shape": "data-processing-dag-node",
            "x": 250,
            "y": 200,
            "ports": [
              {
                "id": "node-2-out",
                "group": "out"
              }
            ],
            "data": {
              "name": "数据输入_2",
              "type": "INPUT"
            }
          },
          {
            "id": "node-3",
            "shape": "data-processing-dag-node",
            "x": 500,
            "y": 100,
            "ports": [
              {
                "id": "node-3-in",
                "group": "in"
              },
              {
                "id": "node-3-out",
                "group": "out"
              }
            ],
            "data": {
              "name": "数据连接_1",
              "type": "JOIN"
            }
          },
          {
            "id": "node-4",
            "shape": "data-processing-dag-node",
            "x": 750,
            "y": 100,
            "ports": [
              {
                "id": "node-4-in",
                "group": "in"
              }
            ],
            "data": {
              "name": "数据输出_1",
              "type": "OUTPUT"
            }
          }
        ],
        "edges": [
          {
            "id": "edge-0",
            "source": {
              "cell": "node-0",
              "port": "node-0-out"
            },
            "target": {
              "cell": "node-1",
              "port": "node-1-in"
            },
            "shape": "data-processing-curve",
            "zIndex": -1,
            "data": {
              "source": "node-0",
              "target": "node-1"
            }
          },
          {
            "id": "edge-1",
            "source": {
              "cell": "node-2",
              "port": "node-2-out"
            },
            "target": {
              "cell": "node-3",
              "port": "node-3-in"
            },
            "shape": "data-processing-curve",
            "zIndex": -1,
            "data": {
              "source": "node-2",
              "target": "node-3"
            }
          },
          {
            "id": "edge-2",
            "source": {
              "cell": "node-1",
              "port": "node-1-out"
            },
            "target": {
              "cell": "node-3",
              "port": "node-3-in"
            },
            "shape": "data-processing-curve",
            "zIndex": -1,
            "data": {
              "source": "node-1",
              "target": "node-3"
            }
          },
          {
            "id": "edge-3",
            "source": {
              "cell": "node-3",
              "port": "node-3-out"
            },
            "target": {
              "cell": "node-4",
              "port": "node-4-in"
            },
            "shape": "data-processing-curve",
            "zIndex": -1,
            "data": {
              "source": "node-3",
              "target": "node-4"
            }
          }
        ]
      },
      // 元素校验状态
      CellStatus: {
          DEFAULT:'default',
          SUCCESS:'success',
          ERROR:'error',
      },
      // 节点状态列表
      nodeStatusList: [
        {
          id: 'node-0',
          status: 'success',
        },
        {
          id: 'node-1',
          status: 'success',
        },
        {
          id: 'node-2',
          status: 'success',
        },
        {
          id: 'node-3',
          status: 'success',
        },
        {
          id: 'node-4',
          status: 'error',
          statusMsg: '错误信息示例',
        },
      ],
      // 边状态列表
      edgeStatusList: [
        {
          id: 'edge-0',
          status: 'success',
        },
        {
          id: 'edge-1',
          status: 'success',
        },
        {
          id: 'edge-2',
          status: 'success',
        },
        {
          id: 'edge-3',
          status: 'success',
        },
      ],
    };
  },
 
  mounted() {
    // 1. 注册节点
    register({
      // shape: 'data-processing-dag-node',
      // width: 212,
      // height: 48,
      // component: DagNode,
      shape: 'data-processing-dag-node',
      width: 212,
      height: 48,
      component: DagNode,
      // port默认不可见
      ports: {
        groups: {
          in: {
            position: 'left',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: 'transparent',
                strokeWidth: 1,
                fill: 'transparent',
              },
            },
          },

          out: {
            position: {
              name: 'right',
              args: {
                dx: -32,
              },
            },

            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: 'transparent',
                strokeWidth: 1,
                fill: 'transparent',
              },
            },
          },
        },
      },
    });
 
   
    // 注册连线
    Graph.registerConnector(
      'curveConnector',
      (sourcePoint, targetPoint) => {
        const hgap = Math.abs(targetPoint.x - sourcePoint.x)
        const path = new Path()
        path.appendSegment(
          Path.createSegment('M', sourcePoint.x - 4, sourcePoint.y),
        )
        path.appendSegment(
          Path.createSegment('L', sourcePoint.x + 12, sourcePoint.y),
        )
        // 水平三阶贝塞尔曲线
        path.appendSegment(
          Path.createSegment(
            'C',
            sourcePoint.x < targetPoint.x
              ? sourcePoint.x + hgap / 2
              : sourcePoint.x - hgap / 2,
            sourcePoint.y,
            sourcePoint.x < targetPoint.x
              ? targetPoint.x - hgap / 2
              : targetPoint.x + hgap / 2,
            targetPoint.y,
            targetPoint.x - 6,
            targetPoint.y,
          ),
        )
        path.appendSegment(
          Path.createSegment('L', targetPoint.x + 2, targetPoint.y),
        )

        return path.serialize()
      },
      true,
    )

    Edge.config({
      markup: [
        {
          tagName: 'path',
          selector: 'wrap',
          attrs: {
            fill: 'none',
            cursor: 'pointer',
            stroke: 'transparent',
            strokeLinecap: 'round',
          },
        },
        {
          tagName: 'path',
          selector: 'line',
          attrs: {
            fill: 'none',
            pointerEvents: 'none',
          },
        },
      ],
      connector: { name: 'curveConnector' },
      attrs: {
        wrap: {
          connection: true,
          strokeWidth: 10,
          strokeLinejoin: 'round',
        },
        line: {
          connection: true,
          stroke: '#A2B1C3',
          strokeWidth: 1,
          targetMarker: {
            name: 'classic',
            size: 6,
          },
        },
      },
    })

    Graph.registerEdge('data-processing-curve', Edge, true)

     // 2. 创建画布
    graph = new Graph({
      // grid: {
      //     size: 10,
      //     visible: true,
      //     type: "dot", // 'dot' | 'fixedDot' | 'mesh'
      //     args: {
      //       color: "#a05410", // 网格线/点颜色
      //       thickness: 1, // 网格线宽度/网格点大小
      //     },
      //   },
        background: {
          color: "#fffbe6", // 设置画布背景颜色
        },
      container: document.getElementById('oneNode'),
      // width: 1000,
      // height: 1000,
      // container: document.getElementById('container'),
      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,
        sourceAnchor: {
          name: 'left',
          args: {
            dx: Platform.IS_SAFARI ? 4 : 8,
          },
        },
        targetAnchor: {
          name: 'right',
          args: {
            dx: Platform.IS_SAFARI ? 4 : -8,
          },
        },
        createEdge() {
          return graph.createEdge({
            shape: 'data-processing-curve',
            attrs: {
              line: {
                strokeDasharray: '5 5',
              },
            },
            zIndex: -1,
          })
        },
        // 连接桩校验
        validateConnection({ sourceMagnet, targetMagnet }) {
          // 只能从输出链接桩创建连接
          if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') {
            return false
          }
          // 只能连接到输入链接桩
          if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') {
            return false
          }
          return true
        },
      },
    });
    graph.use(
      new Selection({
        multiple: true,
        rubberEdge: true,
        rubberNode: true,
        modifiers: 'shift',
        rubberband: true,
      }),
    )
    // 单击节点获取节点信息
    graph.on('node:click', (e) => {
        const nodeItem = e.cell // 获取被点击的节点元素对象
        console.log('单击', nodeItem)
    })
    //右键点击事件显示配置数据和删除弹窗
    graph.on("node:contextmenu", ({ e, x, y, node, view }) => {
        console.log(node,"===============node");
        this.showContextMenu = true;
        this.$nextTick(() => {
          // this.$refs.menuBar.setItem({ type: 'node', item: node })
          const p = graph.localToPage(x, y);
          // this.$refs.menuBar.initFn(p.x, p.y, { type: "node", item: node });
          console.log(x, y,"========x, y")
          this.$refs.menuBar.initFn(x+80, y, { type: "node", item: node });
        });
      });
     

    // // 3. 根据json数据创建节点,此处只取第一个
    // let map = {};
    // map.nodes = dagMap.nodes;
    // graph.fromJSON(map);
 
    // // 4. 设置节点状态
    // let { id, status, statusMsg } = this.nodeStatusList[0];
    // let node = graph.getCellById(id);
    // let data = node.getData();
    // node.setData({
    //   ...data,
    //   status,
    //   statusMsg,
    // });
    this.init(this.nodeData);
  },
  methods: {
    getNodeById(id) {
      return graph.getCellById(id);
    },
    hideFn() {
      this.showContextMenu = false;
    },
     //右键弹窗删除
    contextMenuFn(type, node) {
      console.log(node,"===============2222222")
      switch (type) {
        case "remove":
          if (node.type == "edge") {
            graph.removeEdge(node.item.id);
          } else if (node.type == "node") {
            graph.removeNode(node.item.id);
          }
          break;
        case "source":
          this.$refs.dialogMysql.visible = true;
          this.$refs.dialogMysql.init(node);
          break;
      }
      this.showContextMenu = false;
    },
    //初始获取化工艺图
    init(data){
      graph.fromJSON(data)
      const zoomOptions = {
        padding: {
          left: 10,
          right: 10,
        },
      }
      graph.zoomToFit(zoomOptions)
      setTimeout(() => {
        this.excuteAnimate()
      }, 2000)
      setTimeout(() => {
        this.showNodeStatus()
        this.stopAnimate()
      }, 3000)
    },
    // 显示节点状态
    showNodeStatus(){
      this.nodeStatusList.forEach((item) => {
        const { id, status, statusMsg } = item
        const node = graph.getCellById(id)
        // const data = node.getData() as CellStatus
        // name: "数据输出_1"
        // type: "OUTPUT"
        const data = node.getData()
        node.setData({
          ...data,
          status,
          statusMsg,
        })
      })
    },

    // 开启边的运行动画
    excuteAnimate() {
      graph.getEdges().forEach((edge) => {
        edge.attr({
          line: {
            stroke: '#3471F9',
          },
        })
        edge.attr('line/strokeDasharray', 5)
        edge.attr('line/style/animation', 'running-line 30s infinite linear')
      })
    },
    // 关闭边的动画
    stopAnimate() {
      graph.getEdges().forEach((edge) => {
        edge.attr('line/strokeDasharray', 0)
        edge.attr('line/style/animation', '')
      })
      this.edgeStatusList.forEach((item) => {
        const { id, status } = item
        const edge = graph.getCellById(id)
        if (status === 'success') {
          edge.attr('line/stroke', '#52c41a')
        }
        if (status === 'error') {
          edge.attr('line/stroke', '#ff4d4f')
        }
      })
      // 默认选中一个节点
      graph.select('node-2')
    },
    
    //执行
    startFn(item) {
      console.log(item,"=========item")
      this.init(item || this.nodeData);
      graph.centerContent();
    },
    //放大缩小
    zoomFn(num) {
      graph.zoom(num);
    },
     //屏幕自适应
    centerFn() {
      const num = 1 - graph.zoom();
      num > 1 ? graph.zoom(num * -1) : graph.zoom(num);
      graph.centerContent();
    },
    // 保存
    saveFn() {
      localStorage.setItem(
        "x6Json",
        JSON.stringify(graph.toJSON({ diff: true }))
      );
      console.log(graph.toJSON({ diff: true }))
    },
    //图表加载保存数据
    loadFn() {
      this.timer && clearTimeout(this.timer);
      const x6Json = JSON.parse(localStorage.getItem("x6Json"));
      this.startFn(x6Json.cells);
    },

  },
  
};
</script>
<style scoped>
header {
  display: flex;
  justify-content: flex-end;
  width: 100%;
  height: 50px;
  box-sizing: border-box;
}

header i {
  margin: 8px;
  font-size: 30px;
}
.section-cot {
  display: flex;
}

.section-cot #container {
  position: relative;
  flex: 1;
}

.section-cot #container #oneNode {
  width: 100%;
  height: 450px;
}
</style>

components   menuBar.vue

<template>
  <el-card
    class="box-card"
    :style="{ left: x + 'px', top: y + 'px' }"
    :stop="11"
    @click.stop=""
    style="padding: 0"
  >
    <div
      class="text item"
      @click.stop="callBack('source')"
      v-if="item.type !== 'edge'"
    >
      配置数据源
    </div>
    <div class="text item" @click.stop="callBack('remove')">删除</div>
  </el-card>
</template>

<script>
export default {
  name: "MenuBar",
  data() {
    return {
      x: "",
      y: "",
      item: {},
    };
  },
  mounted() {},
  methods: {
    initFn(x, y, item) {
      this.x = parseInt(x) + "";
      this.y = y + "";
      if (item) {
        this.item = item;
      }
    },
    setItem(item) {
      this.item = item;
    },
    callBack(type) {
      this.$emit("callBack", type, this.item);
    },
  },
};
</script>
<style scoped>
.box-card {
  position: absolute;
  z-index: 112;
}

.box-card ::v-deep .el-card__body {
  padding: 0;
}

.text {
  font-size: 14px;
}

.item {
  padding: 10px 0;
  text-align: center;
}

.item:hover {
  color: #ffffff;
  background-color: #409eff;
}

.box-card {
  width: 150px;
}
</style>

dag.vue

<template>
  <div class="data-processing-dag-node">
    <div
      class="main-area"
      @mouseenter="onMainMouseEnter"
      @mouseleave="onMainMouseLeave"
    >
      <div class="main-info">
        <!-- 节点类型icon -->
        <i
          class="node-logo"
          :style="{
            backgroundImage: 'url(' + global.NODE_TYPE_LOGO[type] + ')',
          }"
        />
        <el-tooltip :content="name" placement="top" :open-delay="800">
          <div class="ellipsis-row node-name">{{ name }}</div>
        </el-tooltip>
      </div>
 
      <!-- 节点状态信息 -->
      <div class="status-action">
        <el-tooltip
          :content="statusMsg"
          v-if="status === global.CellStatus.ERROR"
          placement="top"
        >
          <i class="status-icon status-icon-error" />
        </el-tooltip>
        <i
          class="status-icon status-icon-success"
          v-if="status === global.CellStatus.SUCCESS"
        />
        <!-- 节点操作菜单 -->
        <div class="more-action-container">
          <i class="more-action" />
        </div>
      </div>
    </div>
 
    <!-- +号菜单 -->
    <div class="plus-dag" v-if="type !== global.NodeType.OUTPUT">
      <el-dropdown trigger="click">
        <i class="plus-action" />
        <!-- <i class="el-icon-circle-plus-outline el-icon--right"></i> -->
        <el-dropdown-menu
          slot="dropdown"
          placement="bottom"
          class="processing-node-menu"
        >
          <el-dropdown-item
            v-for="(item, index) in global.PROCESSING_TYPE_LIST"
            :key="index"
          >
            <div
              class="node-dropdown-item"
              @click="clickPlusDragMenu(item.type)"
            >
              <i
                class="node-mini-logo"
                :style="{
                  backgroundImage: `url(${global.NODE_TYPE_LOGO[item.type]})`,
                }"
              />
              {{ item.name }}
            </div>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>
 
<script>
// import dagDictionary from './dagDictionary';
 
export default {
  inject: ['getNode'],
 
  data() {
    return {
      name: '',
      status: '',
      statusMsg: '',
      type: '',
      global: {
        // 节点类型
        NodeType:  {
            INPUT: 'INPUT', // 数据输入
            FILTER: 'FILTER', // 数据过滤
            JOIN: 'JOIN', // 数据连接
            UNION: 'UNION', // 数据合并
            AGG:'AGG', // 数据聚合
            OUTPUT:'OUTPUT', // 数据输出
        },
        // 元素校验状态
        CellStatus: {
            DEFAULT:'default',
            SUCCESS:'success',
            ERROR:'error',
        },
        // 节点位置信息
        Position: {
            x: 0,
            y: 100
        },
        // 加工类型列表
        PROCESSING_TYPE_LIST: [
            {
                type: 'FILTER',
                name: '数据筛选',
            },
            {
                type: 'JOIN',
                name: '数据连接',
            },
            {
                type: 'UNION',
                name: '数据合并',
            },
            {
                type: 'AGG',
                name: '数据聚合',
            },

            {
                type: 'OUTPUT',
                name: '数据输出',
            },
        ],
        // 不同节点类型的icon
        NODE_TYPE_LOGO: {
            INPUT:
                'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*RXnuTpQ22xkAAAAAAAAAAAAADtOHAQ/original', // 数据输入
            FILTER:
                'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ZJ6qToit8P4AAAAAAAAAAAAADtOHAQ/original', // 数据筛选
            JOIN: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*EHqyQoDeBvIAAAAAAAAAAAAADtOHAQ/original', // 数据连接
            UNION:
                'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k4eyRaXv8gsAAAAAAAAAAAAADtOHAQ/original', // 数据合并
            AGG: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*TKG8R6nfYiAAAAAAAAAAAAAADtOHAQ/original', // 数据聚合
            OUTPUT:
                'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*zUgORbGg1HIAAAAAAAAAAAAADtOHAQ/original', // 数据输出
        },

      }
    };
  },
 
  mounted() {
    let node = this.getNode();
    // 初始化数据绑定
    this.mapper(node.data, this.$data);
    // 节点数据变化监听,从而绑定数据
    node.on('change:data', ({ current }) => this.mapper(current, this.$data));
  },
 
  methods: {
    mapper(source, target) {
      for (let key in target) {
        target[key] = source[key] ?? target[key];
      }
    },
 
    // 鼠标进入矩形主区域的时候显示连接桩
    onMainMouseEnter() {
      let node = this.getNode();
      // 获取该节点下的所有连接桩
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: '#fff',
          stroke: '#85A5FF',
        });
      });
    },
 
    // 鼠标离开矩形主区域的时候隐藏连接桩
    onMainMouseLeave() {
      let node = this.getNode();
      // 获取该节点下的所有连接桩
      const ports = node.getPorts() || [];
      ports.forEach((port) => {
        node.setPortProp(port.id, 'attrs/circle', {
          fill: 'transparent',
          stroke: 'transparent',
        });
      });
    },
 
    // 点击添加下游+号
    clickPlusDragMenu(type) {
      this.createDownstream(type);
    //   this.setState({
    //     plusActionSelected: false,
    //   });
    },
    // // 添加下游菜单的打开状态变化
    // onPlusDropdownOpenChange (value){
    //     this.setState({
    //     plusActionSelected: value,
    //     })
    // },
    // 创建下游的节点和边
    createDownstream(type) {
      let node = this.getNode();
      const { graph } = node.model || {};
      if (graph) {
        // 获取下游节点的初始位置信息
        const position = this.getDownstreamNodePosition(node, graph);
        // 创建下游节点
        const newNode = this.createNode(type, graph, position);
        const source = node.id;
        const target = newNode.id;
        // 创建该节点出发到下游节点的边
        this.createEdge(source, target, graph);
      }
    },
    
    /**
     * 创建边并添加到画布
     * @param source
     * @param target
     * @param graph
     */
    createEdge(source, target, graph) {
      const edge = {
        id: this.uuid(),
        shape: 'data-processing-curve',
        source: {
          cell: source,
          port: `${source}-out`,
        },
        target: {
          cell: target,
          port: `${target}-in`,
        },
        zIndex: -1,
        data: {
          source,
          target,
        },
      };
      if (graph) {
        graph.addEdge(edge);
      }
    },
 
    /**
     * 创建节点并添加到画布
     * @param type 节点类型
     * @param graph
     * @param position 节点位置
     * @returns
     */
    createNode(type, graph, position) {
      if (!graph) {
        return {};
      }
      let newNode = {};
      const sameTypeNodes = graph
        .getNodes()
        .filter((item) => item.getData()?.type === type);
      const typeName = this.global.PROCESSING_TYPE_LIST?.find(
        (item) => item.type === type
      )?.name;
      const id = this.uuid();
      const node = {
        id,
        shape: 'data-processing-dag-node',
        x: position?.x,
        y: position?.y,
        ports: this.getPortsByType(type, id),
        data: {
          name: `${typeName}_${sameTypeNodes.length + 1}`,
          type,
        },
      };
      newNode = graph.addNode(node);
      return newNode;
    },
 
    /**
     * 根据起点初始下游节点的位置信息
     * @param node 起始节点
     * @param graph
     * @returns
     */
    getDownstreamNodePosition(node, graph, dx = 250, dy = 100) {
      // 找出画布中以该起始节点为起点的相关边的终点id集合
      const downstreamNodeIdList = [];
      graph.getEdges().forEach((edge) => {
        const originEdge = edge.toJSON()?.data;
        if (originEdge.source === node.id) {
          downstreamNodeIdList.push(originEdge.target);
        }
      });
      // 获取起点的位置信息
      const position = node.getPosition();
      let minX = Infinity;
      let maxY = -Infinity;
      graph.getNodes().forEach((graphNode) => {
        if (downstreamNodeIdList.indexOf(graphNode.id) > -1) {
          const nodePosition = graphNode.getPosition();
          // 找到所有节点中最左侧的节点的x坐标
          if (nodePosition.x < minX) {
            minX = nodePosition.x;
          }
          // 找到所有节点中最x下方的节点的y坐标
          if (nodePosition.y > maxY) {
            maxY = nodePosition.y;
          }
        }
      });
 
      return {
        x: minX !== Infinity ? minX : position.x + dx,
        y: maxY !== -Infinity ? maxY + dy : position.y,
      };
    },
 
    // 根据节点的类型获取ports
    getPortsByType(type, nodeId) {
      let ports = [];
      switch (type) {
        case this.global.NodeType.INPUT:
          ports = [
            {
              id: `${nodeId}-out`,
              group: 'out',
            },
          ];
          break;
        case this.global.NodeType.OUTPUT:
          ports = [
            {
              id: `${nodeId}-in`,
              group: 'in',
            },
          ];
          break;
        default:
          ports = [
            {
              id: `${nodeId}-in`,
              group: 'in',
            },
            {
              id: `${nodeId}-out`,
              group: 'out',
            },
          ];
          break;
      }
      return ports;
    },
 
    uuid() {
      var s = [];
      var hexDigits = '0123456789abcdef';
      for (var i = 0; i < 32; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
      }
      s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010
      s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
      s[8] = s[13] = s[18] = s[23];
      var uuid = s.join('');
      return uuid;
    },
  },
 
//   computed: {
//     global: function () {
//       return dagDictionary;
//     },
//   },
};
</script>
<style lang="less" scoped>
.data-processing-dag-node {
    display: flex;
    flex-direction: row;
    align-items: center;
  }

  .main-area {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    padding: 12px;
    width: 180px;
    height: 48px;
    color: rgba(0, 0, 0, 65%);
    font-size: 12px;
    font-family: PingFangSC;
    line-height: 24px;
    background-color: #fff;
    box-shadow: 0 -1px 4px 0 rgba(209, 209, 209, 50%), 1px 1px 4px 0 rgba(217, 217, 217, 50%);
    border-radius: 2px;
    border: 1px solid transparent;
  }
  .main-area:hover {
    border: 1px solid rgba(0, 0, 0, 10%);
    box-shadow: 0 -2px 4px 0 rgba(209, 209, 209, 50%), 2px 2px 4px 0 rgba(217, 217, 217, 50%);
  }

  .node-logo {
    display: inline-block;
    width: 24px;
    height: 24px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 100%;
  }

  .node-name {
    overflow: hidden;
    display: inline-block;
    width: 70px;
    margin-left: 6px;
    color: rgba(0, 0, 0, 65%);
    font-size: 12px;
    font-family: PingFangSC;
    white-space: nowrap;
    text-overflow: ellipsis;
    vertical-align: top;
  }

  .status-action {
    display: flex;
    flex-direction: row;
    align-items: center;
  }

  .status-icon {
    display: inline-block;
    width: 24px;
    height: 24px;
  }

  .status-icon-error {
    background: url('https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ')
      no-repeat center center / 100% 100%;
  }

  .status-icon-success {
    background: url('https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ')
      no-repeat center center / 100% 100%;
  }

  .more-action-container {
    margin-left: 12px;
    width: 15px;
    height: 15px;
    text-align: center;
    cursor: pointer;
  }

  .more-action {
    display: inline-block;
    width: 3px;
    height: 15px;
    background: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*tFw7SIy-ttQAAAAAAAAAAAAADtOHAQ/original')
      no-repeat center center / 100% 100%;
  }

  .plus-dag {
    visibility: hidden;
    position: relative;
    margin-left: 12px;
    height: 48px;
  }

  .plus-action {
    position: absolute;
    top: calc(50% - 8px);
    left: 0;
    width: 16px;
    height: 16px;
    background: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ScX2R4ODfokAAAAAAAAAAAAADtOHAQ/original')
      no-repeat center center / 100% 100%;
    cursor: pointer;
  }
  .plus-action:hover {
    background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*tRaoS5XhsuQAAAAAAAAAAAAADtOHAQ/original');
  }

  .plus-action:active,
  .plus-action-selected {
    background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k9cnSaSmlw4AAAAAAAAAAAAADtOHAQ/original');
  }

  .x6-node-selected .main-area {
    border-color: #3471f9;
  }

  .x6-node-selected .plus-dag {
    visibility: visible;
  }

  .processing-node-menu {
    padding: 2px 0;
    width: 118px!important;
    background-color: #fff;
    box-shadow: 0 9px 28px 8px rgba(0, 0, 0, 5%), 0 6px 16px 0 rgba(0, 0, 0, 8%),
      0 3px 6px -4px rgba(0, 0, 0, 12%);
    border-radius: 2px;
  }
  .processing-node-menu ul {
    margin: 0;
    padding: 0;
  }
  .processing-node-menu li {
    list-style:none;
  }
  .each-sub-menu {
    padding: 6px 12px;
    width: 100%;
  }

  .each-sub-menu:hover {
    background-color: rgba(0, 0, 0, 4%);
  }

  .each-sub-menu a {
    display: inline-block;
    width: 100%;
    height: 16px;
    font-family: PingFangSC;
    font-weight: 400;
    font-size: 12px;
    color: rgba(0, 0, 0, 65%);
  }

  .each-sub-menu span {
    margin-left: 8px;
    vertical-align: top;
  }

  .each-disabled-sub-menu a {
    cursor: not-allowed;
      color: rgba(0, 0, 0, 35%);
  }

  .node-mini-logo {
    display: inline-block;
    width: 16px;
    height: 16px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 100%;
    vertical-align: top;
  }

  @keyframes running-line {
    to {
      stroke-dashoffset: -1000;
    }
  }
</style>

dialog   mysql.vue

<template>
  <el-dialog
    :title="node.item ? node.item.data.label : ''"
    :visible.sync="visible"
    width="600px"
  >
    <el-form :inline="true" class="demo-form-inline">
      <el-form-item label="名字11">
        <el-input v-model="label" placeholder="修改名字"></el-input>
      </el-form-item>
    </el-form>
    <footer class="footer">
      <el-button type="primary" @click="submit">确定</el-button>
    </footer>
  </el-dialog>
</template>

<script>
export default {
  name: "dialogMysql",

  data() {
    return {
      visible: false,
      bool: true,
      node: {},
      label: "",
    };
  },
  mounted() {},
  methods: {
    init(item) {
      this.node = item;
      this.label = item.item.data.name;
    },
    submit() {
      var node = this.$parent.getNodeById(this.node.item.id);
      node.setData(
        Object.assign({}, this.node.item.data, { name: this.label })
      );
      // this.node.setData({})
      // this.node.item.data.label = this.label;
      this.visible = false;
    },
  },
};
</script>
<style scoped>
section {
  display: flex;
  align-items: center;
  justify-content: center;
}

.footer {
  margin-top: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

### 关于Vue3与AntV X6结合使用的自动排版方法 在Vue3环境中集成并应用AntV X6的自动布局功能,能够显著提高表绘制效率和美观度。为了实现这一目标,开发者可以利用X6内置的各种布局算法来自动化节点位置安排。 #### 使用DAGRE布局引擎进行自动排版 一种常见的做法是采用`dagre`作为布局器来进行有向无环(Directed Acyclic Graph, DAG)类型的网络结构优化[^1]: ```javascript import { Graph } from '@antv/x6'; // 导入 dagre 布局插件 import dagreLayout from 'dagre'; const graph = new Graph({ container: document.getElementById('container'), width: 800, height: 600, }); graph.fromJSON(data); // 加载初始数据源 let layout = new dagre.graphlib.Layout(); layout.nodeSep(20).edgeSep(20).rankDir('TB').ranksep(50); function applyLayout() { const nodes = Array.from(graph.getNodes()); let g = new dagre.graphlib.Graph().setGraph({}).setDefaultEdgeLabel(() => ({})); nodes.forEach(node => { g.setNode(node.id, {width: node.size.width, height: node.size.height}); }); graph.edges().forEach(edge => { g.setEdge(edge.source.cell, edge.target.cell); }); layout.run(g); Object.keys(layout._nodes).forEach(id => { var node = graph.getCellById(id); if (!node) return; node.position( layout.getNode(id).x - node.size.width / 2, layout.getNode(id).y - node.size.height / 2 ); }); } applyLayout(); // 应用布局调整 ``` 此代码片段展示了如何创建一个简单的DAG,并通过调用`applyLayout()`函数执行一次性的自动排列过程。这里使用了`dagre`库提供的API设置了一些基本参数如节点间距(`nodeSep`)、边距(`edgeSep`)等以满足不同应用场景下的需求[^3]。 对于更复杂的拓扑关系或者特定行业内的ER(实体关系),可以根据实际业务逻辑定制相应的约束条件或选择其他适合的布局模式,比如树形布局(tree),圆形布局(circle),力导向布局(force-directed)[^2]。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值