vue3 + antv/x6实现流程图的全过程

 更新时间:2024年06月20日 11:47:44   作者:FenceRain  
随着互联网的发展,越来越多的应用需要实现流程图的制作,如工作流程图、电路图等,文中通过代码以及图文将实现的过程介绍的非常详细,对大家学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下

新建流程图

// AddDag.vue
<template>
  <div class="content-main">
    <div class="tool-container">
      <div @click="undo" class="command" title="后退">
        <Icon icon="ant-design:undo-outlined" />
      </div>
      <div @click="redo" class="command" title="前进">
        <Icon icon="ant-design:redo-outlined" />
      </div>
      <el-divider direction="vertical" />
      <div @click="copy" class="command" title="复制">
        <Icon icon="ant-design:copy-filled" />
      </div>
      <div @click="paste" class="command" title="粘贴">
        <Icon icon="fa-solid:paste" />
      </div>
      <div @click="del" class="command" title="删除">
        <Icon icon="ant-design:delete-filled" />
      </div>
      <el-divider direction="vertical" />
      <div @click="save" class="command" title="保存">
        <Icon icon="ant-design:save-filled" />
      </div>
      <el-divider direction="vertical" />
      <div @click="exportPng" class="command" title="导出PNG">
        <Icon icon="ant-design:file-image-filled" />
      </div>
    </div>
    <div class="content-container" id="">
      <div class="content">
        <div class="stencil" ref="stencilContainer"></div>
        <div class="graph-content" id="graphContainer" ref="graphContainer"> </div>

        <div class="editor-sidebar">
          <div class="edit-panel">
            <el-card shadow="never">
              <template #header>
                <div class="card-header">
                  <span>{{ cellFrom.title }}</span>
                </div>
              </template>
              <el-form :model="nodeFrom" label-width="50px" v-if="nodeFrom.show">
                <el-form-item label="label">
                  <el-input v-model="nodeFrom.label" @blur="changeLabel" />
                </el-form-item>
                <el-form-item label="desc">
                  <el-input type="textarea" v-model="nodeFrom.desc" @blur="changeDesc" />
                </el-form-item>
              </el-form>
              <el-form :model="cellFrom" label-width="50px" v-if="cellFrom.show">
                <el-form-item label="label">
                  <el-input v-model="cellFrom.label" @blur="changeEdgeLabel" />
                </el-form-item>
                <!-- <el-form-item label="连线方式">
                    <el-select v-model="cellFrom.edgeType" class="m-2" placeholder="Select"  @change="changeEdgeType">
                      <el-option
                        v-for="item in EDGE_TYPE_LIST"
                        :key="item.type"
                        :label="item.name"
                        :value="item.type"
                      />
                    </el-select>
                  </el-form-item> -->
              </el-form>
            </el-card>
          </div>
          <div>
            <el-card shadow="never">
              <template #header>
                <div class="card-header">
                  <span>Minimap</span>
                </div>
              </template>
              <div class="minimap" ref="miniMapContainer"></div>
            </el-card>
          </div>
        </div>
      </div>
    </div>
    <div v-if="showMenu" class="node-menu" ref="nodeMenu">
      <div
        class="menu-item"
        v-for="(item, index) in PROCESSING_TYPE_LIST"
        :key="index"
        @click="addNodeTool(item)"
      >
        <el-image :src="item.image" style="width: 16px; height: 16px" fit="fill" />
        <span>{{ item.name }}</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Graph, Path, Edge, StringExt, Node, Cell, Model, DataUri } from '@antv/x6'
import { Transform } from '@antv/x6-plugin-transform'
import { Selection } from '@antv/x6-plugin-selection'
import { Snapline } from '@antv/x6-plugin-snapline'
import { Keyboard } from '@antv/x6-plugin-keyboard'
import { Clipboard } from '@antv/x6-plugin-clipboard'
import { History } from '@antv/x6-plugin-history'
import { MiniMap } from '@antv/x6-plugin-minimap'
//import { Scroller } from '@antv/x6-plugin-scroller'
import { Stencil } from '@antv/x6-plugin-stencil'
import { Export } from '@antv/x6-plugin-export'
import { ref, onMounted, reactive, toRefs, nextTick, onUnmounted } from 'vue'
import '@/styles/animation.less'
import { ElMessage, ElCard, ElForm, ElFormItem, ElInput, ElImage, ElDivider } from 'element-plus'

const stencilContainer = ref()
const graphContainer = ref()
const miniMapContainer = ref()

let graph: any = null

