Bootstrap

基于乐吾乐meta2d从零实现可视化流程图编辑器(七)setting组件框架搭建及其Map组件实现

概要

可视化编辑器已成为前端发展趋势,相关产品层出不穷,但是用户较难根据自身需求去完整实现一个功能较为全面的可视化编辑器,我将采用乐吾乐开源的meta2d.js可视化库来实现一个简单的流程图编辑器,通过这个案例来介绍meta2d的相关功能,并向读者展示如何用meta2d从零出发搭建一个较为完整的项目,让我们在实际项目中来体验meta2d的强大之处吧。

$MUTB]6JJ667KR$SC95VH(1.png

什么是乐吾乐meta2d.js

meta2d是乐吾乐开源的2D图元组成的可视化引擎,集实时数据展示、动态交互、数据管理等一体的全功能2D可视化引擎。能够快速实现数字孪生、大屏可视化、Web组态、SCADA等解决方案。具有实时监控、多样、变化、动态交互、高效、可扩展、支持自动算法、跨平台等特点,最大程度减少研发和运维的成本,并致力于普通业务人员 0 代码开发实现物联网、工业互联网、电力能源、水利工程、智慧农业、智慧医疗、智慧城市等可视化解决方案。

乐吾乐已将其meta2d核心库完全免费开源,本系列教程就是基于meta2d从零实现web端可视化流程图编辑器。

乐吾乐 meta2d开源项目地址:https://github.com/le5le-com/meta2d.js

乐吾乐 meta2d官方文档:https://doc.le5le.com/document/119359590

项目地址

此可视化流程图编辑器项目地址:github.com/Grnetsky/me…

在线体验地址: http://editor.xroot.top/

往期教程

  1. 基本环境搭建: juejin.cn/spost/72617…
  2. 主界面布局及其初始化: juejin.cn/post/726219…
  3. Meta2d核心库图元注册流程及相关概念: juejin.cn/spost/72629…
  4. 侧边栏功能开发:https://juejin.cn/post/7264414580776403003
  5. Nav组件功能实现:https://juejin.cn/post/7264951443344916517
  6. Nav组件扩展-添加工具栏:https://juejin.cn/post/7265692989611147283

7. setting组件框架搭建及其Map组件实现

setting组件部分也叫props组件,该组件包含了与设置有关的核心内容,包括图纸设置、全局配置、图元外观设置、动画设置、事件设置等等,内容丰富且复杂,我们将一个一个来讲解,并在讲解过程中对meta2d的相关API进行介绍,让我们来一点一点挖掘meta2d的潜力,用它做些了不起的东西吧。

基本框架搭建

老规矩,遵循开放封闭原则,为了最大的可扩展性以及可维护性,我们所有的props都将配置在defaultsConfig文件中,然后通过导入到vue文件中进行循环渲染,所以我们首先要去将vue部分的框架搭建好。

整个setting组件分为两个大的部分,一个是为图纸服务的props,简称MapProps,他将展示与图纸和全局配置有关的相关信息并提供更改接口来给予用户最大自由度配置meta2d的相关属性包括文件、图纸基本配置、全局配置等;第二个是为图元服务的的props,简称PenProps,他将展示与图元相关的配置信息,包括图元的外观、事件、动画效果。

我们来进行目录结构的分析,根据以上叙述可知,我们需要分为两个大块,MapProps和PenProps,每个props下面还分为几个子功能界面,所以目录结构应该为这样:

image.png

其中Form组件为核心的循环渲染组件,我们的其他所有组件都将依赖他做表单的渲染处理。像下面这样:

image.png

Form组件是最核心的,所以,接下来让我们来聊聊Form组件的实现。首先来看看Form的源代码(后面可能会增加或更改部分代码)

<script setup>
  const props = defineProps(['formList'])
  </script>

  <template>
    <el-collapse>
      <el-collapse-item  v-for="item in props.formList" :title="item.title">
        <el-form @submit="(e)=>e.preventDefault()">
          <el-form-item v-for="i in item.children" :label="i.title">
  <!--          输入框-->
            <el-input v-model="i.bindProp[i.prop]" :placeholder="i.option?.placeholder || '请输入'" v-if="i.type==='input'" @[i.event]="i.func" :type="i.option?.type||'text'"/>
  <!--          文件框-->
            <input type="file" :accept="i.option?.accept || '*/*'"  v-else-if="i.type==='file'"  @[i.event]="i.func">
  <!--          数字框-->
            <el-input-number v-model="i.bindProp[i.prop]" :min="i.option?.min || -Infinity" :max="i.option?.max || Infinity" @[i.event]="i.func" v-else-if="i.type==='number'"/>
  <!--          选择框-->
            <el-select v-model="i.bindProp[i.prop]" placeholder="Select" v-else-if="i.type==='select'">
              <el-option
                  v-for="item in i.option"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                  :disabled="item.disabled"
              />
            </el-select>

  <!--          取色器-->
            <el-color-picker v-model="i.bindProp[i.prop]" show-alpha v-else-if="i.type === 'color'" @[i.event]="i.func"/>
  <!--          开关-->
            <el-switch v-model="i.bindProp[i.prop]" v-else-if="i.type==='switch'" @[i.event]="i.func"/>

          </el-form-item>
        </el-form>

      </el-collapse-item>
    </el-collapse>
  </template>

  <style scoped>
  :deep(.el-collapse-item__header) {
    font-weight: 1000;
  }
  :deep(.el-collapse-item__content){
    margin-right: 15px;
  }
  </style>

相信上面的代码的逻辑大家一定不陌生,我们之前讲解Nav部分和Icons组件部分已经运用了多次,Form组件接受一个props formList用于获取外部传值,然后在列表中循环渲染根据不同的type值配合v-if指令渲染对应的组件,与之前不同的是,他除了循环数据以外,还增加了对事件函数的处理,也就是说我们完全可以在配置信息中写对应的监听事件和事件处理函数来进一步增强我们的功能,另一个需要注意的是,我们的v-model指令并没有直接绑定在数据源上,而是通过prop提供的属性进行间接绑定,可能不是很好理解,让我们看看我们的配置对象就知道了,拿MapProps中的Map组件举例:

// Map.vue
<script setup>
import Form from "../Form.vue";
import {computed, onMounted, reactive} from "vue";

import { mapProps } from "../../data/defaultsConfig.js";  // 导入mapProps对象
let m = reactive(mapProps)  // 声明为响应式

// 将设置的属性赋值到变量上
function loadOptionsFromMeta2d(options,target){
  for(let i in target){
    target[i] = (options[i] || target[i])
  }
}
// 初始化
onMounted(()=>{
    //监听opened事件用于监听文件打开,根据文件属性赋值对应设置
  meta2d.on('opened',()=>{
    const options = meta2d.data()
    loadOptionsFromMeta2d(options,m)  // 同步到当前设置
    loadOptionsFromMeta2d(options,meta2d.getOptions())  // 同步到meta2d设置
  })
  // 初始化
  const options = meta2d.getOptions()
  loadOptionsFromMeta2d(options,m)
  meta2d.fileName = m.fileName  // 文件名 默认为 “未命名”
})
// map数据 用于传递给Form组件的数据  
const map = computed(()=>{
  return [
    {
      title:"文件", //  title属性 显示一级菜单标题
      children:[
        {
          title:"文件名",  // 元素标题
          type:"input",  // form类型  与Form组件的v-if对应
          option:{  //  表单组件的配置信息 具体配置项根据对应的组件有所不同
            type:"text", 
            placeholder:"请输入文件名"
          },
          prop:"fileName",  // 指定该组件绑定的属性
          bindProp:m,// 该组件绑定的对象  
          event:"change",  // 监听事件类型
          func:function arg1(value){   // 事件回调函数
            meta2d.fileName = value
        }
        },
      ]
    }, {
      title:"画布", //显示名
      children:[
        {
          title:"默认颜色",
          type:"color",
          prop:"color",
          event:"change",
          bindProp:m, // 绑定的属性
          func(value){
            meta2d.setOptions({
              color:value
            })
            meta2d.render()
          }
        },{
          title:"画笔填充颜色",
          type:"color",
          prop:"penBackground",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.store.data.penBackground = value
            meta2d.render()
          }
        },
        {
          title:"背景颜色",
          type:"color",
          prop:"background",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.setBackgroundColor(value)
            meta2d.render()
          }
        },
        {
          title:"背景图片",
          type:"input",
          option:{
            type:"file"
          },
          prop:"backGroundImage",
          bindProp:m, // 绑定的属性
          event:"change",
          func(e){
            let file = e.target.files[0]  
            let fileUrl = URL.createObjectURL(file) // 创建文件引用  
            meta2d.setBackgroundImage(fileUrl)  
            meta2d.render()
          }
        },
        {
          title:"标尺",
          type:"switch",
          prop:"rule",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.setRule({
              rule:value
            })
            meta2d.render()
          }
        },
        {
          title:"标尺颜色",
          type:"color",
          prop:"ruleColor",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.setRule({
              ruleColor:value
            })
            meta2d.render()
          }
        },
        {
          title:"网格",
          type:"switch",
          prop:"grid",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.setGrid({
              grid:value
            })
            meta2d.render()
          }
        },
        {
          title:"网格颜色",
          type:"color",
          prop:"gridColor",
          bindProp:m, // 绑定的属性
          event:"change",
          func(value){
            meta2d.setGrid({
              gridColor:value
            })
            meta2d.render()
          }
        },
        {
          title:"网格大小",
          type:"number",
          prop:"gridSize",
          bindProp:m, // 绑定的属性
          event:"change",
          option:{
            min:1,
            max:100
          },
          func(value){
            console.log(value,+value)
            meta2d.setGrid({
              gridSize:+value
            })
            meta2d.render()
          }
        },
        {
          title:"网格角度",
          type:"number",
          prop:"gridRotate",
          bindProp:m, // 绑定的属性
          event:"change",
          option:{
            min:-Infinity,
            max:Infinity
          },
          func(value){
            meta2d.setGrid({
              gridRotate:+value
            })
            meta2d.render()
          }
        }
      ]
    }
  ]
})
</script>

