可视化拖拽组件库一些技术要点原理分析(三)(一)

简介: 可视化拖拽组件库一些技术要点原理分析(三)

本文是可视化拖拽系列的第三篇,之前的两篇文章一共对 17 个功能点的技术原理进行了分析:

  1. 编辑器
  2. 自定义组件
  3. 拖拽
  4. 删除组件、调整图层层级
  5. 放大缩小
  6. 撤消、重做
  7. 组件属性设置
  8. 吸附
  9. 预览、保存代码
  10. 绑定事件
  11. 绑定动画
  12. 导入 PSD
  13. 手机模式
  14. 拖拽旋转
  15. 复制粘贴剪切
  16. 数据交互
  17. 发布

本文在此基础上,将对以下几个功能点的技术原理进行分析:

  1. 多个组件的组合和拆分
  2. 文本组件
  3. 矩形组件
  4. 锁定组件
  5. 快捷键
  6. 网格线
  7. 编辑器快照的另一种实现方式

如果你对我之前的两篇文章不是很了解,建议先把这两篇文章看一遍,再来阅读此文:

虽然我这个可视化拖拽组件库只是一个 DEMO,但对比了一下市面上的一些现成产品(例如 processon墨刀),就基础功能来说,我这个 DEMO 实现了绝大部分的功能。

如果你对于低代码平台有兴趣,但又不了解的话。强烈建议将我的三篇文章结合项目源码一起阅读,相信对你的收获绝对不小。另附上项目、在线 DEMO 地址:

18. 多个组件的组合和拆分

组合和拆分的技术点相对来说比较多,共有以下 4 个:

  • 选中区域
  • 组合后的移动、旋转
  • 组合后的放大缩小
  • 拆分后子组件样式的恢复

选中区域

在将多个组件组合之前,需要先选中它们。利用鼠标事件可以很方便的将选中区域展示出来:

  1. mousedown 记录起点坐标
  2. mousemove 将当前坐标和起点坐标进行计算得出移动区域
  3. 如果按下鼠标后往左上方移动,类似于这种操作则需要将当前坐标设为起点坐标,再计算出移动区域
// 获取编辑器的位移信息
const rectInfo = this.editor.getBoundingClientRect()
this.editorX = rectInfo.x
this.editorY = rectInfo.y
const startX = e.clientX
const startY = e.clientY
this.start.x = startX - this.editorX
this.start.y = startY - this.editorY
// 展示选中区域
this.isShowArea = true
const move = (moveEvent) => {
    this.width = Math.abs(moveEvent.clientX - startX)
    this.height = Math.abs(moveEvent.clientY - startY)
    if (moveEvent.clientX < startX) {
        this.start.x = moveEvent.clientX - this.editorX
    }
    if (moveEvent.clientY < startY) {
        this.start.y = moveEvent.clientY - this.editorY
    }
}

mouseup 事件触发时,需要对选中区域内的所有组件的位移大小信息进行计算,得出一个能包含区域内所有组件的最小区域。这个效果如下图所示:

这个计算过程的代码:

