Posted in

老板:给你 20 天,写一个可拖拽动态表单生成器 – 掘金_AI阅读总结 — 包阅AI

包阅导读总结

1.

关键词:表单生成器、需求复盘、拖拽、数据结构、组件实现

2.

总结:本文是对表单生成器需求的复盘。作者在短时间内独自承担该需求,借鉴开源项目后,通过分析需求、定义数据结构、解决拖拽问题、实现组件等关键步骤完成了表单生成器页面,并简单提及了表单回显页面的处理。

3.

主要内容:

– 需求背景

– 老板要求 20 天完成表单生成器需求,作者起初感到慌张。

– 需求分析

– 表单生成器页面分为上下两层,包含多个部分和功能。

– 表单回显页面有数据回填等功能。

– 技术实现

– 借鉴 Vben 开源项目。

– 拖拽使用 vue-draggable-plus 插件,解决双向拖拽列表和树结构拖拽问题。

– 定义数据结构,包括表单和表单项。

– 设计组件结构,处理布局、展示、配置组件。

– 实现组件,注意数据控制等要点。

– 实际实现路径

– 完成表单生成器页面,包括增删改等逻辑。

– 处理表单回显提取数据页面和接口联调。

– 总结

– 作者表示完成该需求有收获,强调代码在迭代中完善。

思维导图:

文章地址:https://juejin.cn/post/7383968655077539851

文章来源:juejin.cn

作者:李仲轩

发布时间:2024/6/25 3:25

语言:中文

总字数:3958字

预计阅读时间:16分钟

评分:88分

标签:前端,Vue.js,JavaScript


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

本文用于复盘之前实现的表单生成器需求,记录整体的思路,由于开发时间只有20多天不到一个月,成品比较粗糙,请见谅

效果展示

需求

上面的视频就是最终完成的效果,还存在一些瑕疵,比如拖拽样式优化、input组件后缀样式优化等。

首先「表单生成器页面」分为上下两个部分:

  • 上层:标题、首页附页切换、保存功能
  • 下层:表单生成器
    • 左侧:数据项配置
    • 中间:面板显示
    • 右侧:表单项设置

各自的功能为:

  • 上层:切换首页附页、校验并保存已配置表单,首页附页分开保存
  • 下层:
    • 左侧部分:模块和子项均支持增删改,模块可折叠和展开,点击子项或模块同步显示到中间面板,也可拖动到中间面板
    • 中间部分:显示最终表单效果,组件hover和选中效果,可删除,可拖动调整位置;最终的表单类型只有五种(输入、单选、日期、下拉、表格)
    • 右侧部分:整体表单设置和具体的组件设置

其次为「表单回显页面」,包含数据回填必填校验联动显隐、数据提取并保存功能。

心理活动

当开完需求会的时候,我意识到这个需求的复杂程度不低,此时的我有一点慌🙂;当我询问时间期限的时候,告知我就是这个月底(我是5月6号接到的需求),此时的我感觉慌了🙃;当我再询问这个需求的人员投入的时候,告知我只有我一个前端来处理这个需求,此时的我,已经准备充boss直聘VIP了💀…

开玩笑归开玩笑,该做还得做。其实某些东西是看着大,但很多都是附加功能,只要把核心功能梳理出来,其余的慢慢加就行了。

分析

消化这个需求之后,我暂时没有头绪来处理。我知道这时候我要先去寻找类似的产品取取经。最终我找了Vben开源项目,其中有这个模块实现,如下图:

image.png

其中的核心功能是一致的,在思索几番之后,我准备先研究一下开源项目这个模块的实现。以下是研究结果:

  • 拖拽使用VueDraggablePlus实现
  • 数据结构总体为一个form对象,对象中包含控制form的属性和一个表示表单项的数组,每一个表单项有唯一ID,表单项中内容为表单项配置属性
  • 显示面板渲染逻辑:
    • 层级:form -> VueDraggable(v-for) -> DisplayItem(表单项组件) -> Vue component(利用is属性) -> 自定义组件
    • 逻辑:最外层是form组件,form组件中渲染VueDraggable组件,VueDraggable循环渲染自定义的表单项包装组件DisplayItemDisplayItem中渲染Vue component组件,利用componentis属性来渲染自定义组件
  • 配置面板渲染逻辑:利用Vue component组件的is属性来渲染自定义组件
  • 展示组件和配置组件是配对关系
  • 拖拽表单项、点击新增表单项、删除表单项本质上是表单项数组内「数据」的顺序变化、新增和删除
  • 数据结构是核心,数据结构对应页面结构