const state = reactive({
  cellFrom: {
    title: 'Canvas',
    label: '',
    desc: '',
    show: false,
    id: '',
    edgeType: 'topBottom'
  },
  nodeFrom: {
    title: 'Canvas',
    label: '',
    desc: '',
    show: false,
    id: ''
  },
  showMenu: false,
  data: {
    nodes: [
      {
        id: 'ac51fb2f-2753-4852-8239-53672a29bb14',
        position: {
          x: -340,
          y: -160
        },
        data: {
          name: '诗名',
          type: 'OUTPUT',
          desc: '春望'
        }
      },
      {
        id: '81004c2f-0413-4cc6-8622-127004b3befa',
        position: {
          x: -340,
          y: -10
        },
        data: {
          name: '第一句',
          type: 'SYNC',
          desc: '国破山河在'
        }
      },
      {
        id: '7505da25-1308-4d7a-98fd-e6d5c917d35d',
        position: {
          x: -140,
          y: 180
        },
        data: {
          name: '结束',
          type: 'INPUT',
          desc: '城春草木胜'
        }
      }
    ],
    edges: [
      {
        id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c',
        shape: 'processing-curve',
        source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14', port: '-out' },
        target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '-in' },
        zIndex: -1,
        data: {
          source: 'ac51fb2f-2753-4852-8239-53672a29bb14',
          target: '81004c2f-0413-4cc6-8622-127004b3befa'
        }
      },
      {
        id: '8cbce713-54be-4c07-8efa-59c505f74ad7',
        labels: ['下半句'],
        shape: 'processing-curve',
        source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '-out' },
        target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d', port: '-in' },
        data: {
          source: '81004c2f-0413-4cc6-8622-127004b3befa',
          target: '7505da25-1308-4d7a-98fd-e6d5c917d35d'
        }
      }
    ]
  },
  // 节点状态列表
  nodeStatusList: [
    {
      id: 'ac51fb2f-2753-4852-8239-53672a29bb14',
      status: 'success'
    },
    {
      id: '81004c2f-0413-4cc6-8622-127004b3befa',
      status: 'success'
    }
  ],

  // 边状态列表
  edgeStatusList: [
    {
      id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c',
      status: 'success'
    },
    {
      id: '8cbce713-54be-4c07-8efa-59c505f74ad7',
      status: 'executing'
    }
  ],
  // 加工类型列表
  PROCESSING_TYPE_LIST: [
    {
      type: 'SYNC',
      name: '数据同步',
      image: new URL('@/assets/imgs/persimmon.png', import.meta.url).href
    },
    {
      type: 'INPUT',
      name: '结束',
      image: new URL('@/assets/imgs/lime.png', import.meta.url).href
    }
  ],
  //边类型
  EDGE_TYPE_LIST: [
    {
      type: 'topBottom',
      name: '上下'
    },
    {
      type: 'leftRight',
      name: '左右'
    }
  ]
})

const { cellFrom, nodeFrom, showMenu, PROCESSING_TYPE_LIST } = toRefs(state)

let nodeMenu = ref()

// 节点类型
enum NodeType {
  INPUT = 'INPUT', // 数据输入
  FILTER = 'FILTER', // 数据过滤
  JOIN = 'JOIN', // 数据连接
  UNION = 'UNION', // 数据合并
  AGG = 'AGG', // 数据聚合
  OUTPUT = 'OUTPUT', // 数据输出
  SYNC = 'SYNC' //数据同步
}

// 元素校验状态
// enum CellStatus {
//   DEFAULT = 'default',
//   SUCCESS = 'success',
//   ERROR = 'error'
// }

// 节点位置信息
interface Position {
  x: number
  y: number
}