<template>
<div class="mapProps">
<Form :form-list=map ></Form>
</div>
</template>

<style scoped>

</style>

再来看看mapProps对象

export const mapProps = {
  fileName: "未命名",
  color:"#eeeeee",
  penBackground:"",
  background:"",
  backGroundImage:"",
  rule:false,
  ruleColor:"",
  grid:false,
  gridColor:"",
  gridSize:10,
  gridRotate:90
}

根据上面的代码及其注释,我们来理一下整个过程,首先,我们配置好了Form组件使之成为“万能组件”,根据不同传参来渲染不同的表单,在Form组件中我们通过v-model指令进行了数据绑定,这里是通过间接绑定的方式,原理是通过传参的bindProp属性获取数据源对象,prop属性来指定数据源属性。然后我们需要设定好我们传参的数据和数据源,传参数据用于渲染表单以及初始化相关属性,并且监听对应事件,注册回调函数,数据源用于提供数据并与表单实现数据的双向绑定,然后我们就可以通过对表单操作就进行数据源的实时更新了,另外我们的数据源来自于defaultsConfig文件中,我们需要把所有数据源定义到这里。

就这样,我们的基础框架就基本上清晰了,要添加功能菜单只需要添加数据源并配置好传参数据即可,经过简单的样式调整,最后让我们先来看看Map组件的实现效果吧(先关注UI,功能实现后面讲)。

