动态表单 自定义布局(代码实现)

本文详细介绍了如何使用vuedraggable和vue3-draggable-resizable库在动态表单中创建自定义布局,通过render函数实现各种控件,如输入框、下拉框等,并展示了组件化的渲染组件和其在实际项目中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态表单

  • 目前的动态表单基本都是基于vuedraggable拖拽库进行延伸开发

  • 在动态表单的基础上实现自定义布局,主要是依赖于vue3-draggable-resizable库实现的移动位置和缩放等

  • 控件的实现方式 主要是利用的vue的render函数,方便维护

  • 以下只列举其中几个控件

import { resolveComponent } from 'vue'
/**
 * 在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent 方法
 */
 import { elementData } from '@/utils/type'
export default (h:Function, _self:any) => {
    return [h(resolveComponent('el-input'),
      {
        placeholder: _self.obj.placeholder || '这是一个输入框',
        // maxlength: parseInt(_self.obj.maxLength) || 20,
        modelValue: _self.obj.value || '',
        oninput: function(value:any) {
          _self.obj.value = value.currentTarget.value
        }
      }
    )]
}
export const inputConf:elementData = {
  // 对应数据库内类型
  type: 'input',
  // 是否可配置
  config: true,
  // 控件左侧label内容
  label: '输入框',
  placeholder: '',
  // 是否显示行内元素
  inlineBlock: false,
  // 是否必填
  require: false,
  // 最大长度
  maxLength: 10,
  // 选项内数据
  items: [{ 'label_value': null, 'label_name': '' }],
  value: '',
  // 表单name
  name: '',
  // 验证错误提示信息
  ruleError: '该字段不能为空',
  // 是否关联字段
  relation: false,
  // 关联字段name
  relation_name: '',
  // 关联字段value
  relation_value: '',
  // 是否被渲染
  visibility: true
}


//--------------------------------------
import { resolveComponent } from 'vue'
import { elementData } from '@/utils/type'
export default (h: Function, _self: any) => {
  return [
    h(resolveComponent('el-select'),
      {
        modelValue: _self.obj.value,
        placeholder: _self.obj.placeholder,
        filterable: true,
        onChange: function (val: String) {
          _self.obj.value = val
        }
      },
      () => _self.obj.items.map((v: { label: String, value: Number | String | Array<any> }) => {
        return h(resolveComponent('el-option'),
          {
            label: v.label,
            value: v.value
          })
      })
    )
  ]
}
export const selectConf:elementData = {
  // 对应数据库内类型
  type: 'select',
  // 是否可配置
  config: true,
  // 控件左侧label内容
  label: '下拉框',
  placeholder: '请选择',
  // 是否显示行内元素
  inlineBlock: false,
  // 是否必填
  require: false,
  // 最大长度
  maxLength: 10,
  // 选项内数据
  items: [1, 2, 3, 4, 5].map((v, i) => {
    return {
      label: `选项${i + 1}`,
      value: i + ''
    }
  }),
  value: '',
  // 表单name
  name: '',
  // 验证错误提示信息
  ruleError: '该字段不能为空',
  // 是否关联字段
  relation: false,
  // 关联字段name
  relation_name: '',
  // 关联字段value
  relation_value: '',
  // 是否被渲染
  visibility: true
}

定义完控件以后,新建一个renders组件统一引入

import Input from '../element_builder/Input'
import Select from '../element_builder/Select'
import Radio from '../element_builder/Radio'
import Rate from '../element_builder/Rate'
import Checkbox from '../element_builder/Checkbox'
import DatePicker from '../element_builder/DatePicker'
import Divider from '../element_builder/Divider'
import Switch from '../element_builder/Switch'
import TimePicker from '../element_builder/TimePicker'
import Title from '../element_builder/Title'
import Cascader from '../element_builder/Cascader'
import { defineComponent, h, resolveComponent } from 'vue'
// import trigger from './trigger'
const formlist: {[propName:string]:any} = {
  Title, Input, Select, Cascader, Radio, Checkbox, DatePicker, TimePicker, Rate, Switch, Divider
}