function init() {
  graph = new Graph({
    container: graphContainer.value,
    grid: true,
    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: 'bottom',
      //   args: {
      //     dx: 0,
      //   },
      // },
      // targetAnchor: {
      //   name: 'top',
      //   args: {
      //     dx: 0,
      //   },
      // },
      createEdge() {
        return graph.createEdge({
          shape: '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') === 'out') {
          return false
        }
        return true
      }
    }
  })
  graph.centerContent()

  // #region 使用插件
  graph
    .use(
      new Transform({
        resizing: true,
        rotating: true
      })
    )
    .use(
      new Selection({
        rubberband: true,
        showNodeSelectionBox: true
      })
    )
    .use(
      new MiniMap({
        container: miniMapContainer.value,
        width: 200,
        height: 260,
        padding: 10
      })
    )
    .use(new Snapline())
    .use(new Keyboard())
    .use(new Clipboard())
    .use(new History())
    .use(new Export())
  //.use(new Scroller({
  //  enabled: true,
  //  pageVisible: true,
  //  pageBreak: false,
  //  pannable: true,

  // }))
  // #endregion

  // #region 初始化图形
  const ports = {
    groups: {
      in: {
        position: 'top',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      out: {
        position: 'bottom',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      left: {
        position: 'left',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      right: {
        position: 'right',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      }
    }
    // items: [
    //   {
    //     id: state.currentCode + '-in',
    //     group: 'top',
    //   },
    //   {
    //     id: state.currentCode + '-out',
    //     group: 'out',
    //   }
    // ],
  }

  Graph.registerNode(
    'custom-node',
    {
      inherit: 'rect',
      width: 140,
      height: 76,
      attrs: {
        body: {
          strokeWidth: 1
        },
        image: {
          width: 16,
          height: 16,
          x: 12,
          y: 6
        },
        text: {
          refX: 40,
          refY: 15,
          fontSize: 15,
          'text-anchor': 'start'
        },
        label: {
          text: 'Please nominate this node',
          refX: 10,
          refY: 30,
          fontSize: 12,
          fill: 'rgba(0,0,0,0.6)',
          'text-anchor': 'start',
          textWrap: {
            width: -10, // 宽度减少 10px
            height: '70%', // 高度为参照元素高度的一半
            ellipsis: true, // 文本超出显示范围时,自动添加省略号
            breakWord: true // 是否截断单词
          }
        }
      },
      markup: [
        {
          tagName: 'rect',
          selector: 'body'
        },
        {
          tagName: 'image',
          selector: 'image'
        },
        {
          tagName: 'text',
          selector: 'text'
        },
        {
          tagName: 'text',
          selector: 'label'
        }
      ],
      data: {},
      relation: {},
      ports: { ...ports }
    },
    true
  )

  const stencil = new Stencil({
    //新建节点库
    title: '数据集成',
    target: graph,
    search: false, // 搜索
    collapsable: true,
    stencilGraphWidth: 300, //容器宽度
    stencilGraphHeight: 600, //容器长度
    groups: [
      //分组
      {
        name: 'processLibrary',
        title: 'dataSource'
      }
    ],
    layoutOptions: {
      dx: 30,
      dy: 20,
      columns: 1, //列数(行内节点数)
      columnWidth: 130, //列宽
      rowHeight: 100 //行高
    }
  })
  stencilContainer.value.appendChild(stencil.container)

  // 控制连接桩显示/隐藏
  // eslint-disable-next-line no-undef
  const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => {
    for (let i = 0, len = ports.length; i < len; i += 1) {
      ports[i].style.visibility = show ? 'visible' : 'hidden'
    }
  }
  graph.on('node:mouseenter', () => {
    const container = graphContainer.value
    const ports = container.querySelectorAll('.x6-port-body')
    showPorts(ports, true)
  })
  graph.on('node:mouseleave', () => {
    const container = graphContainer.value
    const ports = container.querySelectorAll(
      '.x6-port-body'
      // eslint-disable-next-line no-undef
    ) as NodeListOf<SVGElement>
    showPorts(ports, false)
  })

  // #region 快捷键与事件
  graph.bindKey(['meta+c', 'ctrl+c'], () => {
    // const cells = graph.getSelectedCells()
    // if (cells.length) {
    //   graph.copy(cells)
    // }
    // return false
    copy()
  })
  graph.bindKey(['meta+x', 'ctrl+x'], () => {
    const cells = graph.getSelectedCells()
    if (cells.length) {
      graph.cut(cells)
    }
    return false
  })
  graph.bindKey(['meta+v', 'ctrl+v'], () => {
    // if (!graph.isClipboardEmpty()) {
    //   const cells = graph.paste({ offset: 32 })
    //   graph.cleanSelection()
    //   graph.select(cells)
    // }
    // return false
    paste()
  })

  // undo redo
  graph.bindKey(['meta+z', 'ctrl+z'], () => {
    // if (graph.canUndo()) {
    //   graph.undo()
    // }
    // return false
    undo()
  })
  graph.bindKey(['meta+y', 'ctrl+y'], () => {
    // if (graph.canRedo()) {
    //   graph.redo()
    // }
    // return false
    redo()
  })
  // select all
  graph.bindKey(['meta+a', 'ctrl+a'], () => {
    const nodes = graph.getNodes()
    if (nodes) {
      graph.select(nodes)
    }
  })

  // delete
  graph.bindKey('backspace', () => {
    // const cells = graph.getSelectedCells()
    // if (cells.length) {
    //   graph.removeCells(cells)
    // }
    del()
  })

  // zoom
  graph.bindKey(['ctrl+1', 'meta+1'], () => {
    const zoom = graph.zoom()
    if (zoom < 1.5) {
      graph.zoom(0.1)
    }
  })
  graph.bindKey(['ctrl+2', 'meta+2'], () => {
    const zoom = graph.zoom()
    if (zoom > 0.5) {
      graph.zoom(-0.1)
    }
  })
  // 节点移入画布事件
  graph.on('node:added', ({ node }: any) => {
    // console.log(node,cell);
    addNodeInfo(node)
  })
  //  节点单击事件
  graph.on('node:click', ({ node }: any) => {
    //  console.log(node,cell)
    addNodeInfo(node)
  })

  //节点被选中时显示添加节点按钮
  graph.on('node:selected', (args: { cell: Cell; node: Node; options: Model.SetOptions }) => {
    if (NodeType.INPUT != args.node.data.type) {
      args.node.removeTools()
      args.node.addTools({
        name: 'button',
        args: {
          x: 0,
          y: 0,
          offset: { x: 160, y: 40 },
          markup: [
            //自定义的删除按钮样式
            {
              tagName: 'circle',
              selector: 'button',
              attrs: {
                r: 8,
                stroke: 'rgba(0,0,0,.25)',
                strokeWidth: 1,
                fill: 'rgba(255, 255, 255, 1)',
                cursor: 'pointer'
              }
            },
            {
              tagName: 'text',
              textContent: '+',
              selector: 'icon',
              attrs: {
                fill: 'rgba(0,0,0,.25)',
                fontSize: 15,
                textAnchor: 'middle',
                pointerEvents: 'none',
                y: '0.3em',
                stroke: 'rgba(0,0,0,.25)'
              }
            }
          ],
          onClick({ e, view }: any) {
            //      console.log(e,cell);
            showNodeTool(e, view)
          }
        }
      })
    }
    // code here
  })

  //节点被取消选中时触发。
  graph.on('node:unselected', (args: { cell: Cell; node: Node; options: Model.SetOptions }) => {
    args.node.removeTools()
  })

  // 添加边事件
  graph.on('edge:added', ({ edge }: any) => {
    // console.log(edge);
    addEdgeInfo(edge)
    edge.data = {
      source: edge.source.cell,
      target: edge.target.cell
    }
  })
  //  线单击事件
  graph.on('edge:click', ({ edge }: any) => {
    //  console.log(node,cell)
    addEdgeInfo(edge)
  })

  //边选中事件
  graph.on('edge:selected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => {
    args.edge.attr('line/strokeWidth', 3)
  })

  //边被取消选中时触发。
  graph.on('edge:unselected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => {
    args.edge.attr('line/strokeWidth', 1)
  })

  const nodeShapes = [
    {
      label: '开始',
      nodeType: 'OUTPUT' as NodeType
    },
    {
      label: '数据同步',
      nodeType: 'SYNC' as NodeType
    },
    {
      label: '结束',
      nodeType: 'INPUT' as NodeType
    }
  ]

  const nodes = nodeShapes.map((item) => {
    const id = StringExt.uuid()
    const node = {
      id: id,
      shape: 'custom-node',
      // label: item.label,
      ports: getPortsByType(item.nodeType, id),
      data: {
        name: `${item.label}`,
        type: item.nodeType
      },
      attrs: getNodeAttrs(item.nodeType)
    }
    const newNode = graph.addNode(node)
    return newNode
  })

  //#endregion
  stencil.load(nodes, 'processLibrary')
}