1690192141359 00_00_00-00_00_30.gif

Map组件功能实现

在上一小节,我们实现了setting组件基本框架的搭建,并且用map组件作为案例讲解了各部分作用以及他是如何工作的,在这一小节,我们着手对具体功能的实现,并在实现功能中讲解meta2d的相关API。

文件名设置

每张图纸都有文件名,我们在保存图纸时都应该为图纸设定文件名,在我们之前的图纸保存的功能实现中,我们是将文件名写死的,在这里,我们应该将文件的名字交给用户来动态的设置,其实这个功能的实现较为简单,只需要在用一个变量存储用户输入的文件名,在保存时将该文件名赋值给文件即可,先来看看文件的表单配置:

// Map.vue 
{
  title:"文件", //显示名
  children:[
    {
      title:"文件名",
      type:"input",
      option:{
        type:"text",
        placeholder:"请输入文件名"
      },
      prop:"fileName",
      bindProp:m,// 绑定的属性
      event:"change", //监听事件
      func:function arg1(value){  // 执行函数
        meta2d.fileName = value  // 设置文件名
    }
    },
  ]
},

为了方便,我们直接将文件名挂载到meta2d的fileName属性身上。导出文件的函数只需要像下面这样更改就行:

// defaultsConfig
saveFile(){
  const jsonData =  window.meta2d.data() 
  const json = JSON.stringify(jsonData)
  const  file = new Blob([json],{type:"application/json"})
  const link = URL.createObjectURL(file)
  let a = document.createElement('a')
  a.setAttribute("download",meta2d.fileName || "未命名")  // 更改处,引入meta2d的fileName属性,缺省则为未命名
  a.setAttribute("href",link)
  a.click()
},