createGroup() {
  // 获取选中区域的组件数据
  const areaData = this.getSelectArea()
  if (areaData.length <= 1) {
      this.hideArea()
      return
  }
  // 根据选中区域和区域中每个组件的位移信息来创建 Group 组件
  // 要遍历选择区域的每个组件,获取它们的 left top right bottom 信息来进行比较
  let top = Infinity, left = Infinity
  let right = -Infinity, bottom = -Infinity
  areaData.forEach(component => {
      let style = {}
      if (component.component == 'Group') {
          component.propValue.forEach(item => {
              const rectInfo = $(`#component${item.id}`).getBoundingClientRect()
              style.left = rectInfo.left - this.editorX
              style.top = rectInfo.top - this.editorY
              style.right = rectInfo.right - this.editorX
              style.bottom = rectInfo.bottom - this.editorY
              if (style.left < left) left = style.left
              if (style.top < top) top = style.top
              if (style.right > right) right = style.right
              if (style.bottom > bottom) bottom = style.bottom
          })
      } else {
          style = getComponentRotatedStyle(component.style)
      }
      if (style.left < left) left = style.left
      if (style.top < top) top = style.top
      if (style.right > right) right = style.right
      if (style.bottom > bottom) bottom = style.bottom
  })
  this.start.x = left
  this.start.y = top
  this.width = right - left
  this.height = bottom - top
  // 设置选中区域位移大小信息和区域内的组件数据
  this.$store.commit('setAreaData', {
      style: {
          left,
          top,
          width: this.width,
          height: this.height,
      },
      components: areaData,
  })
},
getSelectArea() {
    const result = []
    // 区域起点坐标
    const { x, y } = this.start
    // 计算所有的组件数据,判断是否在选中区域内
    this.componentData.forEach(component => {
        if (component.isLock) return
        const { left, top, width, height } = component.style
        if (x <= left && y <= top && (left + width <= x + this.width) && (top + height <= y + this.height)) {
            result.push(component)
        }
    })
    // 返回在选中区域内的所有组件
    return result
}

简单描述一下这段代码的处理逻辑:

  1. 利用 getBoundingClientRect() 浏览器 API 获取每个组件相对于浏览器视口四个方向上的信息,也就是 lefttoprightbottom
  2. 对比每个组件的这四个信息,取得选中区域的最左、最上、最右、最下四个方向的数值,从而得出一个能包含区域内所有组件的最小区域。
  3. 如果选中区域内已经有一个 Group 组合组件,则需要对它里面的子组件进行计算,而不是对组合组件进行计算。

组合后的移动、旋转

为了方便将多个组件一起进行移动、旋转、放大缩小等操作,我新创建了一个 Group 组合组件:

<template>
    <div class="group">
        <div>
             <template v-for="item in propValue">
                <component
                    class="component"
                    :is="item.component"
                    :style="item.groupStyle"
                    :propValue="item.propValue"
                    :key="item.id"
                    :id="'component' + item.id"
                    :element="item"
                />
            </template>
        </div>
    </div>
</template>
<script>
import { getStyle } from '@/utils/style'
export default {
    props: {
        propValue: {
            type: Array,
            default: () => [],
        },
        element: {
            type: Object,
        },
    },
    created() {
        const parentStyle = this.element.style
        this.propValue.forEach(component => {
            // component.groupStyle 的 top left 是相对于 group 组件的位置
            // 如果已存在 component.groupStyle,说明已经计算过一次了。不需要再次计算
            if (!Object.keys(component.groupStyle).length) {
                const style = { ...component.style }
                component.groupStyle = getStyle(style)
                component.groupStyle.left = this.toPercent((style.left - parentStyle.left) / parentStyle.width)
                component.groupStyle.top = this.toPercent((style.top - parentStyle.top) / parentStyle.height)
                component.groupStyle.width = this.toPercent(style.width / parentStyle.width)
                component.groupStyle.height = this.toPercent(style.height / parentStyle.height)
            }
        })
    },
    methods: {
        toPercent(val) {
            return val * 100 + '%'
        },
    },
}
</script>
<style lang="scss" scoped>
.group {
    & > div {
        position: relative;
        width: 100%;
        height: 100%;
        .component {
            position: absolute;
        }
    }
}
</style>

Group 组件的作用就是将区域内的组件放到它下面,成为子组件。并且在创建 Group 组件时,获取每个子组件在 Group 组件内的相对位移和相对大小:

created() {
    const parentStyle = this.element.style
    this.propValue.forEach(component => {
        // component.groupStyle 的 top left 是相对于 group 组件的位置
        // 如果已存在 component.groupStyle,说明已经计算过一次了。不需要再次计算
        if (!Object.keys(component.groupStyle).length) {
            const style = { ...component.style }
            component.groupStyle = getStyle(style)
            component.groupStyle.left = this.toPercent((style.left - parentStyle.left) / parentStyle.width)
            component.groupStyle.top = this.toPercent((style.top - parentStyle.top) / parentStyle.height)
            component.groupStyle.width = this.toPercent(style.width / parentStyle.width)
            component.groupStyle.height = this.toPercent(style.height / parentStyle.height)
        }
    })
},
methods: {
        toPercent(val) {
            return val * 100 + '%'
        },
    },

也就是将子组件的 lefttopwidthheight 等属性转成以 % 结尾的相对数值。

为什么不使用绝对数值

如果使用绝对数值,那么在移动 Group 组件时,除了对 Group 组件的属性进行计算外,还需要对它的每个子组件进行计算。并且 Group 包含子组件太多的话,在进行移动、放大缩小时,计算量会非常大,有可能会造成页面卡顿。如果改成相对数值,则只需要在 Group 创建时计算一次。然后在 Group 组件进行移动、旋转时也不用管 Group 的子组件,只对它自己计算即可。

组合后的放大缩小

组合后的放大缩小是个大问题,主要是因为有旋转角度的存在。首先来看一下各个子组件没旋转时的放大缩小:

从动图可以看出,效果非常完美。各个子组件的大小是跟随 Group 组件的大小而改变的。

现在试着给子组件加上旋转角度,再看一下效果:

为什么会出现这个问题

主要是因为一个组件无论旋不旋转,它的 topleft 属性都是不变的。这样就会有一个问题,虽然实际上组件的 topleftwidthheight 属性没有变化。但在外观上却发生了变化。下面是两个同样的组件:一个没旋转,一个旋转了 45 度。

可以看出来旋转后按钮的 topleftwidthheight 属性和我们从外观上看到的是不一样的。

接下来再看一个具体的示例:

上面是一个 Group 组件,它左边的子组件属性为:

transform: rotate(-75.1967deg);
width: 51.2267%;
height: 32.2679%;
top: 33.8661%;
left: -10.6496%;

可以看到 width 的值为 51.2267%,但从外观上来看,这个子组件最多占 Group 组件宽度的三分之一。所以这就是放大缩小不正常的问题所在。

目录
相关文章
|
2月前
|
JavaScript
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
其实这个从零有点歧义,因为本质上是要基于`tdesign-vue-next`来进行二次封装为一套广告UI组件库,现在让我们在一起快乐的搭建自己的广告UI库之前,先对以下内容做出共识:
80 0
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
|
3月前
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
61 0
|
6月前
|
SQL 前端开发 JavaScript
kettle开发-超好用自定义数据处理组件
kettle开发-超好用自定义数据处理组件
190 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(二)(上)
可视化拖拽组件库一些技术要点原理分析(二)
101 1
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(二)
可视化拖拽组件库一些技术要点原理分析(三)(二)
102 0
|
数据可视化 索引
可视化拖拽组件库一些技术要点原理分析(二)
可视化拖拽组件库一些技术要点原理分析(二)
148 0
|
6月前
|
API
【鸿蒙软件开发】ArkTS基础组件之Gauge(环形图表)、LoadingProgress(动态加载)
【鸿蒙软件开发】ArkTS基础组件之Gauge(环形图表)、LoadingProgress(动态加载)
378 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(上)
可视化拖拽组件库一些技术要点原理分析(四)
94 0
可视化拖拽组件库一些技术要点原理分析(四)(上)
|
数据可视化 API 索引
可视化拖拽组件库一些技术要点原理分析(三)(三)
可视化拖拽组件库一些技术要点原理分析(三)(三)
137 0
|
JSON 数据可视化 前端开发
可视化拖拽组件库一些技术要点原理分析(二)(下)
可视化拖拽组件库一些技术要点原理分析(二)(下)
108 0