// 根据节点的类型获取ports
const getPortsByType = (type: NodeType, nodeId: string) => {
  let ports = [] as any
  switch (type) {
    case NodeType.INPUT:
      ports = [
        {
          id: `${nodeId}-in`,
          group: 'in'
        },
        {
          id: `${nodeId}-left`,
          group: 'left'
        },
        {
          id: `${nodeId}-right`,
          group: 'right'
        }
      ]
      break
    case NodeType.OUTPUT:
      ports = [
        {
          id: `${nodeId}-out`,
          group: 'out'
        },
        {
          id: `${nodeId}-left`,
          group: 'left'
        },
        {
          id: `${nodeId}-right`,
          group: 'right'
        }
      ]
      break
    default:
      ports = [
        {
          id: `${nodeId}-in`,
          group: 'in'
        },
        {
          id: `${nodeId}-out`,
          group: 'out'
        },
        {
          id: `${nodeId}-left`,
          group: 'left'
        },
        {
          id: `${nodeId}-right`,
          group: 'right'
        }
      ]
      break
  }
  return ports
}

// 注册连线 --上下
Graph.registerConnector(
  'curveConnectorTB',
  (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
)

// 注册连线--左右
Graph.registerConnector(
  'curveConnectorLR',
  (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.registerEdge(
  'processing-curve',
  {
    inherit: 'edge',
    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: 'smooth' }, //curveConnectorTB
    attrs: {
      wrap: {
        connection: true,
        strokeWidth: 10,
        strokeLinejoin: 'round'
      },
      line: {
        connection: true,
        stroke: '#A2B1C3',
        strokeWidth: 1,
        targetMarker: {
          name: 'classic',
          size: 6
        }
      }
    }
  },
  true
)

// Graph.registerEdge(
//   'processing-curve-lr',
//   {
//   inherit: 'edge',
//   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: 'curveConnectorLR' },
//     attrs: {
//       wrap: {
//         connection: true,
//         strokeWidth: 10,
//         strokeLinejoin: 'round',
//       },
//       line: {
//         connection: true,
//         stroke: '#A2B1C3',
//         strokeWidth: 1,
//         targetMarker: {
//           name: 'classic',
//           size: 6,
//         },
//       },
//     },
// },
//   true,
// )

//保存
function save() {
  console.log('save')
  const graphData = graph.toJSON()
  console.log(graphData)
}

//撤销
function undo() {
  if (graph.canUndo()) {
    graph.undo()
  }
  return false
}
//取消撤销
function redo() {
  if (graph.canRedo()) {
    graph.redo()
  }
  return false
}
//复制
function copy() {
  const cells = graph.getSelectedCells()
  if (cells.length) {
    graph.copy(cells)
  }
  return false
}
//粘贴
function paste() {
  if (!graph.isClipboardEmpty()) {
    const cells = graph.paste({ offset: 32 })
    graph.cleanSelection()
    graph.select(cells)
  }
  return false
}
//删除
function del() {
  const cells = graph.getSelectedCells()
  if (cells.length) {
    graph.removeCells(cells)
  }
}

//导出PNG
function exportPng() {
  graph.toPNG(
    (dataUri: string) => {
      // 下载
      DataUri.downloadDataUri(dataUri, 'chart.png')
    },
    {
      padding: {
        top: 20,
        right: 20,
        bottom: 20,
        left: 20
      }
    }
  )
  //graph.exportPNG('a.png',{padding:'20px'});
}

function addNodeInfo(node: any) {
  state.nodeFrom.title = 'Node'
  state.nodeFrom.label = node.label
  state.nodeFrom.desc = node.attrs.label.text
  state.nodeFrom.show = true
  state.nodeFrom.id = node.id
  state.cellFrom.show = false
}

function addEdgeInfo(edge: any) {
  state.nodeFrom.show = false
  state.cellFrom.title = 'Edge'
  if (edge.labels[0]) {
    state.cellFrom.label = edge.labels[0].attrs.label.text
  } else {
    state.cellFrom.label = ''
  }
  state.cellFrom.edgeType = edge.data ? edge.data.edgeType : ''
  state.cellFrom.show = true
  state.cellFrom.id = edge.id
}
//修改文本
function changeLabel() {
  const nodes = graph.getNodes()
  nodes.forEach((node: any) => {
    if (state.nodeFrom.id == node.id) {
      node.label = state.nodeFrom.label
    }
  })
}

//修改描述
function changeDesc() {
  const nodes = graph.getNodes()
  nodes.forEach((node: any) => {
    if (state.nodeFrom.id == node.id) {
      node.attr('label/text', state.nodeFrom.desc)
    }
  })
}

//修改边文本
function changeEdgeLabel() {
  const edges = graph.getEdges()
  edges.forEach((edge: any) => {
    if (state.cellFrom.id == edge.id) {
      edge.setLabels(state.cellFrom.label)
      console.log(edge)
    }
  })
}

//修改边的类型
// function changeEdgeType() {
//   const edges = graph.getEdges()
//   edges.forEach((edge: any) => {
//     if (state.cellFrom.id == edge.id) {
//       //    console.log(state.cellFrom.edgeType);
//       if (state.cellFrom.edgeType == 'topBottom') {
//         edge.setConnector('curveConnectorTB')
//       } else {
//         edge.setConnector('curveConnectorLR')
//         //      console.log(edge);
//       }
//       edge.data.edgeType = state.cellFrom.edgeType
//     }
//   })
// }

const getNodeAttrs = (nodeType: string) => {
  let attr = {} as any
  switch (nodeType) {
    case NodeType.INPUT:
      attr = {
        image: {
          'xlink:href': new URL('@/assets/imgs/lime.png', import.meta.url).href
        },
        //左侧拖拽样式
        body: {
          fill: '#b9dec9',
          stroke: '#229453'
        },
        text: {
          text: '结束',
          fill: '#229453'
        }
      }
      break
    case NodeType.SYNC:
      attr = {
        image: {
          'xlink:href': new URL('@/assets/imgs/persimmon.png', import.meta.url).href
        },
        //左侧拖拽样式
        body: {
          fill: '#edc3ae',
          stroke: '#f9723d'
        },
        text: {
          text: '数据同步',
          fill: '#f9723d'
        }
      }
      break
    case NodeType.OUTPUT:
      attr = {
        image: {
          'xlink:href': new URL('@/assets/imgs/rice.png', import.meta.url).href
        },
        //左侧拖拽样式
        body: {
          fill: '#EFF4FF',
          stroke: '#5F95FF'
        },
        text: {
          text: '开始',
          fill: '#5F95FF'
        }
      }
      break
  }
  return attr
}

//加载初始节点
function getData() {
  let cells = [] as any
  const location = state.data
  location.nodes.map((node) => {
    let attr = getNodeAttrs(node.data.type)
    if (node.data.desc) {
      attr.label = { text: node.data.desc }
    }
    if (node.data.name) {
      let temp = attr.text
      if (temp) {
        temp.text = node.data.name
      }
    }
    cells.push(
      graph.addNode({
        id: node.id,
        x: node.position.x,
        y: node.position.y,
        shape: 'custom-node',
        attrs: attr,
        ports: getPortsByType(node.data.type as NodeType, node.id),
        data: node.data
      })
    )
  })
  location.edges.map((edge) => {
    cells.push(
      graph.addEdge({
        id: edge.id,
        source: edge.source,
        target: edge.target,
        zIndex: edge.zIndex,
        shape: 'processing-curve',
        //  connector: { name: 'curveConnector' },
        labels: edge.labels,
        attrs: { line: { strokeDasharray: '5 5' } },
        data: edge.data
      })
    )
  })
  graph.resetCells(cells)
}

// 开启边的运行动画
const excuteAnimate = (edge: any) => {
  edge.attr({
    line: {
      stroke: '#3471F9'
    }
  })
  edge.attr('line/strokeDasharray', 5)
  edge.attr('line/style/animation', 'running-line 30s infinite linear')
}

// 显示边状态
const showEdgeStatus = () => {
  state.edgeStatusList.forEach((item) => {
    const edge = graph.getCellById(item.id)
    if (item.status == 'success') {
      edge.attr('line/strokeDasharray', 0)
      edge.attr('line/stroke', '#52c41a')
    } else if ('error' == item.status) {
      edge.attr('line/stroke', '#ff4d4f')
    } else if ('executing' == item.status) {
      excuteAnimate(edge)
    }
  })
}

// 显示添加按钮菜单
function showNodeTool(e: any, _view: any) {
  //  console.log(view);
  state.showMenu = true
  nextTick(() => {
    nodeMenu.value.style.top = e.offsetY + 60 + 'px'
    nodeMenu.value.style.left = e.offsetX + 210 + 'px'
  })
}

// 点击添加节点按钮
function addNodeTool(item: any) {
  //  console.log(item);
  createDownstream(item.type)
  state.showMenu = false
}

/**
 * 根据起点初始下游节点的位置信息
 * @param node 起始节点
 * @param graph
 * @returns
 */
const getDownstreamNodePosition = (node: Node, graph: Graph, dx = 250, dy = 100) => {
  // 找出画布中以该起始节点为起点的相关边的终点id集合
  const downstreamNodeIdList: string[] = []
  graph.getEdges().forEach((edge) => {
    const originEdge = edge.toJSON()?.data
    console.log(node)
    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
  }
}

// 创建下游的节点和边
const createDownstream = (type: NodeType) => {
  //  console.log(graph.getSelectedCells());
  const cells = graph.getSelectedCells()
  if (cells.length == 1) {
    const node = cells[0]
    //console.log(node,"node");
    if (graph) {
      // 获取下游节点的初始位置信息
      const position = getDownstreamNodePosition(node, graph)
      // 创建下游节点
      const newNode = createNode(type, graph, position)
      const source = node.id
      const target = newNode.id
      // 创建该节点出发到下游节点的边
      createEdge(source, target, graph)
    }
  } else {
    ElMessage({
      message: '请选择一个节点',
      type: 'warning'
    })
  }
}

const createNode = (type: NodeType, graph: Graph, position?: Position): Node => {
  let newNode = {} as Node
  const typeName = state.PROCESSING_TYPE_LIST?.find((item) => item.type === type)?.name
  const id = StringExt.uuid()
  const node = {
    id,
    shape: 'custom-node',
    x: position?.x,
    y: position?.y,
    ports: getPortsByType(type, id),
    data: {
      name: `${typeName}`,
      type
    },
    attrs: getNodeAttrs(type)
  }
  newNode = graph.addNode(node)
  return newNode
}

const createEdge = (source: string, target: string, graph: Graph) => {
  const edge = {
    id: StringExt.uuid(),
    shape: 'processing-curve',
    source: {
      cell: source
      // port: `${source}-out`,
    },
    target: {
      cell: target
      //  port: `${target}-in`,
    },
    zIndex: -1,
    data: {
      source,
      target
    },
    attrs: { line: { strokeDasharray: '5 5' } }
  }
  // console.log(edge);
  if (graph) {
    graph.addEdge(edge)
  }
}

onMounted(() => {
  init()
  // graph.fromJSON(state.data);
  getData()
  showEdgeStatus()
})

onUnmounted(() => {
  graph.dispose()
})
</script>

<style lang="less" scoped>
.content-main {
  display: flex;
  width: 100%;
  flex-direction: column;
  height: calc(100vh - 85px - 40px);
  background-color: #ffffff;
  position: relative;

  .tool-container {
    padding: 8px;
    display: flex;
    align-items: center;
    color: rgba(0, 0, 0, 0.45);

    .command {
      display: inline-block;
      width: 27px;
      height: 27px;
      margin: 0 6px;
      padding-top: 6px;
      text-align: center;
      cursor: pointer;
    }
  }
}
.content-container {
  position: relative;
  width: 100%;
  height: 100%;
  .content {
    width: 100%;
    height: 100%;
    position: relative;

    min-width: 400px;
    min-height: 600px;
    display: flex;
    border: 1px solid #dfe3e8;
    flex-direction: row;
    //   flex-wrap: wrap;
    flex: 1 1;

    .stencil {
      width: 250px;
      height: 100%;
      border-right: 1px solid #dfe3e8;
      position: relative;

      :deep(.x6-widget-stencil) {
        background-color: #fff;
      }
      :deep(.x6-widget-stencil-title) {
        background-color: #fff;
      }
      :deep(.x6-widget-stencil-group-title) {
        background-color: #fff !important;
      }
    }
    .graph-content {
      width: calc(100% - 180px);
      height: 100%;
    }

    .editor-sidebar {
      display: flex;
      flex-direction: column;
      border-left: 1px solid #e6f7ff;
      background: #fafafa;
      z-index: 9;

      .el-card {
        border: none;
      }
      .edit-panel {
        flex: 1 1;
        background-color: #fff;
      }

      :deep(.x6-widget-minimap-viewport) {
        border: 1px solid #8f8f8f;
      }

      :deep(.x6-widget-minimap-viewport-zoom) {
        border: 1px solid #8f8f8f;
      }
    }
  }
}

:deep(.x6-widget-transform) {
  margin: -1px 0 0 -1px;
  padding: 0px;
  border: 1px solid #239edd;
}
:deep(.x6-widget-transform > div) {
  border: 1px solid #239edd;
}
:deep(.x6-widget-transform > div:hover) {
  background-color: #3dafe4;
}
:deep(.x6-widget-transform-active-handle) {
  background-color: #3dafe4;
}
:deep(.x6-widget-transform-resize) {
  border-radius: 0;
}
:deep(.x6-widget-selection-inner) {
  border: 1px solid #239edd;
}
:deep(.x6-widget-selection-box) {
  opacity: 0;
}

.topic-image {
  visibility: hidden;
  cursor: pointer;
}
.x6-node:hover .topic-image {
  visibility: visible;
}
.x6-node-selected rect {
  stroke-width: 2px;
}
.node-menu {
  position: absolute;
  box-shadow: var(--el-box-shadow-light);
  background: var(--el-bg-color-overlay);
  border: 1px solid var(--el-border-color-light);
  padding: 5px 0px;

  .menu-item {
    display: flex;
    align-items: center;
    white-space: nowrap;
    list-style: none;
    line-height: 22px;
    padding: 5px 16px;
    margin: 0;
    font-size: var(--el-font-size-base);
    color: var(--el-text-color-regular);
    cursor: pointer;
    outline: none;
    box-sizing: border-box;
  }

  .menu-item .el-image {
    margin-right: 5px;
  }

  .menu-item:hover {
    background-color: var(--el-color-primary-light-9);
    color: var(--el-color-primary);
  }
}
</style>


显示流程图

<template>
  <div class="content-main">
    <div class="content-container" id="">
      <div class="content">
        <div class="graph-content" id="graphContainer" ref="graphContainer"></div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Graph, Path, Edge } from '@antv/x6'
import { ref, onMounted, reactive } from 'vue'
import '@/styles/animation.less'

const graphContainer = ref()

let graph: any = null

const state = reactive({
  data: {
    nodes: [
      {
        id: 'ac51fb2f-2753-4852-8239-53672a29bb14',
        x: -340,
        y: -160,
        ports: [
          {
            id: 'ac51fb2f-2753-4852-8239-53672a29bb14_out',
            group: 'out'
          }
        ],
        data: {
          name: '数据输入_1',
          type: 'OUTPUT',
          checkStatus: 'sucess'
        },
        attrs: {
          body: {
            fill: '#EFF4FF',
            stroke: '#5F95FF'
          },
          image: {
            'xlink:href': 'http://localhost:20002/src/assets/imgs/rice.png'
          },
          label: {
            text: '春望'
          },
          text: {
            fill: '#5F95FF',
            text: '开始'
          }
        }
      },
      {
        id: '81004c2f-0413-4cc6-8622-127004b3befa',
        x: -340,
        y: -10,
        ports: [
          {
            id: '81004c2f-0413-4cc6-8622-127004b3befa_in',
            group: 'in'
          },
          {
            id: '81004c2f-0413-4cc6-8622-127004b3befa_out',
            group: 'out'
          }
        ],
        data: {
          name: '数据输入_1',
          type: 'SYAN',
          checkStatus: 'sucess'
        },
        attrs: {
          body: {
            fill: '#edc3ae',
            stroke: '#f9723d'
          },
          image: {
            'xlink:href': 'http://localhost:20002/src/assets/imgs/persimmon.png'
          },
          label: {
            text: '国破山河在'
          },
          text: {
            fill: '#f9723d',
            text: '数据同步'
          }
        }
      },
      {
        id: '7505da25-1308-4d7a-98fd-e6d5c917d35d',
        x: -140,
        y: 180,
        ports: [
          {
            id: '7505da25-1308-4d7a-98fd-e6d5c917d35d_in',
            group: 'in'
          }
        ],
        data: {
          name: '数据输入_1',
          type: 'INPUT',
          checkStatus: 'sucess'
        },
        attrs: {
          body: {
            fill: '#b9dec9',
            stroke: '#229453'
          },
          image: {
            'xlink:href': 'http://localhost:20002/src/assets/imgs/lime.png'
          },
          label: {
            text: '城春草木胜'
          },
          text: {
            fill: '#229453',
            text: '结束'
          }
        }
      }
    ],
    edges: [
      {
        attrs: { line: { strokeDasharray: '5 5' } },
        connector: { name: 'curveConnector' },
        id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c',
        shape: 'data-processing-curve',
        source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14', port: '_out' },
        target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '_in' },
        zIndex: -1
      },
      {
        attrs: { line: { strokeDasharray: '5 5' } },
        connector: { name: 'curveConnector' },
        id: '8cbce713-54be-4c07-8efa-59c505f74ad7',
        labels: ['下半句'],
        shape: 'data-processing-curve',
        source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa', port: '_out' },
        target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d', port: '_in' }
      }
    ]
  },
  // 节点状态列表
  nodeStatusList: [
    {
      id: 'ac51fb2f-2753-4852-8239-53672a29bb14',
      status: 'success'
    },
    {
      id: '81004c2f-0413-4cc6-8622-127004b3befa',
      status: 'success'
    }
  ],

  // 边状态列表
  edgeStatusList: [
    {
      id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c',
      status: 'success'
    },
    {
      id: '8cbce713-54be-4c07-8efa-59c505f74ad7',
      status: 'executing'
    }
  ]
})

// const { data } = toRefs(state)

// // 节点类型
// enum NodeType {
//   INPUT = 'INPUT', // 数据输入
//   FILTER = 'FILTER', // 数据过滤
//   JOIN = 'JOIN', // 数据连接
//   UNION = 'UNION', // 数据合并
//   AGG = 'AGG', // 数据聚合
//   OUTPUT = 'OUTPUT' // 数据输出
// }

function init() {
  graph = new Graph({
    container: graphContainer.value,
    interacting: function () {
      return { nodeMovable: false }
    },
    grid: true,
    panning: {
      enabled: false,
      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: 'bottom',
        args: {
          dx: 0
        }
      },
      targetAnchor: {
        name: 'top',
        args: {
          dx: 0
        }
      },
      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') === 'out') {
          return false
        }
        return true
      }
    }
  })
  graph.centerContent()

  // #region 初始化图形
  const ports = {
    groups: {
      in: {
        position: 'top',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      out: {
        position: 'bottom',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      left: {
        position: 'left',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      },
      right: {
        position: 'right',
        attrs: {
          circle: {
            r: 4,
            magnet: true,
            stroke: '#5F95FF',
            strokeWidth: 1,
            fill: '#fff',
            style: {
              visibility: 'hidden'
            }
          }
        }
      }
    }
    // items: [
    //   {
    //     id: state.currentCode + '_in',
    //     group: 'top',
    //   },
    //   {
    //     id: state.currentCode + '_out',
    //     group: 'out',
    //   }
    // ],
  }

  Graph.registerNode(
    'custom-node',
    {
      inherit: 'rect',
      width: 140,
      height: 76,
      attrs: {
        body: {
          strokeWidth: 1
        },
        image: {
          width: 16,
          height: 16,
          x: 12,
          y: 6
        },
        text: {
          refX: 40,
          refY: 15,
          fontSize: 15,
          'text-anchor': 'start'
        },
        label: {
          text: 'Please nominate this node',
          refX: 10,
          refY: 30,
          fontSize: 12,
          fill: 'rgba(0,0,0,0.6)',
          'text-anchor': 'start',
          textWrap: {
            width: -10, // 宽度减少 10px
            height: '70%', // 高度为参照元素高度的一半
            ellipsis: true, // 文本超出显示范围时,自动添加省略号
            breakWord: true // 是否截断单词
          }
        }
      },
      markup: [
        {
          tagName: 'rect',
          selector: 'body'
        },
        {
          tagName: 'image',
          selector: 'image'
        },
        {
          tagName: 'text',
          selector: 'text'
        },
        {
          tagName: 'text',
          selector: 'label'
        }
      ],
      data: {},
      relation: {},
      ports: { ...ports }
    },
    true
  )

  // 注册连线
  Graph.registerConnector(
    'curveConnector',
    (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
  )
}

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)

function getData() {
  let cells = [] as any
  const location = state.data
  location.nodes.map((node) => {
    cells.push(
      graph.addNode({
        id: node.id,
        x: node.x,
        y: node.y,
        shape: 'custom-node',
        attrs: node.attrs,
        ports: node.ports,
        data: node.data
      })
    )
  })
  location.edges.map((edge) => {
    cells.push(
      graph.addEdge({
        id: edge.id,
        source: edge.source,
        target: edge.target,
        zIndex: edge.zIndex,
        shape: 'data-processing-curve',
        connector: { name: 'curveConnector' },
        labels: edge.labels,
        attrs: edge.attrs
      })
    )
  })
  graph.resetCells(cells)
}

// 开启边的运行动画
const excuteAnimate = (edge: any) => {
  edge.attr({
    line: {
      stroke: '#3471F9'
    }
  })
  edge.attr('line/strokeDasharray', 5)
  edge.attr('line/style/animation', 'running-line 30s infinite linear')
}

// 显示边状态
const showEdgeStatus = () => {
  state.edgeStatusList.forEach((item) => {
    const edge = graph.getCellById(item.id)
    if (item.status == 'success') {
      edge.attr('line/strokeDasharray', 0)
      edge.attr('line/stroke', '#52c41a')
    } else if ('error' == item.status) {
      edge.attr('line/stroke', '#ff4d4f')
    } else if ('executing' == item.status) {
      excuteAnimate(edge)
    }
  })
}

onMounted(() => {
  init()
  // graph.fromJSON(state.data);
  getData()
  showEdgeStatus()
})
</script>

<style lang="less" scoped>
.content-main {
  display: flex;
  width: 100%;
  flex-direction: column;
  height: calc(100vh - 85px - 40px);
  background-color: #ffffff;
  position: relative;
}
.content-container {
  position: relative;
  width: 100%;
  height: 100%;
  .content {
    width: 100%;
    height: 100%;
    position: relative;

    min-width: 400px;
    min-height: 600px;
    display: flex;
    border: 1px solid #dfe3e8;
    flex-direction: row;
    //   flex-wrap: wrap;
    flex: 1 1;

    .graph-content {
      width: calc(100%);
      height: 100%;
    }
  }
}

:deep(.x6-widget-transform) {
  margin: -1px 0 0 -1px;
  padding: 0px;
  border: 1px solid #239edd;
}
:deep(.x6-widget-transform > div) {
  border: 1px solid #239edd;
}
:deep(.x6-widget-transform > div:hover) {
  background-color: #3dafe4;
}
:deep(.x6-widget-transform-active-handle) {
  background-color: #3dafe4;
}
:deep(.x6-widget-transform-resize) {
  border-radius: 0;
}
:deep(.x6-widget-selection-inner) {
  border: 1px solid #239edd;
}
:deep(.x6-widget-selection-box) {
  opacity: 0;
}

.topic-image {
  visibility: hidden;
  cursor: pointer;
}
.x6-node:hover .topic-image {
  visibility: visible;
}
.x6-node-selected rect {
  stroke-width: 2px;
}
</style>

总结 

到此这篇关于vue3 + antv/x6实现流程图的文章就介绍到这了,更多相关vue3+antv/x6流程图内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • vue组件props属性监听不到值变化问题

    vue组件props属性监听不到值变化问题

    这篇文章主要介绍了vue组件props属性监听不到值变化问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue3 父控件远程获取数据在子组件上显示不出来的解决方案

    vue3 父控件远程获取数据在子组件上显示不出来的解决方案

    这篇文章主要介绍了vue3 父控件远程获取数据,在子组件上显示不出来,本文给大家分享两种解决方案帮助大家解决这个问题,需要的朋友可以参考下
    2023-08-08
  • VUE登录注册页面完整代码(直接复制)

    VUE登录注册页面完整代码(直接复制)

    这篇文章主要给大家介绍了关于VUE登录注册页面的相关资料,在Vue中可以使用组件来构建登录注册页面,文中通过图文以及代码介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • vue路由跳转时判断用户是否登录功能的实现

    vue路由跳转时判断用户是否登录功能的实现

    下面小编就为大家带来一篇vue路由跳转时判断用户是否登录功能的实现。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 深入浅析Vue.js 中的 v-for 列表渲染指令

    深入浅析Vue.js 中的 v-for 列表渲染指令

    当遍历一个数组或枚举一个对象进行迭代循环展示时,就会用到列表渲染指令 v-for。这篇文章主要介绍了Vue.js 中的 v-for 列表渲染指令,需要的朋友可以参考下
    2018-11-11
  • 在小程序/mpvue中使用flyio发起网络请求的方法

    在小程序/mpvue中使用flyio发起网络请求的方法

    这篇文章主要介绍了在小程序/mpvue中使用flyio发起网络请求的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-09-09
  • 前端vue实现甘特图功能

    前端vue实现甘特图功能

    dhtmlxGantt是一个强大的JavaScript Gantt图表库,提供易于使用、高度可自定义的Gantt图表组件,它支持多项任务和进度条,以及多种列和行布局,可用于创建各种类型的时间线和计划表,本文给大家介绍前端vue实现甘特图的相关知识,感兴趣的朋友跟随小编一起看看吧
    2024-04-04
  • Vue实现动态路由导航的示例

    Vue实现动态路由导航的示例

    本文主要介绍了Vue实现动态路由导航的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • 前端uniapp微信小程序跨域问题的解决方法

    前端uniapp微信小程序跨域问题的解决方法

    跨域指的是在浏览器中,当一个网页尝试加载另一个不同域名(或协议、端口号)下的资源时所面临的限制,这篇文章主要给大家介绍了关于前端uniapp微信小程序跨域问题的解决方法,需要的朋友可以参考下
    2024-08-08
  • 解决vuex改变了state的值,但是页面没有更新的问题

    解决vuex改变了state的值,但是页面没有更新的问题

    这篇文章主要介绍了解决vuex改变了state的值,但是页面没有更新的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11

最新评论