当前位置: 首页 > article >正文

【组件库】使用Vue2+AntV X6+ElementUI 实现拖拽配置自定义vue节点

    先来看看实现效果:
在这里插入图片描述

    【组件库】使用 AntV X6 + ElementUI 实现拖拽配置自定义 Vue 节点
在现代前端开发中,流程图和可视化编辑器的需求日益增加。AntV X6 是一个强大的图形化框架,支持丰富的图形操作和自定义功能。结合 ElementUI,我们可以轻松实现一个基于 Vue 的拖拽配置流程图,支持自定义节点和动态字段编辑。

一、技术栈介绍

AntV X6:一个基于 HTML5 Canvas 的图形化框架,支持流程图、拓扑图等多种图形化场景。

  • ElementUI:一个基于 Vue 的 UI 组件库,提供丰富的表单、表格和弹窗组件。
  • Vue.js:一个渐进式 JavaScript 框架,用于构建用户界面。

二、项目需求

我们的目标是实现一个流程图编辑器,支持以下功能:
拖拽添加节点:用户可以通过拖拽的方式在画布上添加自定义节点。
节点配置:通过弹窗对话框配置节点的属性,包括实体名称和字段信息。
字段动态编辑:支持动态添加、删除字段,并提供字段类型选择。
数据导入导出:支持从 JSON 文件导入数据,以及将当前流程图导出为 JSON 文件。

三、实现步骤

1. 初始化项目

首先,确保你已经安装了 Vue CLI 和相关依赖。创建一个新的 Vue 项目,并安装以下依赖:

vue create x6-vue-flow
cd x6-vue-flow
npm install @antv/x6 @antv/x6-plugin-dnd @antv/x6-vue-shape element-ui

2. 创建主组件

在 src/components/FlowEditor.vue 中,实现流程图编辑器的主组件。以下是核心代码:

<template>
  <div class="flow-container">
    <!-- 导航栏 -->
    <div class="flow-nav">
      <div class="add-entity" @mousedown="startDrag">添加实体</div>
      <el-button type="primary" plain size="medium">保存</el-button>
      <el-button type="warning" plain size="medium">清空</el-button>
      <el-button type="success" plain size="medium">导出</el-button>
      <el-upload accept=".json" :on-progress="handleOnProgress" action="" :show-file-list="false">
        <el-button plain size="primary">导入</el-button>
      </el-upload>
    </div>

    <!-- 画布 -->
    <div class="flow-content">
      <div id="container"></div>
    </div>

    <!-- 实体编辑对话框 -->
    <el-dialog title="新增实体" :visible.sync="dialogVisible" width="45%">
      <el-form ref="form" :model="formData" label-width="130px" :rules="rules" size="small">
        <el-form-item prop="entity_name_CN" label="逻辑实体中文名">
          <el-input v-model="formData.entity_name_CN" clearable></el-input>
        </el-form-item>
        <el-form-item prop="entity_name_EN" label="逻辑实体英文名">
          <el-input v-model="formData.entity_name_EN" clearable></el-input>
        </el-form-item>
        <div class="field-container">
          <div class="field-container__title">
            <div class="primary-title">字段信息</div>
            <el-button type="primary" size="small" icon="el-icon-plus" @click="addField">新增一行</el-button>
          </div>
          <el-table :data="formData.formField" height="250">
            <el-table-column type="index" width="50" label="序号" align="center"></el-table-column>
            <el-table-column prop="cname" label="字段中文名" width="150" align="center">
              <template v-slot:default="{ $index }">
                <el-input size="small" v-model="formData.formField[$index].cname"></el-input>
              </template>
            </el-table-column>
            <el-table-column prop="ename" label="字段英文名" width="200" align="center">
              <template v-slot:default="{ $index }">
                <el-input size="small" v-model="formData.formField[$index].ename"></el-input>
              </template>
            </el-table-column>
            <el-table-column prop="efType" label="字段类型" align="center">
              <template v-slot:default="{ $index }">
                <el-select v-model="formData.formField[$index].efType" size="small">
                  <el-option v-for="item in options" :key="item" :label="item" :value="item"></el-option>
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right" align="center" width="80">
              <template v-slot:default="{ $index }">
                <el-button type="danger" size="small" @click="handleFieldDelete($index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-form>
      <span slot="footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="onSubmit(formData)">确定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { Graph, Shape } from '@antv/x6';
