总结低代码海报平台编辑器难点
创始人
2025-05-28 21:48:12

上篇总结了基础的架构,本篇总结下细节方面的难点。

 这是目前编辑器页面的原型图。

不难看出和市面上大部分低代码平台一样,由三部分组成:左侧组件列表、中间画布区域、右侧属性区域。

大致操作流程就是拖动左侧的组件到中间的画布,选中组件,右侧属性面板就会展示与该组件关联的属性。编辑右侧属性,画布中对应的组件样式就会同步更新。页面拼接完成。

从中看出组件串联其中,在前面一篇文章中,我们大致分析了整体页面和组件的数据结构,但没有细化。抽取一下文字、图片、素材组件的通用特性:

  • 尺寸属性(Size)
    • 宽度(width)
    • 高度(height)
  • 填充属性(Padding)
    • 上填充(padding-top)
    • 右填充(padding-right)
    • 下填充(padding-bottom)
    • 左填充(padding-left)
  • 视觉格式属性
    • 指定如何定位元素(position)
    • 指定所定位元素的上边缘的位置(top)
    • 指定所定位元素的右边缘的位置(right)
    • 指定所定位元素底边的位置(bottom)
    • 指定定位元素左边缘的位置(left)
    • 将一个或多个阴影应用于元素的框(box-shadow)
  • 颜色属性(Color)
    • 透明度(opacity)
  • 边框属性(Border)
    • 设置元素所有四个侧面的边框颜色(border-color)
    • 设置元素所有四个侧面的边框宽度(border-width)
    • 在元素的所有四个面上设置边框的样式(border-style)
    • 定义元素边界角的形状(border-radius)

除此之外,文字组件还具有以下属性:

  • 字体属性(Fonts)
    • 定义元素的字体列表(font-family)
    • 定义文本的字体大小(font-size)
    • 定义文本的字体样式(font-style)
    • 指定文本的字体粗细(font-weight)
  • 文字属性(Text)
    • 设置内联内容的水平对齐方式(text-align)
    • 指定添加到文本的装饰(text-decoration)
    • 设置文本行之间的高度(line-height)

图片组件还具有:

  • 图片属性(Image)
    • 图片链接(src)

素材组件还具有:

  • 背景属性(Background)
    • 定义元素的背景色(background-color)

我们将上面的操作流程拆解为三步:

  1. 拖动左侧的组件到中间的画布
  2. 选中组件,右侧属性面板就会展示与该组件关联的属性
  3. 编辑右侧属性,画布中对应的组件样式就会同步更新

添加组件到画布

编辑器整体的数据结构是这么设计的

state:{ 
// 所有添加到画布中的组件数据 
components: [],
} mutations:{ 
// 添加组件到components 
addComponent(){}, 
// 编辑组件,更新components
updateComponent(){}, 
// 删除组件 
deleteComponent(){} 
}

那么从左侧组件列表添加组件到画布的操作其实就是向componentDatapush一条组件数据。

这里主要是关注下组件列表要怎么设计:为了便于用户快速创建活动,组件列表最好是预设一些模板,其实就是针对文字、图片和素材分别提供一些已有的元素。这样当对应组件点击添加到画布时,对应就会commit一个mutation来修改store中的componentData

选中组件展示其关联属性

当在画布中选中具体组件时,我们需要知道此刻是哪个组件被选中了,意味着需要一个变量来存储当前高亮的组件id。那么在store中添加setActivegetCurrentElement

const editorModule = {state: {componentData: [],currentElementId: '',},mutations: {setActive (state, currentId) {state.currentElementId = currentId},},getters: {getCurrentElement: (state) => {return state.components.find((component) => component.id === state.currentElementId)},}
}

当在画布中选中组件时,就会触发setActive,更新currentElementId。(通过getCurrentElement可以获取到当前正在被操作的组件)。

这个时候,怎么在右侧属性区域动态展示不同组件的不同属性呢?

对于单独的组件来说,属性面板应该是语义化的,无论是开发还是非开发同学,通过属性面板的操作区,就可以直观的知道一个组件的属性是什么,应该如何使用和编辑。