关键步骤

看似比较大的系统,只要保持耐心,先把主干建立起来,然后逐渐丰富,就能够实现,关键步骤如下:

拖拽

这里的拖拽,使用vue-draggable-plus插件来实现,有三种使用方式:

在本次案例中使用的是组件方式,案例中从左侧拖拽到中间面板的行为其实是双列表匹配,官方文档中也有示例,基本使用例子如下:

<template>  <div class="flex">    <VueDraggable      class="flex flex-col gap-2 p-4 w-300px h-300px m-auto bg-gray-500/5 rounded overflow-auto"      v-model="list1"      animation="150"      ghostClass="ghost"      group="people"      @update="onUpdate"      @add="onAdd"      @remove="remove"    >      <div        v-for="item in list1"        :key="item.id"        class="cursor-move h-30 bg-gray-500/5 rounded p-3"      >        {{ item.name }}      </div>    </VueDraggable>    <VueDraggable      class="flex flex-col gap-2 p-4 w-300px h-300px m-auto bg-gray-500/5 rounded overflow-auto"      v-model="list2"      animation="150"      group="people"      ghostClass="ghost"      @update="onUpdate"      @add="onAdd"      @remove="remove"    >      <div        v-for="item in list2"        :key="item.id"        class="cursor-move h-30 bg-gray-500/5 rounded p-3"      >        {{ item.name }}      </div>    </VueDraggable>  </div>  <div class="flex justify-between">    <preview-list :list="list1" />    <preview-list :list="list2" />  </div></template><script setup>import { ref } from 'vue'import { VueDraggable } from 'vue-draggable-plus'const list1 = ref([  {    name: 'Joao',    id: '1'  },  {    name: 'Jean',    id: '2'  },  {    name: 'Johanna',    id: '3'  },  {    name: 'Juan',    id: '4'  }])const list2 = ref(  list1.value.map(item => ({    name: `${item.name}-2`,    id: `${item.id}-2`  })))function onUpdate() {  console.log('update')}function onAdd() {  console.log('add')}function remove() {  console.log('remove')}</script>

如代码所示,只需保证两个列表的group属性的值一致以及数据结构一致即可;

由于实际需求中,只需要从左侧拖拽到中间,并不需要从中间拖拽回去,所以为了规避这种行为,需要给左侧的group属性设置为以下内容:

:group="{ name: DraggableGroup, pull: 'clone', put: false }"

更多插件API请点击这里


解决了双向拖拽列表之后,此时出现了一个新的问题,左侧的内容是一个树,不是一个扁平的数组,同样要实现拖拽,如下图:

image.png

插件也考虑到了这一点,可以实现嵌套的功能,官网嵌套示例;逻辑就是组件自调用。

image.png

数据转换为组件

实现了拖拽的需求之后,下一步就是实现从左侧拖拽到中间面板显示为自定义组件。经过上面的步骤之后,就能够知道拖拽本质上是数据的clone和数组中顺序变化,那么从左侧拖拽过来形成组件,也就是要把这个数据转换成组件。

「数组结构」、「数组每一项代表组件的属性」、「数据转换为组件」,这三个关键词加在一起,很自然就能想到v-for渲染,由于要实现的组件有五种类型,但是数组的每一项结构是一致的,所以需要一个包装组件,我这里命名为DisplayItem,再在DisplayItem中使用componentis属性来生成不同类型的组件。图示如下:显示面板渲染层级关系图

定义数据结构

由于是数据形成组件,所以要先定义数据结构。到这一步,我先把原型里面的表单的功能和五种组件的功能进行了整理:

  • 表单:
  • 表单项:
    • 标签修改
    • 宽度设置
    • 数据项来源设置
    • 隐藏规则设置
    • 是否必填
    • 组件类型导致的配置(日期类型、下拉值、表格列等)

结合Vben中对应的表单结构和表单项结构,定义出了以下结构:

export function createFormConfig(schemas = []) {  return {        inline: true,        "label-position": "right",        "label-width": "200px",        size: "small",        schemas,        currentItem: null  };}
export function addModule(label) {  return {        id: creatUuid(),        label,        collapse: true,        row: true,        componentType: FORM_TYPE_TITLE,        component: null,        componentConfigType: FORM_CONFIG_TITLE,        children: []  };}export function addDataItem(label, componentType, parentID) {  return {        id: creatUuid(),        label,        collapse: true,        row: componentType === FORM_TYPE_TABLE,        componentType,        component: getItemAttr(componentType),        componentConfigType: componentTypeToConfig(componentType),        parentID,        children: null  };}
export const FORM_TYPE_DATE = "display-date";export const FORM_TYPE_RADIO = "display-radio";export const FORM_TYPE_SELECT = "display-select";export const FORM_TYPE_INPUT = "display-input";export const FORM_TYPE_TABLE = "display-table";export const FORM_TYPE_TITLE = "display-title";export const FORM_CONFIG_DATE = "config-date";export const FORM_CONFIG_RADIO = "config-radio";export const FORM_CONFIG_SELECT = "config-select";export const FORM_CONFIG_INPUT = "config-input";export const FORM_CONFIG_TABLE = "config-table";export const FORM_CONFIG_TITLE = "config-title";export const ItemAttrs = {    [FORM_TYPE_DATE]: {        width: 200,        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>  <div class="main-page">    <BaseBox class="box">      <data-item-panel @addSchema="addSchema" />    </BaseBox>    <BaseBox class="box">      <display-panel        :formConfig="formConfig"        :formData="formData"        :IDMap="IDMap"        :currentItem="currentItem"        @setCurrentItem="setCurrentItem"        @deleteCurrentItem="deleteCurrentItem"      />    </BaseBox>    <BaseBox class="box" full>      <config-panel        :formConfig="formConfig"        :currentItem="currentItem"        @changeFormConfig="changeFormConfig"        @changeSchema="changeSchema"      />    </BaseBox>  </div></template>......

详细的总体结构图如下:组件层级图

实现组件

到这一步就是实现组件,在实现组件的过程中,除了一般的业务处理,有几个点值得注意:

注意点 说明
数据控制 整体数据控制在最上层,formConfig(表单内容)使用prop传递给子组件,子组件更改操作通知到最上层组件处理,保证数据流向正确和页面实时更新
显示当前选中的表单项的匹配的配置组件 面板中点击选中表单项时给formConfigcurrentItem属性赋值,配置面板根据currentItem中的「配置组件类型」属性,在右侧显示相应类型的配置组件
表单数据为空提示 配置了required: true的组件,利用el-form-itemerror属性提示错误信息
显隐规则 通过方法将显隐规则数组最终转换为布尔值,结合v-if实现显示隐藏
表单数据回显 利用每个组件的唯一id来达到绑定表单数据的目的
表单配置保存校验 定义校验函数,收集配置错误信息,点击保存时弹出错误信息表格提示用户

实际实现路径

在经过上面的步骤之后,「表单生成器页面」完成了,剩下的就是处理表单回显页面和其他页面;表单回显页面其实就是之前写的display-panel组件,所以剩下的内容比较轻松,就不赘述了。

在这里我梳理了完成这个需求的实际步骤,从零到一,慢慢丰富内容:

  • 熟悉需求
  • 了解相似的产品实现
  • 先处理表单生成器页面
    • 按照原型图还原整体页面
    • 先处理左侧部分的增删改逻辑、折叠展开逻辑
    • 实现左侧部分拖拽到面板的逻辑
    • 实现面板内拖拽逻辑
    • 定义数据结构(表单、表单项)
    • 实现拖拽到面板生成组件逻辑
    • 用一个input类型组件贯穿从左侧拖拽到中间,再到右侧配置的总流程
    • 添加其他四个类型的组件逻辑
    • 处理校验
    • 处理联动显隐
  • 再处理表单回显提取数据页面
  • 接口联调

关键源码

关键代码文件思维导图,如下:

关键代码文件思维导图由于这部分的代码比较多,直接粘贴在博客上,会有较大的阅读心智负担,所以我将代码放到了这里,有需要的朋友可以点击查看。

ps:只提供了关键代码,毕竟这个是公司的业务,肯定不能全部分享;如果你有实现这个的需求,建议结合Vben表单设计源码一同研究。

总结

完成这个需求,我比较有感触的是,设计一个东西和写业务的区别很大,学到了许多东西,挺有意义的。

最后分享一下《Vue.js设计与实现》的作者霍春阳(HcySunYang)在书中的一段话:不要惧怕写出不完美的代码,只要在后续迭代的过程中“见招拆招”,代码就会变得越来越完善,框架也会变得越来越健壮。

我们下一篇文章再见!!!