import { Dnd } from '@antv/x6-plugin-dnd';
import { register } from '@antv/x6-vue-shape';
import CellNode from './CellNode.vue';

export default {
  name: 'FlowEditor',
  data() {
    return {
      graph: null,
      dialogVisible: false,
      formData: {
        entity_name_CN: '',
        entity_name_EN: '',
        formField: [{ cname: '', ename: '', efType: '' }],
      },
      options: ['STRING', 'NUMBER', 'BOOLEAN', 'DATE', 'EMAIL'],
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.initGraph();
    });
  },
  methods: {
    initGraph() {
      const container = document.getElementById('container');
      const config = {
        container,
        width: '100%',
        height: '100%',
        autoResize: true,
        panning: true,
        mousewheel: true,
      };
      this.graph = new Graph(config);
      const dnd = new Dnd({
        target: this.graph,
        validateNode: (node) => {
          this.currentDragNode = node;
          this.dialogVisible = true;
          return false;
        },
      });
      this.dnd = dnd;
    },
    startDrag(e) {
      const node = this.graph.createNode({
        shape: 'CellNode',
      });
      this.dnd.start(node, e);
    },
    onSubmit(data) {
      this.dialogVisible = false;
      const node = this.graph.addNode(this.currentDragNode);
      node.setData(data);
    },
    addField() {
      this.formData.formField.push({ cname: '', ename: '', efType: '' });
    },
    handleFieldDelete(index) {
      this.formData.formField.splice(index, 1);
    },
  },
};
</script>

3. 创建自定义 Vue 节点

在 src/components/CellNode.vue 中,实现自定义的 Vue 节点组件。以下是代码:

<template>
  <div style="background-color: #ffffff;">
    <el-table :data="formData.formField" style="width: 100%" height="250">
      <el-table-column label="资源名称" :formatter="() => formData.entity_name_CN"></el-table-column>
      <el-table-column prop="cname" label="中文名"></el-table-column>
      <el-table-column prop="ename" label="英文名"></el-table-column>
      <el-table-column prop="efType" label="类型"></el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  name: 'CellNode',
  inject: ['getNode'],
  data() {
    return {
      node: null,
      formData: {
        entity_name_CN: '',
        entity_name_EN: '',
        formField: [{ cname: '', ename: '', efType: '' }],
      },
    };
  },
  mounted() {
    const node = this.getNode();
    this.node = node;
    node.on('change:data', (data) => {
      if (data.cell && data.cell.data) {
        this.formData = node.getData();
      }
    });
  },
};
## 4. 注册自定义节点 在主组件中,使用 @antv/x6-vue-shape 注册自定义的 Vue 节点:
import { register } from '@antv/x6-vue-shape';
import CellNode from './CellNode.vue';

register({
  shape: 'CellNode',
  width: 300,
  height: 250,
  component: CellNode,
});
  1. 样式优化
    在 src/styles/index.scss 中,添加全局样式:
.flow-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;

  .flow-nav {
    display: flex;
    align-items: center;
    margin-bottom: 5px;

    .add-entity {
      border-radius: 6px;
      background: rgba(64, 158, 255, 0.7);
      border-color: #409eff;
      border-style: dashed;
      text-align: center;
      color: #fff;
      font-size: 14px;
      padding: 5px;
      box-sizing: content-box;
      width: 100px;
      cursor: pointer;
    }
  }

  .flow-content {
    flex: 1;
  }
}

四、运行项目

完成以上步骤后,运行项目:

npm run serve

打开浏览器访问 http://localhost:8080,你将看到一个支持拖拽添加节点、动态字段编辑的流程图编辑器。

五、总结

    通过 AntV X6 和 ElementUI 的结合,我们实现了一个功能丰富的流程图编辑器。AntV X6 提供了强大的图形化能力,ElementUI 提供了丰富的 UI 组件,两者结合可以快速搭建出高效的可视化工具。

完整代码

Graph.vue代码

