Criando um Fluxograma Arrastável com Vue3 e AntV X6 em Minutos

Construindo um Editor de Fluxograma com Suporte a Arrastar e Soltar Usando Vue3 e AntV X6

O desenvolvimento de ferramentas visuais para edição tornou-se essencial no cenário atual da programação front-end. Seja para criar designer de fluxos de trabalho, representar arquiteturas de sistemas ou visualizar relações entre dados, precisamos de soluções图形高效。O AntV X6 se destaca como um mecanismo gráfico robusto, e quando combinado com o Vue3, oferece uma experiência de desenvolvimento excepcional. Neste guia, você aprenderá a implementar rapidamente um editor de fluxograma com funcionalidade de arrastar e soltar, além de explorar técnicas avançadas para criar nodes personalizados.

Preparação do Ambiente e Integração Básica

Inicie criando um projeto Vue3 utilizando o Vite:

npm create vite@latest meu-projeto-x6 --template vue
cd meu-projeto-x6

Instale a biblioteca principal do AntV X6:

npm install @antv/x6 --save

Para projetos Vue3, é necessário instalar também o pacote de suporte a nodes Vue:

npm install @antv/x6-vue-shape --save

A seguir, apresentamos o código básico para integração:

<template>
  <div id="canvas" class="x6-graph"></div>
</template>

<script setup>
import { onMounted } from 'vue'
import { Graph } from '@antv/x6'
import { register } from '@antv/x6-vue-shape'

onMounted(() => {
  const diagrama = new Graph({
    container: document.getElementById('canvas'),
    width: 800,
    height: 600,
    grid: true,
    background: {
      color: '#f5f5f5'
    }
  })
})
</script>

<style>
.x6-graph {
  border: 1px solid #e2e2e3;
  border-radius: 4px;
}
</style>

Implementando a Funcionalidade de Arrastar e Soltar

Para permitir que o usuário arraste elementos do painel lateral para a área de desenho, precismaos tratar três eventos importantes:

  1. Início do arraste: Armazenar os dados do elemento que está sendo movido
  2. Mover sobre o canvas: Permitir o comportamento de soltar através de event.preventDefault()
  3. Soltar o elemento: Criar o nó no local onde o usuário soltou
<template>
  <div class="editor-layout">
    <div class="sidebar">
      <div 
        v-for="item in tiposNodos" 
        :key="item.tipo"
        class="draggable-item"
        draggable="true"
        @dragstart="iniciarArraste(item)"
      >
        {{ item.rotulo }}
      </div>
    </div>
    <div 
      id="canvas" 
      class="x6-graph"
      @dragover="permitirSoltura"
      @drop="processarSolta"
    ></div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Graph } from '@antv/x6'

const tiposNodos = ref([
  { tipo: 'inicio', rotulo: 'Nó Inicial' },
  { tipo: 'termino', rotulo: 'Nó Final' },
  { tipo: 'processamento', rotulo: 'Processamento' },
  { tipo: 'decisao', rotulo: 'Decisão' }
])

const nodoArrastado = ref(null)
const diagrama = ref(null)

onMounted(() => {
  diagrama.value = new Graph({
    container: document.getElementById('canvas'),
    width: 800,
    height: 600,
    grid: true,
    connecting: {
      router: 'manhattan',
      connector: 'rounded'
    }
  })
})

const iniciarArraste = (item) => {
  nodoArrastado.value = item
}

const permitirSoltura = (evento) => {
  evento.preventDefault()
}

const processarSolta = (evento) => {
  evento.preventDefault()
  if (!nodoArrastado.value || !diagrama.value) return
  
  const coordenadas = diagrama.value.clientToLocal(evento.clientX, evento.clientY)
  adicionarNodo(nodoArrastado.value.tipo, coordenadas.x, coordenadas.y)
}