那么属性面板应该包含哪些内容呢?

1、label:属性名称。这个可以显式的告诉具体的属性的作用,比如元素的宽高、边框、背景颜色等。

2、description:属性的描述信息。对于一些特殊属性,可能第一下通过label并不能直观的识别属性的含义,添加描述信息可以进行详细的阐述。

3、content:属性渲染器。用户可以基于此实现对属性的修改。最常见的有 textarea、input、select 等。

4、error:属性校验信息。当用户输入了不合法的或者类型不匹配时,可给予适当的错误提示信息。

通过以上描述,我们会发现,这其实就是我们常用的表单。

对应上面组件的props信息,我们可以对这些属性做一些归类,那归类的标准又是什么呢?我认为应该把属性与js中的数据类型做一下映射,然后在具体的分类下选用合适的渲染器。

我们知道在JavaScript中,一共有七种数据类型,字符串(String)、数字(Number)、空(Null)、未定义(Undefined)、Symbol和对象(Object)。其中对象类型包括:数组(Array)、函数(Function)、还有两个特殊的对象:正则(RegExp)和日期(Date)。

这里面的空(Null)、未定义(Undefined)、Symbol和正则(RegExp)在渲染器中基本用不到。

我们先来看一下字符串(String)、数字(Number)和日期(Date)可能渲染的方式:

字符串(String 

渲染器类型组件

input和textarea

数字(Number)

渲染器类型组件
input-number
slider

日期(Date

渲染器类型组件
date

除了这几种,还有对象(Object)、数组(Array)、函数(Function)。

对象和数组属于较复杂的类型,不过我们可以把它抽象为多层级(可以理解为嵌套)的基础数据类型:

渲染器类型组件
array

像数组一般是用下拉框的形式来展现。

到这里,不难想到,我们要维护一个属性和表单组件的对应关系。属性对应上面的key,像borderColortextwidthfontFamily这些,那组件呢?组件其实就是对属性的具体呈现,像width可以用数字输入框、text可以用普通输入框,但是对于一些比较复杂的特性,我们自己去实现这些组件,就显得捉襟见肘了,这个时候我们就可以考虑和现有的组件库做一下结合了(这里我采用的是Ant Design Vue)。

那么这样,属性propcomponent基础的对应关系就有了:

const mapPropsToForms = {text: {component: "a-textarea",},width: {component: "a-input-number",},borderWidth: {component: "a-slider",},// ...
}

但这只是满足了常规的基础组件设计,像一些独有的属性或者基础组件不能满足的情况,我们需要对其做一定扩展:

渲染器类型组件
upload
color-picker

上面提到的上传组件和颜色选择组件是需要我们单独去实现的。 

编辑属性,画布同步更新

上面只是初步建立了属性和组件的对应关系,组件初始值的展示、复杂组件的展示以及表单值更新后,画布如何同步更新,这些问题我们还都没有解决。

其实把问题简化,这就是表单的回显和更新问题。

以我以往的经验来看:表单组件在设计时,有两点是必须的:

  • 表单初始值(默认value),供初始展示使用
  • 表单属性更改的事件(默认为 change

对于不同的表单,初始值和属性更改后,参数的处理是不一样的:

  • 像高度、宽度这种数字类型的,传入表单时应保证是number(24)类型,属性更改后,事件参数应该是string(24px)类型的
  • 字体加粗与否、倾斜与否、加下划线与否,传入表单时应保证是boolean(true/false)类型,属性更改后,事件参数应该是string(bold/normal)类型的

所以给每一个属性在传入表单和事件更改后都要加一个额外的转化函数去处理值:

  • intialTransform
  • afterTransform

还有对属性进行赋值时,不是所有的表单控件接收的都是value,像checkbox就是checked,这种单独抽一个属性valueProp去控制即可。

其次,像上面提到的复杂组件(我们这里是指父子层级)的渲染,除了component还要多加一个subComponent

完善后,属性propcomponent的对应关系为:

const mapPropsToForms = {text: {text: '文本',component: 'a-textarea',extraProps: { rows: 3 },afterTransform: (e: any) => e.target.value},width: {text: '宽度',component: 'a-input-number',initalTransform: (v: string) => v ? parseInt(v): 0,afterTransform: (e: number) => e ? `${e}px` : ''},textDecoration: {component: 'icon-switch',initalTransform: (v: string) => v === 'underline',afterTransform: (e: boolean) => e ? 'underline' : 'none',valueProp: 'checked',extraProps: { iconName: 'UnderlineOutlined', tip: '下划线' }},borderStyle: {component: 'a-input',eventName: 'change',valueProp: 'value',intialTransform: (v: any) => v,afterTransform: (e: any) => e,component: 'a-select',subComponent: 'a-select-option',text: '边框类型',options: [{ value: 'none', text: '无' },{ value: 'solid', text: '实线' },{ value: 'dashed', text: '破折线' },{ value: 'dotted', text: '点状线' }]},// ...
}

我们的数据始终保持自上而下的顺序,也就是说表单更新最终要反射回到总体的 store 当中去。这个时候我们在对应的组件当中发射出一个事件(change),当 change 发生的时候,我们能够知道是哪个元素的哪个属性,以及新的值是什么,我们就用这些信息更新这个值,这样 store完成更新,元素的 props 发生更新,那么整个数据流动就完成了。

画布区域交互设计实现

上面说了这么多,基本都是围绕左侧组件区域、中间画布区域、右侧属性区域相互之间的数据流动来讲的。最后来说一下画布区域本身一些比较复杂的交互实现。

我大概整理了这几种:

  1. 拖拽(组件在画布中移动)
  2. 组件图层
  3. 放大/缩小
  4. 撤销/重做

拖拽(组件在画布中移动)

这个相对比较简单,就是mousedownmousemovemouseup事件的结合使用:在组件上按下鼠标后,记录组件当前位置,也就是x、y坐标(对应的是css中的left和top);每次鼠标移动时用当前最新的xy坐标减去最开始的xy坐标,计算出移动的距离,然后更新组件位置;鼠标抬起时结束移动。

组件图层

图层面板主要是控制组件的显示/隐藏、不同组件的层级关系以及点击选中。

这里主要说一下层级关系吧,正常情况下,我们会选择使用z-index来控制层级。但是这里我没有使用z-index,而是利用了层叠领域黄金准则的第二条。

层叠领域黄金准则:1、谁大谁上: 当具有明显的层叠水平标示的时候,如识别的z-indx值,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。2、后来居上: 当元素的层叠水平一致、层叠顺序相同的时候,在DOM流中处于后面的元素会覆盖前面的元素。

为什么选择第二个而没有选择最常见的第一条呢?首先,我们需要一个图层列表可以对每个组件对应的图层进行排序,其实就是对store中的components进行排序,也就是数组排序了,那么在图层列表中,如果你想增加某一图层的层级,把它放置到后面就可以了(这样渲染时,数组后面的元素就会处在DOM流的后面了。对应的层叠顺序也就居上了),这样不仅操作方便,也不用增加额外冗余代码,可谓一举两得

放大/缩小

核心实现:在画布组件的四个角(↖️、↗️、↙️、↘️)分别加一个小圆点:

  1. 左上:组件left、top均减小;width、height均变大
  2. 右上:组件left不变、top减小;width、height均变大
  3. 左下:组件left减小、top不变;width、height均变大
  4. 右下:组件left、top均不变;width、height均变大

撤销/重做

撤销、重做其实是我们平时一直在用的操作。对应快捷键一般就是⌘ Z / Ctrl+Z⌘⇧ Z / Ctrl+Shift+Z。这个功能是很常见的,他可以极大的提升用户体验,提高编辑效率。

相关内容

热门资讯

【数据结构】KMP算法细节详解 KMP算法细节详解前言一、字符串匹配问题1.BF算法2.KMP算法二、next数组三、手写nex思想...
【中间间】Redis与MySQ... 文章目录前言谈谈一致性三个经典的缓存模式Cache-Aside PatternCache-Aside...
【CSS】盒子模型内边距 ④ ... 文章目录一、盒子模型内部尺寸计算1、设置内边距和边框对盒子模型的影响2、盒子模型尺寸计算二、代码示例...
TIA博途中添加程序注释的具体... TIA博途中添加程序注释的具体方法示例_汇总 添加程序注释可以帮助自己和阅读程序的技术人员更好地理...
[LsSDK][tool] l... 文章目录一、首先是界面介绍。二、工具的目的三、ls_gpio.h模板四、ls_syscfg.h 模板...
Linux的目录结构 目录 一:重要性和基本介绍 二:目录结构​编辑 ​编辑 2.1 bin...
java中IO流的操作 对于java中io流的一些操作和类进行总结 io流的分类:  字节流:...
HydroD 实用教程(七)静... 目 录一、前言二、稳性分析三、Hydrostatic Rule Checks四、AVCG Analy...
记录使用Dockerfile来... 一准备一个安装了docker的虚拟机 首先准备一个安装好了docker的虚拟机,我的d...
Nginx学习笔记(三)Lin... 目录一、官网下载二、配置基本信息1.上传 Linux2.解压3.安装编译环境4.执行命令4.1 配置...
怎样展示你在项目中的重要性? 今天我们聊聊面试中,怎样介绍你的项目,以及怎样突出你的重要性。面试中除了...
LA-Lib库c++环境下的编... 下载地址一:GitHub仓库 下载地址二:(内含己编译一个...
在 AI 上训练 AI:Cha... ChatGPT 可以像 Linux 终端一样运行,并在给出以下提示时返回执行结果。下面...
import 或者requir... 最近看别人写的源码时发现一个有趣的现象, import {promises as f...
通过sysfs文件系统接口来改... 通过sysfs文件系统接口来改变内核模块中的变量值(二) 文章目录通过...
JavaWeb《一》概念、服务... 🍎道阻且长,行则将至。🍓 本文是javaweb的...
iOS上架App Store详... 上架基本需求资料 1、苹果开发者账号(如还没账号先申请- 苹果开发者账号申请教程&#x...
【LeetCode每日一题】—... 文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目...
代码随想录算法训练营第三十五天... 860.柠檬水找零 题目链接 思路: 情况一:账单是5,直...
VsCoe离线安装插件【步骤+... 文章目录1、下载插件2、安装 svn插件为例2.1、下载插件2.2、安装2.3、查看安装插件 前言...
【C++】13.多态 1.多态的概念 通俗来说,就是多种形态,具体点就是去完成某个行为...
光伏并网逆变器学习1-simu... 分为三个部分:光伏电池 、 boost+mppt 、 并网逆变 光伏电池&#...
WEB前端第三次作业——CSS... WEB前端第三次作业——CSS样式案例 做出如下图中的效果 用到的图片素材均来源于对应网站源代码 ...
Liunx下的进程空间地址理解... 文章目录前言1.进程空间地址1.从编程语言角度理解地址的划分2.进程虚拟地址3.扩展1.为啥要有虚拟...
【尊享版】你真的会休息吗?如何... 超友们,早上好~ 今天我为你带来的分享是《你真的会休息吗?如何开始正确的...
数据更新 | CnOpenDa... 证券从业人员信息数据 一、数据简介   证券从业人员是指被中国证监会依法批准的证券从业机构正式聘用或...
HTML 音频(Audio) HTML 音频(Audio) 声音在HTML中可以以不同的方式播放. 问题以及解决方法 在 HTML...
树莓派/linux/ubunt... 问题描述需要给树莓派设置静态ip并且要可以连接网络。但实际情况是有静态ip时:ping...
C语言指针操作(十)动态内存分... 目录 一、什么是内存的动态分配 二、怎样建立内存的动态分配 2.1用malloc函数开辟动态存储区 ...
测试怎么做到尽可能全和尽可能快 需求 目标 就上面这样一个很简单的功能点,但是他的测试数据产品一共给了6K多条 本...