这样功能就实现了,看看效果:

文件名2 00_00_00-00_00_30.gif

画布相关

先给出官网meta2dAPI文档,相关内容可以在里面寻找。

画笔颜色

画布相关的设置主要是对工程的一些基本配置,包括画布背景、画笔颜色、填充颜色、网格、标尺等。在这里我以设置画笔颜色为例子向大家展示如何实现类似功能。

首先第一步,定义好数据源:

export const mapProps = {
    ...
    color:"",
    ...
}

然后定义好表单数据用于生成表单:

{  
title:"画布", //字段名  
    children:[  
    {  
        title:"默认颜色",  
        type:"color",  
        prop:"color",  // 绑定对应的属性名
        event:"change",  
        bindProp:m, // 绑定对象 
        func(value){  
            meta2d.setOptions({ // 设置属性的核心方法
                color:value  
            })  
            meta2d.render()  
    }  
    },...]
...
}

可以看到,我们的事件回调函数只做了两件事,那就是调用了meta2d的setOptipns方法,然后调用render更新视图。meta2d提供了setOptions方法用于更新meta2d的配置信息,我们先看看官网是怎么说的:

image.png

官网说的很简单,设置引擎选项,那么具体有哪些选项可设置呢?
或查看官网信息

名称类型描述
colorstring画笔默认颜色,如果没特别设置,颜色包括:文字和边框
activeColorstring画笔选中颜色
activeBackgroundstring画笔选中背景颜色
hoverColorstring鼠标移动到画笔上的颜色
hoverBackgroundstring鼠标移动到画笔上的背景颜色
anchorColorstring锚点颜色
anchorRadiusnumber锚点半径
anchorBackgroundstring锚点背景颜色
dockColorstring辅助线颜色
dragColorstring鼠标框选多个节点时,边框颜色
animateColorstring连线动画颜色
textColorstring文字颜色
fontFamilystring文字字体
fontSizenumber文字大小
lineHeightnumber文字行高
textAlignstring文字水平对齐方式
textBaselinestring文字垂直对齐方式
rotateCursorstring旋转控制点的鼠标样式
hoverCursorstring鼠标经过画笔的样式
disableInputboolean禁用双击弹出输入框
disableRotateboolean禁止旋转
disableAnchorboolean禁止显示锚点
autoAnchorboolean连线时,自动选中节点锚点
disableEmptyLineboolean禁止存在两端关联缺少的连线
disableRepeatLineboolean禁止存在关联重复的连线
disableScaleboolean禁止画布缩放
disableTranslateboolean禁止画布移动
disableDockLineboolean取消辅助线
minScalenumber画布最小缩放比例
maxScalenumber画布最大缩放比例
keydownKeydownType快捷键监听对象,默认 document;-1 不监听快捷键,需在 Meta2d 初始化时配置
gridboolean是否显示网格
gridColorstring网格颜色
gridSizenumber网格大小
ruleboolean是否显示标尺
ruleColorstring标尺颜色
drawingLineNamestring默认连线类型名称
fromArrowstring默认连线起始箭头
toArrowstring默认连线终点箭头
autoPolylineboolean是否自动计算多线段锚点
intervalnumber绘画帧时长
animateIntervalnumber动画帧时长
dragAllInboolean框选画笔时,是否需要全部在框选区域内
scrollboolean默认是否显示滚动条。与默认缩放互斥
defaultAnchorsPoint[]默认图形的默认锚点,例如:正方形等。
moveConnectedLineboolean是否允许拖动连接线
mouseRightActiveboolean是否允许右键选中节点,默认true允许
disableClipboardboolean是否禁止系统剪切板,默认false不禁止
drawingLineLengthnumber画线过程中允许的最大长度,为0表示不限
disableTouchPadScaleboolean是否禁止触控板双指缩放,默认false
domShapesstring[]扩展的dom画笔name,处理dom移动过程中会产生新的dom问题
textRotateboolean文字是否选择,默认true
textFlipboolean文字是否镜像,默认true

