使用 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的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



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

相关内容

热门资讯

电视安卓系统哪个品牌好,哪家品... 你有没有想过,家里的电视是不是该升级换代了呢?现在市面上电视品牌琳琅满目,各种操作系统也是让人眼花缭...
安卓会员管理系统怎么用,提升服... 你有没有想过,手机里那些你爱不释手的APP,背后其实有个强大的会员管理系统在默默支持呢?没错,就是那...
安卓系统软件使用技巧,解锁软件... 你有没有发现,用安卓手机的时候,总有一些小技巧能让你玩得更溜?别小看了这些小细节,它们可是能让你的手...
安卓系统提示音替换 你知道吗?手机里那个时不时响起的提示音,有时候真的能让人心情大好,有时候又让人抓狂不已。今天,就让我...
安卓开机不了系统更新 手机突然开不了机,系统更新还卡在那里,这可真是让人头疼的问题啊!你是不是也遇到了这种情况?别急,今天...
安卓系统中微信视频,安卓系统下... 你有没有发现,现在用手机聊天,视频通话简直成了标配!尤其是咱们安卓系统的小伙伴们,微信视频功能更是用...
安卓系统是服务器,服务器端的智... 你知道吗?在科技的世界里,安卓系统可是个超级明星呢!它不仅仅是个手机操作系统,竟然还能成为服务器的得...
pc电脑安卓系统下载软件,轻松... 你有没有想过,你的PC电脑上安装了安卓系统,是不是瞬间觉得世界都大不一样了呢?没错,就是那种“一机在...
电影院购票系统安卓,便捷观影新... 你有没有想过,在繁忙的生活中,一部好电影就像是一剂强心针,能瞬间让你放松心情?而我今天要和你分享的,...
安卓系统可以写程序? 你有没有想过,安卓系统竟然也能写程序呢?没错,你没听错!这个我们日常使用的智能手机操作系统,竟然有着...
安卓系统架构书籍推荐,权威书籍... 你有没有想过,想要深入了解安卓系统架构,却不知道从何下手?别急,今天我就要给你推荐几本超级实用的书籍...
安卓系统看到的炸弹,技术解析与... 安卓系统看到的炸弹——揭秘手机中的隐形威胁在数字化时代,智能手机已经成为我们生活中不可或缺的一部分。...
鸿蒙系统有安卓文件,畅享多平台... 你知道吗?最近在科技圈里,有个大新闻可是闹得沸沸扬扬的,那就是鸿蒙系统竟然有了安卓文件!是不是觉得有...
宝马安卓车机系统切换,驾驭未来... 你有没有发现,现在的汽车越来越智能了?尤其是那些豪华品牌,比如宝马,它们的内饰里那个大屏幕,简直就像...
p30退回安卓系统 你有没有听说最近P30的用户们都在忙活一件大事?没错,就是他们的手机要退回安卓系统啦!这可不是一个简...
oppoa57安卓原生系统,原... 你有没有发现,最近OPPO A57这款手机在安卓原生系统上的表现真是让人眼前一亮呢?今天,就让我带你...
安卓系统输入法联想,安卓系统输... 你有没有发现,手机上的输入法真的是个神奇的小助手呢?尤其是安卓系统的输入法,简直就是智能生活的点睛之...
怎么进入安卓刷机系统,安卓刷机... 亲爱的手机控们,你是否曾对安卓手机的刷机系统充满好奇?想要解锁手机潜能,体验全新的系统魅力?别急,今...
安卓系统程序有病毒 你知道吗?在这个数字化时代,手机已经成了我们生活中不可或缺的好伙伴。但是,你知道吗?即使是安卓系统,...
奥迪中控安卓系统下载,畅享智能... 你有没有发现,现在汽车的中控系统越来越智能了?尤其是奥迪这种豪华品牌,他们的中控系统简直就是科技与艺...