export default defineComponent({
  name: 'Renders',
  props: {
    ele: {
      type: String,
      default: ''
    },
    obj: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  render() :any {
    // 获取当前渲染控件
    var arr = (formlist[this.ele] && formlist[this.ele](h, this))
    // 已被绑定name,且require为必填,视为校验字段
    const validate = this.obj.require
    if (['title', 'hr', 'p'].indexOf((this.ele.toLowerCase())) < 0) {
      const FormItem = {
        label: (this.obj.label || this.ele) + ':',
        // 指定验证name
        prop: this.obj.name || 'temp',
        rules: {
          required: validate
        }
      }
      return h(
        resolveComponent('el-form-item'), FormItem,
        () => arr
      )
    } else {
      return h(
        'div',
        {
          style: {
            'text-align': 'center',
            'width': '100%'
          },
          class: {
            items: true
          }
        },
        arr
      )
    }
  }
})


//-----------------------------------
import render from './render'
const form_builder = {
  install: function(Vue:Object) {}
}

form_builder.install = function(Vue:Object) {
  Vue['component'](render.name, render)
}

export default form_builder

最后在页面组件中引用

<template>
  <div class="row">
    <div class="from-left">
      <el-form
        ref="ruleFormLeft"
        :model="ruleForm1"
        label-width="80px"
        class="demo-ruleForm"
      >
        <draggable
          :list="form_list1"
          :clone="cloneData"
          :group="{ name: 'formDynamic', pull: 'clone', put: false }"
          item-key="ele"
          :sort="false"
          @change="changeLocation"
        >
          <template #item="{ element }">
            <div class="from-left-item">
              <div class="left-item">
                <Menu style="width: 1em; height: 1em; margin-right: 8px;" /> {{ element.obj.label }}
              </div>
              <operation style="width: 1em; height: 1em; margin-right: 8px;" />
            </div>
          </template>
        </draggable>
      </el-form>
    </div>

    <div class="from-center">
      <el-radio-group v-model="tabPosition" style="margin-bottom: 30px">
        <el-radio-button label="1">设计</el-radio-button>
        <el-radio-button label="2">预览</el-radio-button>
      </el-radio-group>
      <el-form
        ref="ruleFormRight"
        :model="ruleForm2"
        label-width="120px"
        class="demo-ruleForm"
      >
        <draggable
          v-if="tabPosition==='1'"
          :list="form_list2"
          tag="transition-group"
          group="formDynamic"
          item-key="ele"
          :component-data="componentData"
          :animation="100"
          @change="changeLocation"
        >
          <template #item="{ element,index }">

            <div class="from-center-item" @click="checkItem(element)">
              <div class="delete">
                <el-popconfirm
                  confirm-button-text="确定"
                  cancel-button-text="取消"
                  :icon="InfoFilleds"
                  icon-color="red"
                  title="确定删除当前控件吗?"
                  @confirm="confirmEventDel(index)"
                >
                  <template #reference>
                    <DeleteFilled style="width: 1em; height: 1em; margin-right: 8px;" />
                  </template>
                </el-popconfirm>
              </div>
              <renders :ele="element.ele" :obj="element.obj || {}" @changeCascader="changeCascader" />
            </div>
          </template>
        </draggable>
        <template v-else>
          <div v-for="(element,index) in form_list2" :key="index" class="look">
            <renders :ele="element.ele" :obj="element.obj || {}" @changeCascader="changeCascader" />
          </div>
        </template>
      </el-form>
    </div>
    <div class="item-options">
      <el-form
        ref="form1"
        class="item-options-form"
        :model="itemOptions"
        label-width="120px"
      >
        <el-form-item label="控件名称:" prop="label">
          <el-input v-model="itemOptions.label" />
        </el-form-item>
        <el-form-item label="placeholder:" prop="placeholder">
          <el-input v-model="itemOptions.placeholder" />
        </el-form-item>
        <el-form-item label="最大长度:" prop="maxLength">
          <el-input v-model="itemOptions.maxLength" />
        </el-form-item>
        <el-form-item label="校验错误:" prop="ruleError">
          <el-input v-model="itemOptions.ruleError" />
        </el-form-item>
        <el-form-item label="是否必填:" prop="require">
          <el-radio-group v-model="itemOptions.require">
            <el-radio :label="false"></el-radio>
            <el-radio :label="true"></el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit">保存</el-button>
          <el-button>重置</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script lang="ts">
import { Event } from 'element-plus/es/hooks/use-events'
import { defineComponent, reactive, toRefs, shallowRef } from 'vue'
import draggable from 'vuedraggable'
import formList from '@/components/custom/formList'

import { Operation, Menu, DeleteFilled, InfoFilled } from '@element-plus/icons'
// ts接口定义
interface fromObj {
  form_list1: Array<Object>,
  form_list2: Array<Object>,
  componentData: Object,
  ruleForm1: Object,
  ruleForm2: Object,
  itemOptions: any,
  name: String,
  [propName:string]:any
}
interface ItemObj{
  ele:String,
  obj:{
     [propName: string]: any
  }
}
export default defineComponent({
  name: 'Clone',
  components: {
    draggable, Operation, Menu, DeleteFilled
  },
  setup() {
    const Data = reactive<fromObj>({
      form_list1: formList,
      form_list2: [],
      componentData: {
        type: 'transition',
        name: 'flip-list'
      },
      ruleForm1: {},
      ruleForm2: {},
      itemOptions: {},
      name: '',
      tabPosition: '1'
    })
    const InfoFilleds = shallowRef(InfoFilled)
    const changeLocation = (evt: Event): void => {}
    const checkItem = (val: ItemObj): void => {
      Data.itemOptions = val.obj
    }
    const confirmEventDel = (id:any) => {
      Data.form_list2.splice(id, 1)
    }
    const onSubmit = () => {
      console.log(Data.itemOptions)
    }

    const cloneData = (original: any) => {
      // 深拷贝对象,防止默认空对象被更改
      return JSON.parse(JSON.stringify(original))
    }
    const changeCascader = (value:any) => {
      console.log(value)
    }
    return {
      ...toRefs(Data),
      cloneData,
      onSubmit,
      changeLocation,
      checkItem,
      confirmEventDel,
      InfoFilleds, changeCascader
    }
  }
})
</script>

最终效果如下

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值