上述内容都能通过此api进行更改,后面我们会将该配置内容移植到全局配置功能中。

让我们来看看实际效果。

setOptionsColor 00_00_00-00_00_30.gif

画笔填充色

上面的配置项中,我们发现setOptions并没有为我们提供修改画笔填充色的选项,并且官网在似乎也没有相关内容的介绍(可能后期会加上),像这种,要想修改的内容官方并未直接提供相关API的,我们可以直接在meta2d.store.data.xxx 来进行修改。

原理上来说,所有图元在渲染时都会先从meta2d.store.data.xxx中获取属性,若该对象上没有该属性则会从meta2d.store.options.xxx中获取,而seOptions方法的实现是将用户传入的属性通过Object.assign注册到meta2d.store.options对象上,故更直接的方法是直接修改meta2d.store.data.xxx,但是不推荐,官网提供API的尽量用官方提供的

那么我们的代码就很好写了

{
  title:"画笔填充颜色",
  type:"color",
  prop:"penBackground",//绑定数据源属性
  bindProp:m, // 绑定数据源对象
  event:"change",
  func(value){
    meta2d.store.data.penBackground = value // 更改属性
    meta2d.render()  // 重绘
  }
}

来看看实际效果

penColor 00_00_00-00_00_30.gif

更改画布背景颜色

meta2d提供了直接更改画布背景色的API setBackgroundColor方法,查看文档,该方法接受一个颜色字符串,只需要在回调中调用该方法即可,像下面这样。

{
  title:"背景颜色",
  type:"color",
  prop:"background",
  bindProp:m, // 绑定的属性
  event:"change",
  func(value){
    meta2d.setBackgroundColor(value) // 更改背景颜色
    meta2d.render()
  }
}

看看效果

bgc 00_00_00-00_00_30.gif

其他配置

由于篇幅有限,画布相关的其他配置的实现在这里就不赘述了,实现方法与上面的所讲一样的,只不过要提前阅读官方文档去查看相关内容如何实现,读者也可下载本项目源码自行查看。下面看看整体效果:

mapProps 00_00_00-00_00_30.gif

总结

本章,我们先进行setting组件的框架搭建,讲解了其原理和步骤,在此基础上我们实现了Map组件的开发,同时我们对meta2d的几个API进行了探讨,结合官方文档讲解了他们的使用方法和使用注意事项,在下一章,我们将继续深入mapProps组件的开发,完成其子组件Global(全局配置)组件的开发,我们将重点讲解如何控制meta2d引擎的相关设置,下篇内容很简单,下次再会~

Meta2d.js 开源地址

给大家推荐一下 Meta2d.js是一个实时数据响应和交互的2d引擎,可用于Web组态,物联网,数字孪生等场景。

Github:https://github.com/le5le-com/meta2d.js

Gitee: https://gitee.com/le5le/meta2d.js

如果本篇文章帮助到了你,欢迎为meta2d项目star点星。

其他配置

由于篇幅有限,画布相关的其他配置的实现在这里就不赘述了,实现方法与上面的所讲一样的,只不过要提前阅读官方文档去查看相关内容如何实现,读者也可下载本项目源码自行查看。下面看看整体效果:

[外链图片转存中…(img-YJfj4mVR-1698388036826)]

总结

本章,我们先进行setting组件的框架搭建,讲解了其原理和步骤,在此基础上我们实现了Map组件的开发,同时我们对meta2d的几个API进行了探讨,结合官方文档讲解了他们的使用方法和使用注意事项,在下一章,我们将继续深入mapProps组件的开发,完成其子组件Global(全局配置)组件的开发,我们将重点讲解如何控制meta2d引擎的相关设置,下篇内容很简单,下次再会~

Meta2d.js 开源地址

给大家推荐一下 Meta2d.js是一个实时数据响应和交互的2d引擎,可用于Web组态,物联网,数字孪生等场景。

Github:https://github.com/le5le-com/meta2d.js

Gitee: https://gitee.com/le5le/meta2d.js

如果本篇文章帮助到了你,欢迎为meta2d项目star点星。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;