// Graph.vue
<template>
    <div class="flow-container">
        <!-- 导航栏 -->
        <div class="flow-nav">
            <div class="add-entity" @mousedown="startDrag">添加实体</div>
            <el-button type="primary" style="margin-left: 10px" plain  size="medium">保存</el-button>
            <el-button type="warning" style="margin-left: 10px" plain size="medium">清空</el-button>
            <el-button type="success" style="margin-left: 10px" plain  size="medium">导出</el-button>
            <el-upload accept=".JSON" :on-progress="handleOnProgress" action="" :show-file-list="false">
                <el-button plain size="primary" style="margin-left: 10px">导入</el-button>
            </el-upload>
        </div>
        <!-- 画布 -->
        <div class="flow-content" >
            <div id="container"></div>
        </div>
        <!-- 实体编辑对话框 -->
        <el-dialog class="dialog-box" title="新增实体" :visible.sync="dialogVisible" width="45%"  :before-close="handleClose" append-to-body="false">
            <div class="content-wrapper">
      <div class="primary-title">基本信息</div>
      <el-form
        ref="form"
        :model="formData"
        label-width="130px"
        :rules="rules"
        size="small"
      >
        <el-form-item prop="entity_name_CN" label="逻辑实体中文名">
          <el-input v-model="formData.entity_name_CN" clearable></el-input>
        </el-form-item>
        <el-form-item prop="entity_name_EN" label="逻辑实体英文名">
          <el-input v-model="formData.entity_name_EN" clearable></el-input>
        </el-form-item>

      <div class="field-container">
        <div class="field-container__title">
          <div class="primary-title">字段信息</div>
          <div class="field-operation">
            <el-button
              type="primary"
              size="small"
              icon="el-icon-plus"
              @click="addField"
              >新增一行</el-button
            >
          </div>
        </div>
        <el-table :data="formData.formField" label-position="center" height="250">
          <el-table-column type="index" width="50" label="序号" align="center">
          </el-table-column>
          <el-table-column
            property="cname"
            label="字段中文名"
            width="150"
            align="center"
          >
          <template v-slot:default="{ $index }" >
            <el-input size="small" v-model="formData.formField[$index].cname"></el-input>
          </template>
        </el-table-column>
          <el-table-column
            property="ename"
            label="字段英文名"
            width="200"
            align="center"
          >
          <template v-slot:default="{ $index }" >
            <el-input size="small" v-model="formData.formField[$index].ename"></el-input>
          </template>
        </el-table-column>
          <el-table-column
            property="efType"
            label="字段类型"
            align="center"
          >
        <template v-slot:default="{ $index }">
          <el-select
          v-model="formData.formField[$index].efType"
          default-first-option
          placeholder="请选择字段类型"
          clearable
          size="small"
        >
          <el-option
            v-for="item in options"
            :key="item"
            :label="item"
            :value="item"
          >
          </el-option>
        </el-select>
        </template>
        </el-table-column>
          <el-table-column label="操作" fixed="right" align="center" width="80">
            <template v-slot:default="{ $index }">
              <el-button
                type="danger"
                size="small"
                @click="handleFieldDelete($index)"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-form>
    </div>
            <span slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="onSubmit(formData)">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
// import index from '../../index.js'
import { Graph, Shape } from '@antv/x6'
import { Dnd } from '@antv/x6-plugin-dnd'
import { register } from '@antv/x6-vue-shape'
import CellNode from './components/cellNode.vue'

// 画布配置
const config = {
  container: null,
  width: '100%',
  height: '100%',
  autoResize: true,
  // 拖拽画布
  panning: true,
  mousewheel: true,
  connecting: {
    snap: true, // 是否开启连线自动吸附
    highlight: true, // 是否高亮显示连线
    router: 'manhattan',
    connectionPoint: 'anchor',
    anchor: 'center',
    allowBlank: false,
    // 不允许创建循环连线,即边的起始节点和终止节点为同一节点
    allowEdge: false,
    // 不允许起点终点相同
    allowLoop: false,
    //  是否允许边连接到非节点上
    allowNode: false,
    // 起始和终止节点的相同连接桩之间只允许创建一条边
    allowMulti: 'witPort'
  },
  background: {
    color: '#F2F7FA'
  },
  grid: {
    visible: true,
    type: 'doubleMesh',
    args: [
      {
        color: '#eee', // 主网格线颜色
        thickness: 1 // 主网格线宽度
      },
      {
        color: '#ddd', // 次网格线颜色
        thickness: 1, // 次网格线宽度
        factor: 4 // 主次网格线间隔
      }
    ]
  }
}
// 连接桩配置
const ports = {
  groups: {
    // 对话框需要的一些外部数据
    right: {
      position: 'right',
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: '#5F95FF',
          strokeWidth: 1,
          fill: '#fff'
        }
      }
    },
    left: {
      position: 'left',
      attrs: {
        circle: {
          r: 5,
          magnet: true,
          stroke: '#5F95FF',
          strokeWidth: 1,
          fill: '#fff'
        }
      }
    }
  }
}
// 注册HTML节点
Shape.HTML.register({
  shape: 'custom-html',
  width: 160,
  height: 80,
  effect: ['data'],
  html (formData) {
    const data = formData
    const div = document.createElement('div')
    div.className = 'custom-html'
    const span1 = document.createElement('span')
    const span2 = document.createElement('span')
    const span3 = document.createElement('span')
    span1.innerText = '1111'
    span2.innerText = '2222'
    span3.innerText = '3333'
    div.appendChild(span1)
    div.appendChild(span2)
    div.appendChild(span3)
    return div
  }
})
// 注册Vue节点
register({
  shape: 'CellNode',
  width: 300,
  height: 250,
  component: CellNode,
  ports: {
    ...ports,
    items: [{ group: 'left' }, { group: 'right' }]
  }
})
// 注册边
Graph.registerEdge(
  'dag-edge',
  {
    inherit: 'edge',
    attrs: {
      line: {
        stroke: '#C2C8D5',
        strokeWidth: 1,
        targetMarker: null
      }
    }
  },
  true
)

