自学内容网 自学内容网

Vue3+TypeScript完美实现AntVX6官方人工智能建模 DAG 图

简述:本文通过理解官方文档进行编写,实现官方的示例效果。

详情请见AntVX6官网:https://x6.antv.antgroup.com/

官方示例链接:https://x6.antv.antgroup.com/zh/examples/showcase/practices/#dag 

实现结果如下: 

代码解析:

代码解释为onMounted初始化页面顺序进行讲解

onMounted(() => {
  buildAntvX6Container();
  init(initData);
  showNodeStatus(nodeStatusList);
  // 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
  graph.value!.zoomToFit({ maxScale: 2 })
});

一、节点注册:

通过以下代码注册新的自定义节点dag-node,并使用组件化节点,组件节点代码请见后文。

register({
  shape: 'dag-node', // 自定义节点的名称
  width: 180, // 节点的宽度
  height: 36, // 节点的高度
  component: AlgoNode, // 与此节点关联的组件(通常用于渲染节点的内容)
  ports: { // 定义节点的连接点(ports)
    groups: {
      left: { // 左侧连接点组
        position: 'left', // 连接点的位置
        attrs: {
          circle: { // 连接点的形状和样式
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
      right: { // 右侧连接点组
        position: 'right', // 连接点的位置
        attrs: {
          circle: { // 连接点的形状和样式
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
    },
  }
});

二、边、连接器注册:

 以下代码为注册自定义边与连接器的样式(此连接器注册参考官方数据加工 DAG 图)

// 注册自定义边样式
Graph.registerEdge(
    "dag-edge",
    {
      inherit: "edge",
      attrs: {
        line: {
          stroke: "#C2C8D5",
          strokeWidth: 1,
          targetMarker: null,
        },
      },
    },
    true
);
//注册连接器的样式
Graph.registerConnector(
    'algo-connector',
    (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,
)

 三、新建画布

        设置新注册的节点、线、连接器样式,并开启数据监听!!!很重要!!!

const buildAntvX6Container = () => {
  const container = document.getElementById("antvcontainer");
  if (!container) {
    console.error("AntV X6 容器未找到");
    return;
  }
  // 创建图形实例
  graph.value = new Graph({
    container: container,
    // autoResize: true,
    width: 2000,
    height: 2000,
    panning: {
      enabled: true,
      eventTypes: ["leftMouseDown", "mouseWheel"],
    },
    mousewheel: true,
    background: {
      color: "#d9e4f5",
    },
    grid: {
      visible: true,
      type: "doubleMesh",
      args: [
        {
          color: "#eee",
          thickness: 1,
        },
        {
          color: "#ddd",
          thickness: 1,
          factor: 4,
        },
      ],
    },
    connecting: {
      connector: 'algo-connector',
      snap: { radius: 50 },
      allowBlank: false,
      allowLoop: false,
      allowNode: false,
      allowEdge: false,
      allowMulti: true,
      highlight: true,
      validateMagnet({ magnet }) {
        return magnet.getAttribute("port-group") !== "left";
      },
      createEdge() {
        return graph.value!.createEdge({
          shape: "dag-edge",
          attrs: {
            line: {
              strokeDasharray: "5 5",
            },
          },
          zIndex: -1,
        });
      },
    },
    highlighting: {
      magnetAvailable: {
        name: "stroke",
        args: {
          attrs: {
            fill: "#fff",
            stroke: "#31d0c6",
            strokeWidth: 4,
          },
        },
      },
    },
  });


  // 使用选择插件
  graph.value.use(
      new Selection ({
        multiple: true,
        rubberEdge: true,
        rubberNode: true,
        modifiers: 'shift',
        rubberband: true,
      })
  )
  // 监听边连接事件
  graph.value.on('edge:connected', ({ edge }) => {
    edge.attr({
      line: {
        strokeDasharray: '',
      },
    })
  })
  // 监听节点数据变化事件
  graph.value.on('node:change:data', ({ node }) => {
    const edges = graph.value!.getIncomingEdges(node) // 获取入边
    const { status } = node.getData() as { status: string } // 获取节点状态

    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', '') // 移除动画
      }
    })
  })

};

四、初始化数据

       初始化节点的连接关系、位置关系、id、形状,这个步骤结束后界面就会显示出节点信息,接下来就是模拟运行动效。

const init = (data: Cell.Metadata[]) => {
  const cells = data.map((item) => {
    if (item.shape === "dag-node") {
      return graph.value!.createNode(item);
    }
    return graph.value!.createEdge(item);
  });
  graph.value!.resetCells(cells);
};


// 初始化节点和边
const initData: Cell.Metadata[] = [
  {
    id: "1",
    shape: "dag-node",
    x: 300,
    y: 110,
    data: { label: "读数据", status: "success" },
    ports: [{ id: "1-1", group: "right" }],
  },
  {
    id: "2",
    shape: "dag-node",
    x: 600,
    y: 110,
    data: { label: "逻辑回归", status: "success" },
    ports: [
      { id: "2-1", group: "left" },
      { id: "2-2", group: "right" },
      { id: "2-3", group: "right" },
    ],
  },
  {
    id: "3",
    shape: "dag-node",
    x: 900,
    y: 0,
    data: { label: "模型预测", status: "success" },
    ports: [
      { id: "3-1", group: "left" },
      { id: "3-2", group: "right" },
    ],
  },
  {
    id: "4",
    shape: "dag-node",
    x: 900,
    y: 300,
    data: { label: "读取参数", status: "success" },
    ports: [
      { id: "4-1", group: "left" },
      { id: "4-2", group: "right" },
    ],
  },
  {
    id: "5",
    shape: "dag-edge",
    source: { cell: "1", port: "1-1" },
    target: { cell: "2", port: "2-1" },
    zIndex: 0,
  },
  {
    id: "6",
    shape: "dag-edge",
    source: { cell: "2", port: "2-2" },
    target: { cell: "3", port: "3-1" },
    zIndex: 0,
  },
  {
    id: "7",
    shape: "dag-edge",
    source: { cell: "2", port: "2-3" },
    target: { cell: "4", port: "4-1" },
    zIndex: 0,
  },
];

五、进行数据处理模拟

        通过回调showNodeStatus方法进行数据处理模拟,每3s进行一次遍历nodeStatusList参数,并通过shift()函数进行数据优化,也就是每次执行完都把已经执行完的数据给删除。

const showNodeStatus = async (statusList: NodeStatus[][]) => {
  const status = statusList.shift();
  if (status) {
    status.forEach(({ id, status }) => {
      const node = graph.value!.getCellById(id);
      const data = node.getData() as NodeStatus;
      node.setData({
        ...data,
        status,
      });
    });
    setTimeout(() => {
      showNodeStatus(statusList);
    }, 3000 );
  }
};




// 节点状态列表
const nodeStatusList: NodeStatus[][] = [
  [
    { id: "1", status: "running" },
    { id: "2", status: "default" },
    { id: "3", status: "default" },
    { id: "4", status: "default" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "running" },
    { id: "3", status: "default" },
    { id: "4", status: "default" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "success" },
    { id: "3", status: "running" },
    { id: "4", status: "running" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "success" },
    { id: "3", status: "success" },
    { id: "4", status: "failed" },
  ],
];

 线变化的在初始化画布时就已开启了数据监听,通过数据监听进行渲染。

  // 监听节点数据变化事件
  graph.value.on('node:change:data', ({ node }) => {
    const edges = graph.value!.getIncomingEdges(node) // 获取入边
    const { status } = node.getData() as { status: string } // 获取节点状态

    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', '') // 移除动画
      }
    })
  })

六、节点样式的动态变化 

初始想法:通过watch去监听props,后发现props并不为Vue的响应数据,也就是此数据并不是通过响应式数据传输的,所以用watch监听不到props参数,这也就导致节点的渲染一直停留在第一次的状态。(但其实props的值是在变化的)

// 监听 node 数据变化
watch(
    () => props,
    (newData, oldData) => {
      console.log("Data changed:", newData);
      updateNodeData();
    },
    { deep: true }
);

改进后的想法 :通过监听change:data进行节点动态渲染,通过监听chage:data可以发现其实数据是在动态变化的,然后就可以通过chage:data的监听实现节点的动态改变。

const props = defineProps<{
  node: {
    getData: () => { label: string; status: string };
    on: (event: string, callback: Function) => void;
    off: (event: string, callback: Function) => void;
  };
}>();

// 监听节点数据变化
onMounted(() => {
  updateNodeData(); // 初始化数据
  props.node.on('change:data', updateNodeData);
});

// 清理监听器
onBeforeUnmount(() => {
  props.node.off('change:data', updateNodeData);
});

记得要使用响应式数据哦~ 

<template>
  <div :class="['node', status]">
    <img :src="image.logo" alt="logo" />
    <span class="label">{{ label }}</span>
    <span class="status">
      <img v-if="status === 'success'" :src="image.success" alt="success" />
      <img v-if="status === 'failed'" :src="image.failed" alt="failed" />
      <img v-if="status === 'running'" :src="image.running" alt="running" />
    </span>
  </div>
</template>

// 定义响应式变量
const label = ref('');
const status = ref<'default' | 'success' | 'failed' | 'running'>('default');


const updateNodeData = () => {
  const data = props.node.getData();
  label.value = data.label;
  status.value = data.status as any;
};

成果的效果如下: 

 

 

完整代码 

一、画布代码如下:

<template>
  <div style="width: 100%; height: 100vh;display: flex;justify-content: center;align-items: center">
    <div id="antvcontainer"></div>
  </div>
  

</template>

<script lang="ts" setup>
import { onMounted, ref } from "vue";
import {Cell, Graph, Path} from "@antv/x6";
import AlgoNode from "@/views/AntVDag/node.vue";
import { register } from '@antv/x6-vue-shape'

// AlgoNode 组件

register({
  shape: 'dag-node', // 自定义节点的名称
  width: 180, // 节点的宽度
  height: 36, // 节点的高度
  component: AlgoNode, // 与此节点关联的组件(通常用于渲染节点的内容)
  ports: { // 定义节点的连接点(ports)
    groups: {
      left: { // 左侧连接点组
        position: 'left', // 连接点的位置
        attrs: {
          circle: { // 连接点的形状和样式
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
      right: { // 右侧连接点组
        position: 'right', // 连接点的位置
        attrs: {
          circle: { // 连接点的形状和样式
            r: 4,
            magnet: true,
            stroke: '#C2C8D5',
            strokeWidth: 1,
            fill: '#fff',
          },
        },
      },
    },
  }
});
// 注册自定义边样式
Graph.registerEdge(
    "dag-edge",
    {
      inherit: "edge",
      attrs: {
        line: {
          stroke: "#C2C8D5",
          strokeWidth: 1,
          targetMarker: null,
        },
      },
    },
    true
);
//注册连接器的样式
Graph.registerConnector(
    'algo-connector',
    (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,
)


// 定义 graph 的引用
const graph = ref<Graph | null>(null);
// 节点状态的类型定义
interface NodeStatus {
  id: string;
  status: "default" | "success" | "failed" | "running";
  label?: string;
}

import { Selection } from '@antv/x6-plugin-selection'

const buildAntvX6Container = () => {
  const container = document.getElementById("antvcontainer");
  if (!container) {
    console.error("AntV X6 容器未找到");
    return;
  }
  // 创建图形实例
  graph.value = new Graph({
    container: container,
    // autoResize: true,
    width: 2000,
    height: 2000,
    panning: {
      enabled: true,
      eventTypes: ["leftMouseDown", "mouseWheel"],
    },
    mousewheel: true,
    background: {
      color: "#d9e4f5",
    },
    grid: {
      visible: true,
      type: "doubleMesh",
      args: [
        {
          color: "#eee",
          thickness: 1,
        },
        {
          color: "#ddd",
          thickness: 1,
          factor: 4,
        },
      ],
    },
    connecting: {
      connector: 'algo-connector',
      snap: { radius: 50 },
      allowBlank: false,
      allowLoop: false,
      allowNode: false,
      allowEdge: false,
      allowMulti: true,
      highlight: true,
      validateMagnet({ magnet }) {
        return magnet.getAttribute("port-group") !== "left";
      },
      createEdge() {
        return graph.value!.createEdge({
          shape: "dag-edge",
          attrs: {
            line: {
              strokeDasharray: "5 5",
            },
          },
          zIndex: -1,
        });
      },
    },
    highlighting: {
      magnetAvailable: {
        name: "stroke",
        args: {
          attrs: {
            fill: "#fff",
            stroke: "#31d0c6",
            strokeWidth: 4,
          },
        },
      },
    },
  });


  // 使用选择插件
  graph.value.use(
      new Selection ({
        multiple: true,
        rubberEdge: true,
        rubberNode: true,
        modifiers: 'shift',
        rubberband: true,
      })
  )
  // 监听边连接事件
  graph.value.on('edge:connected', ({ edge }) => {
    edge.attr({
      line: {
        strokeDasharray: '',
      },
    })
  })
  // 监听节点数据变化事件
  graph.value.on('node:change:data', ({ node }) => {
    const edges = graph.value!.getIncomingEdges(node) // 获取入边
    const { status } = node.getData() as { status: string } // 获取节点状态

    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', '') // 移除动画
      }
    })
  })

};

// 节点状态列表
const nodeStatusList: NodeStatus[][] = [
  [
    { id: "1", status: "running" },
    { id: "2", status: "default" },
    { id: "3", status: "default" },
    { id: "4", status: "default" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "running" },
    { id: "3", status: "default" },
    { id: "4", status: "default" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "success" },
    { id: "3", status: "running" },
    { id: "4", status: "running" },
  ],
  [
    { id: "1", status: "success" },
    { id: "2", status: "success" },
    { id: "3", status: "success" },
    { id: "4", status: "failed" },
  ],
];

const showNodeStatus = async (statusList: NodeStatus[][]) => {
  const status = statusList.shift();
  if (status) {
    status.forEach(({ id, status }) => {
      const node = graph.value!.getCellById(id);
      const data = node.getData() as NodeStatus;
      node.setData({
        ...data,
        status,
      });
    });
    setTimeout(() => {
      showNodeStatus(statusList);
    }, 3000 );
  }
};

// 初始化节点和边
const initData: Cell.Metadata[] = [
  {
    id: "1",
    shape: "dag-node",
    x: 300,
    y: 110,
    data: { label: "读数据", status: "success" },
    ports: [{ id: "1-1", group: "right" }],
  },
  {
    id: "2",
    shape: "dag-node",
    x: 600,
    y: 110,
    data: { label: "逻辑回归", status: "success" },
    ports: [
      { id: "2-1", group: "left" },
      { id: "2-2", group: "right" },
      { id: "2-3", group: "right" },
    ],
  },
  {
    id: "3",
    shape: "dag-node",
    x: 900,
    y: 0,
    data: { label: "模型预测", status: "success" },
    ports: [
      { id: "3-1", group: "left" },
      { id: "3-2", group: "right" },
    ],
  },
  {
    id: "4",
    shape: "dag-node",
    x: 900,
    y: 300,
    data: { label: "读取参数", status: "success" },
    ports: [
      { id: "4-1", group: "left" },
      { id: "4-2", group: "right" },
    ],
  },
  {
    id: "5",
    shape: "dag-edge",
    source: { cell: "1", port: "1-1" },
    target: { cell: "2", port: "2-1" },
    zIndex: 0,
  },
  {
    id: "6",
    shape: "dag-edge",
    source: { cell: "2", port: "2-2" },
    target: { cell: "3", port: "3-1" },
    zIndex: 0,
  },
  {
    id: "7",
    shape: "dag-edge",
    source: { cell: "2", port: "2-3" },
    target: { cell: "4", port: "4-1" },
    zIndex: 0,
  },
];

const init = (data: Cell.Metadata[]) => {
  const cells = data.map((item) => {
    if (item.shape === "dag-node") {
      return graph.value!.createNode(item);
    }
    return graph.value!.createEdge(item);
  });
  graph.value!.resetCells(cells);
};

// 挂载后初始化
onMounted(() => {
  buildAntvX6Container();
  init(initData);
  showNodeStatus(nodeStatusList);
  // 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
  graph.value!.zoomToFit({ maxScale: 2 })
});
</script>

<style scoped>

</style>

二、节点组件代码如下: 

<template>
  <div :class="['node', status]">
    <img :src="image.logo" alt="logo" />
    <span class="label">{{ label }}</span>
    <span class="status">
      <img v-if="status === 'success'" :src="image.success" alt="success" />
      <img v-if="status === 'failed'" :src="image.failed" alt="failed" />
      <img v-if="status === 'running'" :src="image.running" alt="running" />
    </span>
  </div>
</template>

<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch} from 'vue';

// 定义响应式变量
const label = ref('');
const status = ref<'default' | 'success' | 'failed' | 'running'>('default');

// 图片路径
const image = {
  logo: 'logo.png',
  success: 'success.png',
  failed: 'failed.png',
  running: 'running.png',
};

const props = defineProps<{
  node: {
    getData: () => { label: string; status: string };
    on: (event: string, callback: Function) => void;
    off: (event: string, callback: Function) => void;
  };
}>();

// 数据更新处理函数
const updateNodeData = () => {
  const data = props.node.getData();
  label.value = data.label;
  status.value = data.status as any;
};



// 监听节点数据变化
onMounted(() => {
  updateNodeData(); // 初始化数据
  props.node.on('change:data', updateNodeData);
});

// 清理监听器
onBeforeUnmount(() => {
  props.node.off('change:data', updateNodeData);
});

</script>

<style >
.node {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
  background-color: #fff;
  border: 1px solid #c2c8d5;
  border-left: 4px solid #5F95FF;
  border-radius: 4px;
  box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
}
.node img {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  margin-left: 8px;
}
.node .label {
  display: inline-block;
  flex-shrink: 0;
  width: 104px;
  margin-left: 8px;
  color: #666;
  font-size: 12px;
}
.node .status {
  flex-shrink: 0;
}
.node.success {
  border-left: 4px solid #52c41a;
}
.node.failed {
  border-left: 4px solid #ff4d4f;
}
.node.running .status img {
  animation: spin 1s linear infinite;
}
.x6-node-selected .node {
  border-color: #1890ff;
  border-radius: 2px;
  box-shadow: 0 0 0 4px #d4e8fe;
}
.x6-node-selected .node.success {
  border-color: #52c41a;
  border-radius: 2px;
  box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
  border-color: #ff4d4f;
  border-radius: 2px;
  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>


原文地址:https://blog.csdn.net/qq_58055766/article/details/145140207

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!