likes
comments
collection
share

基于vue3+ts+vite封装的动态表单,支持手动编辑生成页面表单配置并渲染使用,所有源码都在文章中

作者站长头像
站长
· 阅读数 16

效果图

基于vue3+ts+vite封装的动态表单,支持手动编辑生成页面表单配置并渲染使用,所有源码都在文章中

一、第一个文件index.vue,也就是表单生成器的根页面

<script setup>
import axios from 'axios'
import { ref, reactive } from 'vue'
import formRender from './modules/formRender.jsx'
import formCreator from './modules/formCreator.vue'
import { ElNotification } from 'element-plus'
// 表单配置结构
let config = reactive({})
const flag = ref(false)
// 获取表单配置结构
const getFormConfig = async () => {
  const { data } = await axios.get('http://localhost:3000/formConfig')
  config = reactive(data)
  flag.value = true
  console.log('点击了生成, 重新请求了页面配置', config);
}
getFormConfig()
// 刷新配置
const refresh = () => {
  flag.value = false
  getFormConfig()
}
// 表单初始数据
const data = {}
// 提交表单
const submitForm = (data) => {
  console.log('submit', data)
  ElNotification({
    title: 'Success',
    message: data,
    type: 'success',
  })
}
</script>

<template>
  <Suspense v-if="flag">
    <el-main class="main">
      <div class="configBox">
        <formCreator @refresh="refresh"></formCreator>
      </div>
      <div class="formBox">
        <formRender :config="config" :init="data" @submitForm="submitForm"></formRender>
      </div>
    </el-main>
  </Suspense>
</template>

