老板:给你20天,写一个可拖拽动态表单生成器
点击关注公众号,“技术干货”及时达!效果展示:https://www.ixigua.com/7384272739487449600?logTag=4085ff0b0fcd343e7666需求上面的视频就是最终完成的效果,还存在一些瑕疵,比如拖拽样式优化、input组件后缀样式优化等。
首先「表单生成器页面」分为上下两个部分:
上层:标题、首页附页切换、保存功能下层:表单生成器左侧:数据项配置中间:面板显示右侧:表单项设置各自的功能为:
上层:切换首页附页、校验并保存已配置表单,首页附页分开保存下层:左侧部分:模块和子项均支持增删改,模块可折叠和展开,点击子项或模块同步显示到中间面板,也可拖动到中间面板中间部分:显示最终表单效果,组件hover和选中效果,可删除,可拖动调整位置;最终的表单类型只有五种(输入、单选、日期、下拉、表格)右侧部分:整体表单设置和具体的组件设置其次为「表单回显页面」,包含数据回填、必填校验、联动显隐、数据提取并保存功能。
心理活动当开完需求会的时候,我意识到这个需求的复杂程度不低,此时的我有一点慌??;当我询问时间期限的时候,告知我就是这个月底(我是5月6号接到的需求),此时的我感觉慌了??;当我再询问这个需求的人员投入的时候,告知我只有我一个前端来处理这个需求,此时的我,已经准备充boss直聘VIP了??...
开玩笑归开玩笑,该做还得做。其实某些东西是看着大,但很多都是附加功能,只要把核心功能梳理出来,其余的慢慢加就行了。
分析消化这个需求之后,我暂时没有头绪来处理。我知道这时候我要先去寻找类似的产品取取经。最终我找了Vben开源项目(https://vben.vvbin.cn/#/),其中有这个模块实现,如下图:
image.png其中的核心功能是一致的,在思索几番之后,我准备先研究一下开源项目这个模块的实现。以下是研究结果:
拖拽使用VueDraggablePlus实现数据结构总体为一个form对象,对象中包含控制form的属性和一个表示表单项的数组,每一个表单项有唯一ID,表单项中内容为表单项配置属性显示面板渲染逻辑:层级:form - VueDraggable(v-for) - DisplayItem(表单项组件) - Vue component(利用is属性) - 自定义组件逻辑:最外层是form组件,form组件中渲染VueDraggable组件,VueDraggable循环渲染自定义的表单项包装组件DisplayItem,DisplayItem中渲染Vue component组件,利用component的is属性来渲染自定义组件配置面板渲染逻辑:利用Vue component组件的is属性来渲染自定义组件展示组件和配置组件是配对关系拖拽表单项、点击新增表单项、删除表单项本质上是表单项数组内「数据」的顺序变化、新增和删除数据结构是核心,数据结构对应页面结构关键步骤看似比较大的系统,只要保持耐心,先把主干建立起来,然后逐渐丰富,就能够实现,关键步骤如下:
拖拽这里的拖拽,使用vue-draggable-plus插件来实现,有三种使用方式:
组件使用函数使用指令使用在本次案例中使用的是组件方式,案例中从左侧拖拽到中间面板的行为其实是双列表匹配,官方文档中也有示例,基本使用例子如下:
template
divclass="flex"
VueDraggable
class="flexflex-colgap-2p-4w-300pxh-300pxm-autobg-gray-500/5roundedoverflow-auto"
v-model="list1"
animation="150"
ghostClass="ghost"
group="people"
@update="onUpdate"
@add="onAdd"
@remove="remove"
div
v-for="iteminlist1"
:key="item.id"
class="cursor-moveh-30bg-gray-500/5roundedp-3"
{{item.name}}
/div
/VueDraggable
VueDraggable
class="flexflex-colgap-2p-4w-300pxh-300pxm-autobg-gray-500/5roundedoverflow-auto"
v-model="list2"
animation="150"
group="people"
ghostClass="ghost"
@update="onUpdate"
@add="onAdd"
@remove="remove"
div
v-for="iteminlist2"
:key="item.id"
class="cursor-moveh-30bg-gray-500/5roundedp-3"
{{item.name}}
/div
/VueDraggable
/div
divclass="flexjustify-between"
preview-list:list="list1"/
preview-list:list="list2"/
/div
/template
scriptsetup
import{ref}from'vue'
import{VueDraggable}from'vue-draggable-plus'
constlist1=ref([
{
name:'Joao',
id:'1'
},
{
name:'Jean',
id:'2'
},
{
name:'Johanna',
id:'3'
},
{
name:'Juan',
id:'4'
}
])
constlist2=ref(
list1.value.map(item=({
name:`${item.name}-2`,
id:`${item.id}-2`
}))
)
functiononUpdate(){
console.log('update')
}
functiononAdd(){
console.log('add')
}
functionremove(){
console.log('remove')
}
/script
如代码所示,只需保证两个列表的group属性的值一致以及数据结构一致即可;
由于实际需求中,只需要从左侧拖拽到中间,并不需要从中间拖拽回去,所以为了规避这种行为,需要给左侧的group属性设置为以下内容:
//DraggableGroup是定义的字符串
// pull 从列表中移动的能力。克隆——复制项目,而不是移动。
// put 是否可以从其他列表中添加元素,或者可以从中获取元素的组名数组。
:group="{name:DraggableGroup,pull:'clone',put:false}"
更多插件API请点击这里
解决了双向拖拽列表之后,此时出现了一个新的问题,左侧的内容是一个树,不是一个扁平的数组,同样要实现拖拽,如下图:
image.png插件也考虑到了这一点,可以实现嵌套的功能,官网嵌套示例;逻辑就是组件自调用。
image.png数据转换为组件实现了拖拽的需求之后,下一步就是实现从左侧拖拽到中间面板显示为自定义组件。经过上面的步骤之后,就能够知道拖拽本质上是数据的clone和数组中顺序变化,那么从左侧拖拽过来形成组件,也就是要把这个数据转换成组件。
「数组结构」、「数组每一项代表组件的属性」、「数据转换为组件」,这三个关键词加在一起,很自然就能想到v-for渲染,由于要实现的组件有五种类型,但是数组的每一项结构是一致的,所以需要一个包装组件,我这里命名为DisplayItem,再在DisplayItem中使用component的is属性来生成不同类型的组件。图示如下:
定义数据结构由于是数据形成组件,所以要先定义数据结构。到这一步,我先把原型里面的表单的功能和五种组件的功能进行了整理:
表单:表单布局调整控件尺寸调整表单项:标签修改宽度设置数据项来源设置隐藏规则设置是否必填组件类型导致的配置(日期类型、下拉值、表格列等)结合Vben中对应的表单结构和表单项结构,定义出了以下结构:
//当前表单的默认配置
exportfunctioncreateFormConfig(schemas=[]){
return{
//行内表单模式
inline:true,
//表单域标签的位置right/left/top,固定right
"label-position":"right",
//label-width表单域标签的宽度,例如'50px'
"label-width":"200px",
//用于控制该表单内组件的尺寸medium/small/mini
size:"small",
//表单配置数组,每一项代表一个组件
schemas,
//当前选中的控件
currentItem:null
}
//新增模块schema,是上面schemas中的一项
exportfunctionaddModule(label){
return{
//唯一ID,用于绑定值
id:creatUuid(),
//模块名
label,
//默认展开
collapse:true,
//独占一行
row:true,
//组件类型
componentType:FORM_TYPE_TITLE,
//组件信息
component:null,
//配置组件类型
componentConfigType:FORM_CONFIG_TITLE,
//子项
children:[]
}
//新增数据项schema,是上面schemas中的一项
exportfunctionaddDataItem(label,componentType,parentID){
return{
//唯一ID,用于绑定值
id:creatUuid(),
//模块名
label,
//默认展开
collapse:true,
//独占一行
row:componentType===FORM_TYPE_TABLE,
//组件类型
componentType,
//组件信息
component:getItemAttr(componentType),
//配置组件类型
componentConfigType:componentTypeToConfig(componentType),
//父项
parentID,
//子项
children:null
}
//展示时的控件
exportconstFORM_TYPE_DATE="display-date";
exportconstFORM_TYPE_RADIO="display-radio";
exportconstFORM_TYPE_SELECT="display-select";
exportconstFORM_TYPE_INPUT="display-input";
exportconstFORM_TYPE_TABLE="display-table";
exportconstFORM_TYPE_TITLE="display-title";
//配置时的控件
exportconstFORM_CONFIG_DATE="config-date";
exportconstFORM_CONFIG_RADIO="config-radio";
exportconstFORM_CONFIG_SELECT="config-select";
exportconstFORM_CONFIG_INPUT="config-input";
exportconstFORM_CONFIG_TABLE="config-table";
exportconstFORM_CONFIG_TITLE="config-title";
//五种控件的组件信息,对应上面的component属性
exportconstItemAttrs={
//日期时间
[FORM_TYPE_DATE]:{
//组件宽度
width:200,
//传给给组件的属性,默认会把所有的props都传递给控件
componentProps:{
placeholder:"请选择日期",
type:"date",
//显示在输入框的结构
format:"yyyy-MM-dd",
//最终值的结构
"value-format":"yyyy-MM-dd"
},
//组件选项
options:[],
//数据来源
componentData:{
//对应表
tableName:"",
//对应的字段
fieldName:""
},
//是否隐藏
hidden:false,
//组件显隐规则
hiddenRules:[],
//是否必选
required:false,
//必选提示
message:"数据缺失",
//组件校验规则
validateRules:[]
},
.....剩下4种
}
设计组件结构定义好数据结构之后,先不着急实现组件,因为这个层级和逻辑比较复杂,最好是先把组件结构和组件抽离处理了;结合原型图,一共要处理三类组件:
image.png布局组件左侧数据项面板中间展示面板右侧配置面板展示组件五种类型展示组件配置组件表单配置组件组件配置组件(五种类型配置组件)用于处理表单相关的数据和处理函数封装到一个js文件中最终要实现的最外层组件结构如下所示:
template
divclass="main-page"
BaseBoxclass="box"
data-item-panel@addSchema="addSchema"/
/BaseBox
BaseBoxclass="box"
display-panel
:formConfig="formConfig"
:formData="formData"
:IDMap="IDMap"
:currentItem="currentItem"
@setCurrentItem="setCurrentItem"
@deleteCurrentItem="deleteCurrentItem"
/
/BaseBox
BaseBoxclass="box"full
config-panel
:formConfig="formConfig"
:currentItem="currentItem"
@changeFormConfig="changeFormConfig"
@changeSchema="changeSchema"
/
/BaseBox
/div
/template
......
详细的总体结构图如下:
实现组件到这一步就是实现组件,在实现组件的过程中,除了一般的业务处理,有几个点值得注意:
注意点说明数据控制整体数据控制在最上层,formConfig(表单内容)使用prop传递给子组件,子组件更改操作通知到最上层组件处理,保证数据流向正确和页面实时更新显示当前选中的表单项的匹配的配置组件面板中点击选中表单项时给formConfig的currentItem属性赋值,配置面板根据currentItem中的「配置组件类型」属性,在右侧显示相应类型的配置组件表单数据为空提示配置了required: true的组件,利用el-form-item的error属性提示错误信息显隐规则通过方法将显隐规则数组最终转换为布尔值,结合v-if实现显示隐藏表单数据回显利用每个组件的唯一id来达到绑定表单数据的目的表单配置保存校验定义校验函数,收集配置错误信息,点击保存时弹出错误信息表格提示用户实际实现路径在经过上面的步骤之后,「表单生成器页面」完成了,剩下的就是处理表单回显页面和其他页面;表单回显页面其实就是之前写的display-panel组件,所以剩下的内容比较轻松,就不赘述了。
在这里我梳理了完成这个需求的实际步骤,从零到一,慢慢丰富内容:
熟悉需求了解相似的产品实现先处理表单生成器页面按照原型图还原整体页面先处理左侧部分的增删改逻辑、折叠展开逻辑实现左侧部分拖拽到面板的逻辑实现面板内拖拽逻辑定义数据结构(表单、表单项)实现拖拽到面板生成组件逻辑用一个input类型组件贯穿从左侧拖拽到中间,再到右侧配置的总流程添加其他四个类型的组件逻辑处理校验处理联动显隐再处理表单回显提取数据页面定义数据回显规则接口联调关键源码关键代码文件思维导图,如下:
由于这部分的代码比较多,直接粘贴在博客上,会有较大的阅读心智负担,所以我将代码放到了这里,有需要的朋友可以点击查看。(https://gist.github.com/StudyDayByDay/ae0cdbdb1955d4a51538887e69bdebf9)
ps:只提供了关键代码,毕竟这个是公司的业务,肯定不能全部分享;如果你有实现这个的需求,建议结合Vben表单设计源码(https://github.com/vbenjs/vue-vben-admin/tree/main/src/views/form-design)一同研究。
总结完成这个需求,我比较有感触的是,设计一个东西和写业务的区别很大,学到了许多东西,挺有意义的。
最后分享一下《Vue.js设计与实现》的作者霍春阳(HcySunYang)在书中的一段话:不要惧怕写出不完美的代码,只要在后续迭代的过程中“见招拆招”,代码就会变得越来越完善,框架也会变得越来越健壮。
我们下一篇文章再见!!!
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线