使用 Next.js、 Prisma 和 PostgreSQL 全栈开发视频网站
admin
2024-01-29 02:12:03
0

通过上一篇文章,我们对乔巴乐高海报平台的整体架构有了初步的了解。今天我们深入到编辑器部分,对其中的难点和实现细节进行分析。

这是目前生产的编辑器页面:

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

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

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

  • 尺寸属性(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:{ 
// 所有添加到画布中的组件数据 
componentData:[], 
} reducers:{ 
// 添加组件到componentData 
addComponentData(){}, 
// 编辑组件,更新componentData及curComponent 
editComponentData(){}, 
// 删除组件 
delComponentData(){} 
} 

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

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

这里组件列表底层渲染也是用的组件库,只是不同模板的props不同。

选中组件展示其关联属性

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

const editorModule = {state: {componentData: [],currentElement: "",},mutations: { addComponentData(state, component) { }, setActive(state, id) {state.currentElement = id; },},getters: {getCurrentElement: (state) => {return state.componentData.find((component) => component.id === state.currentElement);},}
} 

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

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

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

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

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

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

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

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

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

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

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

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

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

字符串(String

渲染器类型组件
input
textarea

数字(Number)

渲染器类型组件
input-number
slider

布尔(Boolean)

渲染器类型组件
switch

日期(Date

渲染器类型组件
date

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

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

渲染器类型组件
array

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

至于函数(Function),可以采用预定义的形式:

渲染器类型组件
function

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

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

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

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

渲染器类型组件
upload
color-picker

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

编辑属性,画布同步更新

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

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

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

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

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

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

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

  • initialValueConvert
  • eventChangeValueConvert

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

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

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

const mapPropsToComponents = {text: {component: "textarea-fix",eventName: "change",valueProp: "value",initialValueConvert: (v: any) => v,eventChangeValueConvert: (e: any) => e.target.value,text: "文本",},width: {component: "a-input-number",eventName: "change",valueProp: "value",initialValueConvert: (v: string) => (v ? parseInt(v) : ""),eventChangeValueConvert: (e: number) => (e ? `${e}px` : ""),text: "宽度",},textDecoration: {component: "icon-switch",eventName: "change",initialValueConvert: (v: string) => v === "underline",eventChangeValueConvert: (e: boolean) => (e ? "underline" : "none"),valueProp: "checked",},backgroundSize: {component: "a-select",eventName: "change",initialValueConvert: (v: any) => v,eventChangeValueConvert: (e: any) => e,subComponent: "a-select-option",text: "背景大小",options: [{ value: "contain", text: "自动缩放" },{ value: "cover", text: "自动填充" },{ value: "", 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。这个功能是很常见的,他可以极大的提升用户体验,提高编辑效率,但是用代码应该如何实现呢?

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...