export default {
  name: 'vue-flow',
  data () {
    return {
      graph: null,
      currentEdge: null,
      dnd: null,
      dialogVisible: false,
      currentDragNode: null,
      // 对话框类型 编辑/新增
      formData: {
        entity_name_CN: '',
        entity_name_EN: '',
        // field_list: [],
        formField: [{
          cname: '',
          ename: '',
          efType: ''
        }]
      },
      options: [
        'STRING',
        'NUMBER',
        'BOOLEAN',
        'DATE',
        'EMAIL',
        'URL',
        'ARRAY',
        'OBJECT',
        'TEXTAREA',
        'SELECT',
        'RADIO',
        'CHECKBOX',
        'PASSWORD',
        'FILE',
        'IMAGE',
        'RANGE',
        'COLOR',
        'TEL',
        'SEARCH',
        'DATETIME',
        'DATETIME_LOCAL',
        'MONTH',
        'WEEK',
        'TIME',
        'HIDDEN'
      ],
      rules: {
        entity_name_CN: [
          { required: true, trigger: 'blur', message: '请输入逻辑实体中文名' }
        ],
        entity_name_EN: [
          { required: true, trigger: 'blur', message: '请输入逻辑实体英文名' }
        ]
      }
    }
  },
  provide () {
    return {
      // 要用箭头函数保证this在其他组件获取正确
      getGraph: () => {
        return this.graph
      }
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.initGraph()
    })
  },
  methods: {
    initGraph () {
      // 容器dom
      const container = document.getElementById('container')
      config.container = container
      // 实例化画布
      this.graph = new Graph(config)
      // 实例化拖拽节点
      const dnd = new Dnd({
        target: this.graph,
        validateNode: (node) => {
          this.currentDragNode = node
          this.dialogVisible = true
          return false
        }
      })
      this.dnd = dnd
      this.graph.centerContent() // 居中显示
    },
    startDrag (e) {
      const node = this.graph.createNode({
        shape: 'CellNode'
      })
      this.dnd.start(node, e)
    },
    handleClose (done) {
      this.$confirm('确认关闭?')
        .then(_ => {
          done()
        })
        .catch(_ => {})
    },
    // 添加节点
    addNode (node) {
      return this.graph.addNode(node)
    },
    onSubmit (data) {
      this.dialogVisible = false
      if (this.currentDragNode) {
        const node = this.addNode(this.currentDragNode)
        const dataSource = {
          ...data
        }
        // TODO 有异步问题需要处理先写死200ms
        setTimeout(() => {
          node.setData(dataSource)
        }, 200)
      }
    },
    addField () {
      this.formData.formField.push({
        cname: '',
        ename: '',
        efType: ''
      })
    },
    handleFieldDelete () {
      this.formData.formField.splice(0, 1)
    }
  }
}
</script>

<style lang="scss">
$height: 40px;
.custom-html {
  display: flex;
    width: 100%;
    height: 100%;
    align-items: center;
    background-color: #fff;

    span {
      display: inline-block;
      height: $height;
      line-height: $height;
      border: 1px solid #0f7bcc;
      text-align: center;
      min-width: 0;
      flex: 1;
    }
}

