当前位置: 代码网 > it编程>编程语言>Javascript > vue3 + antv/x6实现流程图的全过程

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

2024年07月02日 Javascript 我要评论
新建流程图// adddag.vue<template> <div class="content-main"> <div class="tool-containe

新建流程图

// 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流程图内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com