<style scoped lang="scss">
.main {
  display: flex;

  .configBox {
    flex: 1;
    margin-right: 10px;
    padding: 20px 10px;
    border-radius: 20px;
    background-color: #8EC5FC;
    background-image: linear-gradient(62deg, #8EC5FC 0%, #E0C3FC 100%);
  }

  .formBox {
    flex: 1;
    padding: 20px 10px;
    border-radius: 20px;
    background-color: #FFDEE9;
    background-image: linear-gradient(0deg, #FFDEE9 0%, #B5FFFC 100%);
  }
}
</style>
  • 此页面放置了两个组件,一个生成器,一个渲染器,主要功能就是生成器会制造出提供表单渲染的数据结构,再传递给渲染器去渲染使用。
  • 第12行,http://localhost:3000/formConfig,这个是使用json-server生成的接口,数据存在目录下的一个json文件中,如果不了解的小伙伴儿可以看一下这篇文章blog.csdn.net/ligonglanyu…

二、json-server存放的页面渲染的数据结构

文件位置,JSONData下的db.json

基于vue3+ts+vite封装的动态表单,支持手动编辑生成页面表单配置并渲染使用,所有源码都在文章中

重点看一下下面的数据结构

{
  "formConfig": {
    "title": "表单标题",
    "formItems": [
      [
        {
          "colspan": 12,
          "options": [],
          "label": "姓名",
          "type": "input",
          "key": "username",
          "placeholder": "请输入姓名",
          "inputType": "text"
        },
        {
          "colspan": 12,
          "options": [],
          "label": "账号",
          "type": "number",
          "key": "account",
          "placeholder": "请输入年龄",
          "inputType": "number"
        }
      ],
      [        {          "colspan": 12,          "options": [            {              "label": "男",              "value": "1"            },            {              "label": "女",              "value": "2"            }          ],
          "label": "性别",
          "type": "select",
          "key": "sadf"
        },
        {
          "colspan": 12,
          "options": [
            {
              "label": "西瓜",
              "value": "1"
            },
            {
              "label": "橡胶",
              "value": "2"
            },
            {
              "label": "巴啦啦",
              "value": "3"
            }
          ],
          "label": "喜欢的",
          "type": "radio",
          "key": "ddd"
        }
      ]
    ]
  }
}
  • 数据结构总对象formConfig,也是我的json-server接口名字。
  • formItems,二维数组,功能是渲染页面,第一层数组代表整个表单是个数组构成;第二层数组代表每一行都是一个数组构成;二层数组中有多个对象组成,代表每一列是一个对象,来存放每一项表单的各个属性。
  • colspan: 使用element的栅格系统, 分为24格
  • options:选项式表单需要的配置项
  • label:表单标题
  • type:表单类型
  • key:表单绑定的字段
  • placeholder:提示语
  • inputType:表单为文本输入框时的类型

三、formCreator.vue,表单生成器组件

<script setup lang='ts'>
import axios from 'axios'
import { ref, reactive, defineEmits } from 'vue'
import { Plus, Lightning } from '@element-plus/icons-vue'
import rowMenu from './menu.vue'
import formOption from './formOption.vue'
// 菜单类型接口
interface menuPosition {
  x: number,
  y: number,
  rowIndex: number,
  colIndex: number
}
// 列数据类型接口
interface colData {
  label: string,
  type: string,
  colspan: number,
  key: string,
  inputType: string,
  placeholder: string,
  options: Array<object>
}
const emit = defineEmits(['refresh'])
// 表单结构
let formConstruction: Array<Array<object>> = reactive([])
let flag = ref(false)
// 获取表单配置结构
axios.get('http://localhost:3000/formConfig').then(res => {
  formConstruction = reactive(res.data.formItems)
  flag.value = true
})
// 新增行
const addRow = () => {
  formConstruction.push([{}])
}
// 删除行
const delRow = ({ rowIndex }: menuPosition) => {
  formConstruction.splice(rowIndex, 1)
}
// 添加列
const addCol = ({ rowIndex }: menuPosition) => {
  formConstruction[rowIndex].push([])
}
// 删除列
const delCol = ({ rowIndex, colIndex }: menuPosition) => {
  if (formConstruction[rowIndex].length > 1) {
    formConstruction[rowIndex].splice(colIndex, 1)
  }
}
// 表单配置显隐
let formConfigDialogVisible = ref(false)
const cancelFormConfigDialog = () => {
  formConfigDialogVisible.value = false
}
// 该列位置
let colPosition = reactive({})
// 该列数据
let colData = reactive({})
// 设置该列表单
const setColForm = (menuPosition: menuPosition) => {
  formConfigDialogVisible.value = true
  colPosition = menuPosition
  colData = formConstruction[menuPosition.rowIndex][menuPosition.colIndex]
}
// 保存该列表单
const saveFormConfig = (formConfigObj: colData) => {
  const { rowIndex, colIndex } = <menuPosition>colPosition
  formConstruction[rowIndex][colIndex] = { ...formConfigObj }
}
// 生成表单
const createFormConfig = async () => {
  const formConfig = { title: '表单标题', formItems: formConstruction }
  await axios.post('http://localhost:3000/formConfig', formConfig)
  emit('refresh')
}
// 菜单显隐
let showMenu = ref(false)
// 菜单位置
const menuPosition = reactive({
  x: 0,
  y: 0,
  rowIndex: 0,
  colIndex: 0
})
// 右键菜单
const handleContextmenu = (e: any, rowIndex: number, colIndex: number) => {
  showMenu.value = true
  menuPosition.x = e.clientX
  menuPosition.y = e.clientY
  menuPosition.rowIndex = rowIndex
  menuPosition.colIndex = colIndex
}
window.onclick = () => {
  showMenu.value = false
}
window.oncontextmenu = () => {
  showMenu.value = false
}
</script>

<template>
  <el-button style="margin-bottom: 20px;" type="primary" @click="addRow">
    新增行<el-icon class="el-icon--right">
      <Plus />
    </el-icon>
  </el-button>
  <el-button style="margin-bottom: 20px;" type="primary" @click="createFormConfig">
    生成<el-icon class="el-icon--right">
      <Lightning />
    </el-icon>
  </el-button>
  <!-- 操作菜单 -->
  <rowMenu v-if="showMenu" :menuPosition="menuPosition" @delRow="delRow" @addCol="addCol" @delCol="delCol"
    @setColForm="setColForm"></rowMenu>
  <!-- 表单结构 -->
  <Suspense v-if="flag">
    <div class="row" v-for="(row, rowIndex) in formConstruction" :key="rowIndex">
      <div class="col" v-for="(col, colIndex) in row" :key="colIndex"
        @contextmenu.prevent.stop="handleContextmenu($event, rowIndex, colIndex)">
        <el-tag v-if="col.type" style="height: 100%;" type="success">已设置</el-tag>
      </div>
    </div>
  </Suspense>
  <!-- 表单配置选择 -->
  <el-dialog v-model="formConfigDialogVisible" title="form config" width="30%" destroy-on-close>
    <formOption :colData="colData" @cancel="cancelFormConfigDialog" @save="saveFormConfig"></formOption>
  </el-dialog>
</template>

<style scoped lang="scss">
.row {
  display: flex;
  height: 40px;
  min-height: 40px;
  margin-bottom: 20px;
  padding: 2px;
  border: 1px solid #fff;
  border-radius: 5px;
  cursor: pointer;

  .col {
    position: relative;
    flex: 1;
    height: 100%;
    border-radius: 5px;
    border-right: 1px solid #fff;
    background-color: transparent;
    transition: all 0.5s;

    &:last-child {
      border-right: none;
    }

    &:hover {
      background-color: #f0f0f0;
    }
  }
}
</style>
  • 改文件中包含了两个组件,操作菜单组件和表单配置组件;
  • 操作菜单组件,负责操作整个表单的结构,添加行或者列,提供操作选项;
  • 表单配置组件,负责配置该列中存放的表单具体信息,什么类型,什么标题等

四、menu.vue,操作菜组件

<script setup lang='ts'>
const { menuPosition } = defineProps({
  menuPosition: {
    type: Object,
    required: true,
    default: () => ({})
  }
})
const emit = defineEmits(['delRow', 'addCol', 'delCol', 'setColForm'])
// 删除行
const delRow = () => {
  emit('delRow', menuPosition)
}
// 添加列
const addCol = () => {
  emit('addCol', menuPosition)
}
// 删除列
const delCol = () => {
  emit('delCol', menuPosition)
}
// 设置该列表单
const setColForm = () => {
  emit('setColForm', menuPosition)
}
</script>

<template>
  <div class="menuBox" :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }">
    <div class="btn" @click="delRow">删除当前行</div>
    <div class="btn" @click="addCol">添加列</div>
    <div class="btn" @click="delCol">删除当前列</div>
    <div class="btn" @click="setColForm">配置该列表单</div>
  </div>
</template>

<style scoped lang='scss'>
.menuBox {
  position: fixed;
  width: 200px;
  background-color: #409eff;
  color: #fff;
  z-index: 1;
  cursor: pointer;
  border-radius: 10px;
  .btn {
    padding: 5px 0;
    text-align: center;
    &:hover {
      background-color: #304156;
    }
    &:first-child {
      border-radius: 10px 10px 0 0;
    }
    &:last-child {
      border-radius: 0 0 10px 10px;
    }
  }
}
</style>

五、formOption.vue,表单配置组件

<script setup lang='ts'>
import { reactive, defineProps, defineEmits, computed } from 'vue'
const { colData } = defineProps({
  colData: {
    type: Object,
    required: true,
    default: () => ({})
  }
})
const emit = defineEmits(['cancel', 'save'])
// 表单配置对象
let formConfigObj = reactive({ ...colData })
// 反转换列宽百分比
formConfigObj.colspan = formConfigObj.colspan ? Math.floor(formConfigObj.colspan / 0.24) : 1
// 默认options
formConfigObj.options = formConfigObj.options ? formConfigObj.options : []
// 表单类型
const options = [
  {
    value: 'input',
    label: '文本输入框',
  },
  {
    value: 'number',
    label: '数字框',
  },
  {
    value: 'password',
    label: '密码框',
  },
  {
    value: 'select',
    label: '下拉选择框',
  },
  {
    value: 'radio',
    label: '单选框',
  },
  {
    value: 'checkbox',
    label: '多选框',
  },
  {
    value: 'date',
    label: '时间选择器',
  },
]
// 显隐选项填写表单
let showOption = computed(() => formConfigObj.type === 'select' || formConfigObj.type === 'radio' || formConfigObj.type === 'checkbox')
// 添加选项
const addOptions = () => {
  formConfigObj.options.push({})
}
// 取消
const cancel = () => {
  emit('cancel')
}
// 保存
const save = () => {
  const { colspan, type } = formConfigObj
  // 列宽百分比转换
  if (colspan === 1) {
    formConfigObj.colspan = 24
  } else if (colspan > 100) {
    formConfigObj.colspan = 24
  } else {
    formConfigObj.colspan = Math.ceil(colspan * 0.24) || 1
  }
  //表单类型处理
  if (type === 'input') {
    formConfigObj.inputType = 'text'
  } else if (type === 'number') {
    formConfigObj.inputType = 'number'
  } else if (type === 'password') {
    formConfigObj.type = 'input'
    formConfigObj.inputType = 'password'
  }
  emit('save', formConfigObj)
  cancel()
}
</script>

<template>
  <el-form ref="formConfig" :model="formConfigObj">
    <el-form-item label="标题">
      <el-input v-model="formConfigObj.label" placeholder="请输入标题"></el-input>
    </el-form-item>
    <el-form-item label="类型">
      <el-select style="width: 100%;" v-model="formConfigObj.type" placeholder="选择表单类型">
        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
      </el-select>
    </el-form-item>
    <el-form-item label="选择性表单选项" v-if="showOption">
      <div>
        <el-button type="primary" size="small" @click="addOptions">添加</el-button>
        <div class="optionRow" v-for="(item, index) in formConfigObj.options" :key="index">
          <el-input v-model="item.label" placeholder="请输入选项名"></el-input>
          <el-input v-model="item.value" placeholder="请输入选项名值"></el-input>
        </div>
      </div>
    </el-form-item>
    <el-form-item label="字段">
      <el-input v-model="formConfigObj.key" placeholder="请输入表单绑定字段"></el-input>
    </el-form-item>
    <el-form-item label="提示语">
      <el-input v-model="formConfigObj.placeholder" placeholder="请输入表单内提示语"></el-input>
    </el-form-item>
    <el-form-item label="该列宽度占百分比">
      <el-input type="number" :min="1" :max="100" v-model.number="formConfigObj.colspan"></el-input>
    </el-form-item>
  </el-form>
  <div style="display: flex;justify-content: flex-end;">
    <span class="dialog-footer">
      <el-button @click="cancel">取消</el-button>
      <el-button type="primary" @click="save">保存</el-button>
    </span>
  </div>
</template>

<style scoped lang='scss'>
.optionRow {
  margin-bottom: 10px;
}
</style>

六、formRender.jsx,渲染器组件, 该组件是jsx文件,vue3是完全支持jsx的,非常的友好,如果不了解的小伙伴可以看一下这篇文章blog.csdn.net/lwf3115841/…

import { reactive } from 'vue'
export default {
  props: {
    config: {
      type: Object
    },
    init: {
      type: Object
    }
  },
  emits: ['submitForm'],
  setup(props, context) {
    // 表单配置
    const { title, formItems } = props.config
    // 表单回显
    let formData = reactive(props.init)
    // 渲染表单元素
    const renderEle = (item) => {
      switch (item.type) {
        case 'input':
          return <el-input type={item.inputType} placeholder={item.placeholder} v-model={formData[item.key]}></el-input>
        case 'select':
          return <el-select style="width: 100%;" v-model={formData[item.key]}>
            {item.options.map(opt => (
              <el-option label={opt.label} value={opt.value}></el-option>
            ))}
          </el-select>
        case 'number':
          return <el-input min={0} type={item.inputType} v-model={formData[item.key]}></el-input>
        case 'radio':
          return <el-radio-group v-model={formData[item.key]}>
            {item.options.map(opt => (
              <el-radio label={opt.value}>{opt.label}</el-radio>
            ))}
          </el-radio-group>
        case 'checkbox':
          return <el-checkbox-group v-model={formData[item.key]}>
            {item.options.map(opt => (
              <el-checkbox label={opt.value}>{opt.label}</el-checkbox>
            ))}
          </el-checkbox-group>
        case 'date':
          return <el-date-picker style="width: 100%;" v-model={formData[item.key]}
            type="date"
            placeholder="请选择时间"
            size={formData[item.size]} />
      }
    }
    // 渲染列
    const renderColumn = (cols) => {
      return cols.map(col => (
        <el-col span={col.colspan}>
          <el-form-item label={col.label}>
            {renderEle(col)}
          </el-form-item>
        </el-col>
      ))
    }
    // 渲染行
    const renderRows = (rows) => {
      return rows.map(row => (
        <el-row>
          {renderColumn(row)}
        </el-row>
      ))
    }
    // 提交
    const submit = () => {
      context.emit('submitForm', formData)
    }
    return () => (<>
      <el-button type="primary" style="margin: 0 0 20px 40px;" onClick={submit}>提交</el-button>
      <el-form label-width="80px">
        {formItems && renderRows(formItems)}
      </el-form>
    </>)
  }
}

该文件负责了整个表单的渲染,如果我使用vue文件来做渲染,那整体页面结构会非常混乱,映入眼帘的是数不清的v-for,难以维护,使用jsx就完美的解决了这个问题


以上就是全部的内容了,该组件只是初步实现了动态表单的功能,后续的功能完善交给你们啦,不明白可以随时私信我哈,谢谢大家~