.flow-container {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;

    .flow-nav {
        display: flex;
        align-items: center;
        margin-bottom: 5px;

        .add-entity {
            border-radius: 6px;
            background: rgba(64, 158, 255, 0.7);
            background-clip: padding-box;
            border-color: #409eff;
            border-style: dashed;
            text-align: center;
            color: #fff;
            font-size: 14px;
            padding: 5px;
            box-sizing: content-box;
            width: 100px;
            cursor: pointer;
        }
    }

    .flow-content {
        flex: 1;
    }

    .my-selecting {
        border: 1px dashed #40ff7c;
        background-color: #0f7bcc;
    }

    .x6-widget-selection-box {
        border: 0px dashed rgba(0, 0, 0, 0);
        box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, 0.3);
        border-radius: 6px;
        // background-color: #0F7BCC;
        // opacity: 0.1;
    }

    .x6-widget-selection-inner {
        opacity: 0.1;
        border: 5px solid #000000;
        background-color: #0f7bcc;
    }

}
.dialog-box{
  // width: 600%;
  // height: 800px;

  .el-dialog__title{
  color:#fff;
  font-size: 18px;
}

.content-wrapper {
  padding:15px;
  .primary-title {
    font-size: 18px;
    font-weight: 700;
    padding-bottom: 10px;
  }

  .field-container {
    &__title {
      display: flex;
      justify-content: space-between;
    }
  }
}
}
</style>

cellNode.vue代码

<template>
  <div style="background-color:  #ffffff;">
    <el-table
      :data="formData.formField"
      style="width: 100%"
      height="250"
    >
      <el-table-column :label="`资源名称:${formData.entity_name_CN}`">
      <el-table-column
        prop="cname"
        label="中文名"
      >
      </el-table-column>
      <el-table-column
        prop="ename"
        label="英文名"
      >
      </el-table-column>
      <el-table-column
        prop="efType"
        label="类型"
      >
      </el-table-column>
    </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  name: 'CellNode',
  inject: ['getNode'],
  data () {
    return {
      // 当前实体节点
      node: null,
      formData: {
        entity_name_CN: '',
        entity_name_EN: '',
        formField: [
          {
            cname: '',
            ename: '',
            efType: ''
          }
        ]
      }
    }
  },
  mounted () {
    // 获取node节点
    const node = this.getNode()
    this.node = node
    // 节点data改变监听
    node.on('change:data', (data) => {
      if (data.cell && data.cell.data) {
        this.formData = node.getData()
        console.log('🚀 ~ :', this.formData)
      }
    })
  }
}
</script>


http://www.kler.cn/a/513619.html

相关文章:

  • 微软预测 AI 2025,AI Agents 重塑工作形式
  • 微服务知识——4大主流微服务架构方案
  • Java虚拟机面试题:内存管理(中)
  • ovs实现lb负载均衡
  • 解决npm install安装出现packages are looking for funding run `npm fund` for details问题
  • 走进DevOps:让开发与运维齐头并进
  • Springboot sse 示例
  • (done) 并行计算学习 (Day1: 两个简单的 OpenMP 例子)
  • JavaWeb开发(十五)实战-生鲜后台管理系统(二)注册、登录、记住密码
  • 【C++】揭秘类与对象的内在机制(核心卷之深浅拷贝与拷贝构造函数的奥秘)
  • 《从入门到精通:蓝桥杯编程大赛知识点全攻略》(五)-数的三次方根、机器人跳跃问题、四平方和
  • Python 进阶 - Excel 基本操作
  • 智能系统的感知和决策
  • 第15篇:从入门到精通:Python标准库详解
  • LeetCode 热题 100_全排列(55_46_中等_C++)(递归(回溯))
  • 简识JVM私有内存区域栈、数据结构
  • 蓝桥杯R格式--高精度算法模拟
  • 【MySQL】 常见数据类型
  • 10倍数据交付提升 | 通过逻辑数据仓库和数据编织高效管理和利用大数据
  • C#程序关闭时保证所有线程结束的方法
  • elasticsearch 数据导出/导入
  • 【记录】记录项目中的问题
  • Linux常用汇总
  • windows下修改docker的镜像存储地址
  • 易语言模拟真人鼠标轨迹算法 - 防止游戏检测
  • Axios HTTP库基础教程:从安装到GET与POST请求的实现