数据可视化大屏设计器开发-右键菜单操作

1,545 阅读10分钟

数据可视化大屏设计器开发-右键菜单操作

开头

本文是数据可视化开始的开发细节第三章。关于大屏中组件的一些操作,比如成组、复制粘贴、层级控制、复制样式、组件切换、前进后退等等。
本文就针对各个操作逐一讲解其实现思路。

开始之前

在正式开始之前,先预先介绍一下相关的信息。

以下讲解均只涉及PC端的逻辑。

下面所说的元素表示的是组或者组件的简称。

因为本文说的是右键菜单,所以肯定无法避免二次操作。比如先进行复制,然后进行粘贴,所以一般认为二次操作所选中的元素为最终可能需要操作的位置(比如复制了组内的元素,然后右键粘贴选择的元素在组外,那么目标元素就会被粘贴到组外)。

组件整体的数据结构。

组件在画布中的存在形式是一个的结构,其实就是一个简单的数组形式,当然并不只是一个一层,它是一个深层嵌套的结构。
类似于下面这样:

  type Components = {
    // 组件id
    id: string
    // 父级的组件id 
    parent?: string 
    // 元素标识 组或者组件
    type: 'component' | 'group'
    // 子元素
    components: Components[]
    // ...another config 
  }[]
  • id 元素的id
  • parent
    父级的id,如果为最上层的元素则不存在
  • type
    标记当前元素是组还是组件
  • components
    当前组的下层元素

组可以嵌套组件,也可以嵌套组,组件为最下层。
元素通过parent来达到与父级的关联。
如此形成一个无限向下嵌套的结构。

平滑结构

虽然上述的结构可以很好的描述和直观的看到整个大屏的全貌,但是有时候操作起来相对来说还是有点不方便。
此时就需要一个临时的平滑的单层次结构来应对业务中的一些逻辑操作。
当前的想法是以对象的形式来表达。
类似于下面这样:

  type ComponentPath = {
    [key: string]: {
      id: string;
      path: string;
      // ...another info 
    }
  }
  • 键名
    key即为元素的id
  • id
    与键名同意
  • path
    这个是此结构的关键,它是当前元素在结构中的路径,关于他的格式可以参考lodash的get方法
    比如0.components.2表示获取第一个组的第二个子元素。

开始

首先讲一下本文所涉及的所有操作: 成组/取消成组、复制/粘贴、层级控制(置顶/置底/上一个/下一个)、删除、组件切换、复制组件样式、隐藏、锁定、恢复默认配置、前进后退。

其实本来对于上述的操作,实现逻辑并不复杂,但是因为设计器当中存在多选操作的逻辑,以及成组的逻辑,导致上面的操作实现起来并没有看上去那么简单。

成组/取消成组

一上来就是最复杂的逻辑🤷🏻‍♀️ 。

先讲成组是因为它对后面的逻辑或多或少会产生一定的影响。
并且成组是所有操作当中,最难实现的部分。

即是将一个或多个元素进行包裹,组内元素的定位将不再基于画布,而是基于组。
因为多个元素成组后,免不了对组进行大小位置调整等。
位置调整相对来说影响不大,因为内部元素均基于组来进行定位。
大小的变动会对内部的元素影响非常大。

比如,初始组的宽度为200px,组内的一个组件的宽度为50px,之后将组调整为400px,那么组件的宽度应该调整为多少?
组的宽度放大了2倍,所以组件的宽度同时也放大2倍,变为100px
同时还要注意,组件的位置同样也需要调整,如上述的,说明组件的left也要放大2倍

如此进行上述的计算调整,相对来说还是比较耗费性能的,因为他不仅仅是只有一层的结构。

最终选择的方案是,通过对每一个元素无差别带上scale配置:scaleXscaleY,默认为1
对于组件的操作,无须修改scale配置,而组的大小调整时,修改尺寸和scale配置即可。
const newScaleX = (newWidth / width) * (scaleX || 1),如此可以计算出新的scaleX

而内部的元素在实际渲染前,只需要将父级的scale进行计算即可。
const componentWidth = width * parentScaleX 而组内组的话,则是需要将自己的scale与父级的scale进行相乘,因为组内组之下还有组件,是相互影响的。

成组

成组的元素有两种情况:同级成组和非同级成组。

首先需要计算出对应的元素相对于画布的位置和尺寸。(上面已经说过,组内元素是根据父级组来进行定位的,尺寸也受到了scale配置的影响)
如果是组的话也要将scale配置计算出来。

接着获取到对应的成组位置的父级元素的信息(可以通过上面说的parent配置,用平滑结构获取到元素配置)。

创建新组并根据子元素信息重新计算宽高及位置。

接着递归去修改子组内的子元素(因为子组在树内的结构可能发生了变化),最重要的是子元素的parent配置,需要指向当前的父元素。

取消成组

成组的反向操作。