const adicionarNodo = (tipo, x, y) => {
  const configBase = {
    x,
    y,
    width: 120,
    height: 40,
    attrs: {
      body: {
        stroke: '#8f8f8f',
        strokeWidth: 1,
        fill: '#fff',
        rx: 4,
        ry: 4
      },
      label: {
        text: nodoArrastado.value.rotulo,
        fill: '#333',
        fontSize: 12
      }
    }
  }

  switch (tipo) {
    case 'inicio':
      diagrama.value.addNode({
        ...configBase,
        shape: 'rect',
        attrs: {
          ...configBase.attrs,
          body: {
            ...configBase.attrs.body,
            fill: '#ffd591'
          }
        }
      })
      break
    case 'termino':
      diagrama.value.addNode({
        ...configBase,
        shape: 'ellipse',
        attrs: {
          ...configBase.attrs,
          body: {
            ...configBase.attrs.body,
            fill: '#ffccc7'
          }
        }
      })
      break
    default:
      diagrama.value.addNode(configBase)
  }
}
</script>

<style>
.editor-layout {
  display: flex;
  height: 100vh;
}
.sidebar {
  width: 200px;
  padding: 10px;
  border-right: 1px solid #e8e8e8;
}
.draggable-item {
  padding: 8px 12px;
  margin-bottom: 8px;
  background: #fafafa;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  cursor: move;
}
.draggable-item:hover {
  border-color: #1890ff;
  color: #1890ff;
}
.x6-graph {
  flex: 1;
  border: 1px solid #e2e2e3;
}
</style>

Criação de Nodes Personalizados Avançados

Uma das funcionalidades mais poderosas do AntV X6 é a capacidade de utilizar componentes Vue como conteúdo de nodes. A seguir, implementaremos um nó personalizado que exibe uma barra de progresso:

Primeiro, crie o componente do nó personalizado:

<!-- components/NodoProgresso.vue -->
<template>
  <div class="custom-progress-node">
    <div class="header">{{ dadosNodo.titulo }}</div>
    <div class="progress-container">
      <div 
        class="progress-bar"
        :style="{ width: `${progresso}%` }"
      ></div>
      <span class="progress-text">{{ progresso }}%</span>
    </div>
  </div>
</template>

<script setup>
import { ref, defineProps } from 'vue'

const props = defineProps({
  dadosNodo: {
    type: Object,
    required: true
  }
})

const progresso = ref(props.dadosNodo.valor || 0)
</script>

<style scoped>
.custom-progress-node {
  width: 100%;
  height: 100%;
  padding: 8px;
  background: #fff;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
}
.header {
  font-weight: bold;
  margin-bottom: 6px;
}
.progress-container {
  position: relative;
  height: 20px;
  background: #f0f0f0;
  border-radius: 3px;
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  background: #1890ff;
  transition: width 0.3s ease;
}
.progress-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 11px;
  color: #333;
}
</style>

Agora, registe o componente e utilize-o no fluxograma:

<script setup>
import { onMounted, ref } from 'vue'
import { Graph } from '@antv/x6'
import { register } from '@antv/x6-vue-shape'
import NodoProgresso from './components/NodoProgresso.vue'

// Registrando o componente Vue como um nó do X6
register({
  shape: 'vue-progress-node',
  width: 180,
  height: 70,
  component: NodoProgresso
})

const diagrama = ref(null)
const dadosNodoProgresso = ref({
  titulo: 'Projeto Alpha',
  valor: 65
})

onMounted(() => {
  diagrama.value = new Graph({
    container: document.getElementById('canvas'),
    width: 800,
    height: 600,
    grid: true
  })

  // Adicionando o nó personalizado ao canvas
  diagrama.value.addNode({
    shape: 'vue-progress-node',
    x: 300,
    y: 200,
    dadosNodo: dadosNodoProgresso.value
  })
})
</script>

Considerações Finais

O AntV X6 integrada ao Vue3 proporciona uma base sólida para construção de editores visuais complexos. As técnicas apresentadas neste artigo — desde a configuração inicial até a criação de componentes Vue como nodes — permitem desenvolver soluções personalizadas para diversas necessidades de edição graphical.

Para expandir ainda mais este projeto, considere implementar funcionalidades como salvamento e carregamento de diagramas, conexão entre nodes com validação, toolbar de ferramentas e recursos de zoom e pan.

Publicado em 7-4 11:11