相对于成组来说,简单一些,只需要去掉外层包裹的组的一些配置,比如scale配置,定位配置,parent配置。
因为现在是相对于原父组的再上一层父组进行定位,所以要将原父组的配置合并到对应的子元素上面。

其他

具体的代码请查看这里
当前版本还发现一个问题:组内元素的宽高通过右侧配置进行修改,外层组的尺寸未同步。预计在下一个版本进行处理。

层级

层级控制包含四个操作:置顶、置底、上一个、下一个。
层级是指元素在画布当中显示的层级顺序。
虽然PC端H5端显示的逻辑不一样,但是也大差不差。

PC端中画布的元素,采用的是绝对定位的布局,那么层级即是元素的zIndex样式
H5端中画布的元素,采用普通的流式布局,那么层级就是元素在数组中的索引顺序。

层级分成3种:1(置底)、2(正常)、3(置顶)。
元素在html的结构中也是按顺序进行渲染的,所以第一个元素和第二个元素虽然zIndex都是2,但是第二个元素还是会在第一个元素的上面。

第一个元素
第二个元素

所以逻辑就很简单了:

  • 置顶
    将元素的zIndex修改为3。
  • 置底
    将元素的zIndex修改1。
  • 上一个/下一个
    修改元素在数组中的索引位置。

因为组本身也是一个容器,所以对于此部分的逻辑,基本处理都是一样的。

当多选了多个元素时,为了操作方便,统一显示的都是一个状态。
比如置顶,如果一个元素已经置顶了,那么操作按钮显示的是取消置顶,根据上面的逻辑,多个元素同时操作时,操作按钮统一显示成置顶

删除

删除操作就很简单了,单纯的从数据结构中,删除对应的元素即可。

而在大屏中,需要弹出确认提示:是否删除xxx、xxx等组件。如果从结构中寻找,难免显得麻烦。
此时就可以使用上面的平滑结构来进行操作,只需要简单的拿到对应元素的path路径,接着拿到他的父级,删除即可👍 。

隐藏

隐藏操作即是将组件在画布当中隐藏。
此操作也十分简单,设置组件的样式即可,这里使用的是visibility样式来控制,实践中因为display可能会使部分组件的宽高计算出现问题,故使用visibility

需要注意的是, 因为visibility的特性,可能会使得鼠标多选时还是能选中该元素,所以需要特别处理。

锁定

锁定操作与隐藏操作类似,不同的是锁定是正常显示元素的,但是无法对其进行相应的操作罢了,比如选中、修改配置等。
这是为了防止有人误操作,导致相关已经完成设计的元素被修改,因为整体数据是实时保存的。

组件切换

组件切换为根据数据进行组件切换。
比如当在设计完成一个组件时,突然想换成另外一种呈现形式,此时就可以通过组件切换达到目的。
当然要求是被切换的组件和切换组件的数据格式是相同的。

关于数据格式,即对应组件所需要的数据的类型,及字段。
比如柱形图,他的数据格式为:

  type BarData = {
    x: string 
    y: number 
    s?: string 
  }[]

折线图的数据格式同样也是上面的形式。
所以说明柱形图和折线图是可以进行相互切换的。

组件切换操作.jpg

复制组件样式

复制组件样式类似于复制操作,当然他是把复制和粘贴操作进行了合并。
和名称一样,复制的是组件的样式配置,对于组件的数据交互等,均为组件的初始值。

逻辑即简单使用lodashmergeWith将组件配置和默认配置进行合并即可。

恢复默认配置

恢复默认配置,将组件的配置恢复成初始值,简单覆盖即可。

前进后退

前进和后退,允许操作可以前进和后退。
比如将一个组件右移100px,后退操作可以将组件回到原来的位置。

这里使用的是自己实现的类库react-undo-component
他可以让class组件或者function组件,在内部记录state的一系列操作记录,方便进行前进和后退。
比如function组件,可以使用useUndo方法。

  import React from 'react'
  import { useUndo } from 'react-undo-component'

  const UndoComponent = () => {

    const [ counter, setCounter, {
      undo
    } ] = useUndo<number>(0)

    const handleAdd = () => {
      setCounter(prev => prev + 1)
    }

    const handleUndo = () => {
      undo()
    }

    return (
        <div>
          <div>counter: {counter}</div>
          <button onClick={handleAdd}>+1</button>
          <button onClick={handleUndo}>undo</button>
        </div>
      )
  }

内部即是简单拦截了useState方法来记录state变化。
因为设计器使用的是dva,所以不能简单使用上述的方法,但是他同样导出了内部class的Histroy可以自行实现对应细节。
具体的实现代码可以查看这里

结束

以上逻辑均为本人自己的想法,如有问题或错误可指正🙏🏻 。

结束🔚。

顺便在下面附上相关的链接。

试用地址
试用账号
操作文档
代码地址