Vue3封装动态Form表单

发布于:2025-07-05 ⋅ 阅读:(21) ⋅ 点赞:(0)

在使用Vue3的时候,有的时候,经常需要写很多的表单,使用UI组件库的时候,经常需要一大堆的Form和Form-Item;写的多,拷贝的也多,看到网上大佬的优雅代码,醍醐灌顶,改造了如下代码

UI库: @arco-design/web-vue ^2.56.3

vue: ^3.5.12,

  1. 父组件
<template>
    <div class="container">
        <!-- 第一种用法,单文件.vue的用法,所有的属性通过props传递过去 -->
        <!-- <FormBuilder ref="formRef" v-model="formData" :fields="fields" :label-col-props="{ span: 2 }"
            :wrapper-col-props="{ span: 20 }" :rules="rules">
        </FormBuilder> -->
        
        <!-- 第二种用法,hooks的方式,通过渲染函数进行渲染,h -->
        <FormBuilder2></FormBuilder2>
        <a-button @click="handleSubmit">提交</a-button>
    </div>
</template>

<script setup lang="ts">
import FormBuilder from '@/components/FormBuilder.vue';
import { Message } from '@arco-design/web-vue';
import useFormBuilder from '@/hooks/useFormBuilder';

const formRef = useTemplateRef<InstanceType<typeof FormBuilder>>('formRef')

const formData = reactive({
    name: '张',
    age: 18,
    gender: '1',
})

const fields = computed(() => ([
    {
        componentType: 'input',
        formItemProps: {
            label: '姓名',
            field: 'name',
            required: true,
        },
        componentProps: {
            placeholder: '请输入姓名',
        }
    },
    {
        componentType: 'number',
        isHidden: formData.name === "张三", // ",
        formItemProps: {
            label: '年龄',
            field: 'age',
        },
        componentProps: {
            placeholder: '请输入年龄',
        }
    },
    {
        componentType: 'select',
        formItemProps: {
            label: '性别',
            field: 'gender',
        },
        componentProps: {
            placeholder: '请选择性别',
            options: [
                { label: '男', value: '1' },
                { label: '女', value: '2' }
            ]
        }
    }
]))

const rules = {
    name: [{ required: true, message: '请输入姓名' }],
    age: [{ required: true, message: '请输入年龄' }],
    gender: [{ required: true, message: '请选择性别' }],
}

const handleSubmit = async () => {
    // const valid = await formRef.value?.validate()
    const valid = await getFormBuilderRef().value?.validate()
    if (valid) return Message.error('提交失败')
    console.log(formData);
    Message.success('提交成功')
}
const { FormBuilder: FormBuilder2, getRef: getFormBuilderRef } = useFormBuilder({
    modelValue: formData,
    fields: fields.value,
    rules,
    wrapperColProps: {
        span: 20
    },
    labelColProps: {
        span: 2
    }
})
</script>

<style lang="scss" scoped></style>
  1. FormBuilder.vue 子组件
<template>
    <a-form ref="formRef" :model="formData" v-bind="$attrs">
        <a-row>
            <a-col :span="getSpan(item)" v-for="item in filterFields" :key="item.formItemProps.field">
                <a-form-item v-bind="item.formItemProps">
                    <slot :name="item.formItemProps.field">
                        <component v-model="formData[item.formItemProps.field]" :is="getComponent(item)"
                            v-bind="item.componentProps">
                        </component>
                    </slot>
                </a-form-item>
            </a-col>
        </a-row>
    </a-form>
</template>

<script setup lang="ts" name="FormBuilder">
import { Input, InputNumber, Select } from '@arco-design/web-vue'
import type { ValidatedError } from '@arco-design/web-vue/es/form/interface'
const props = defineProps({
    fields: {
        type: Array as PropType<Record<string, any>[]>,
        default: () => []
    }
})

const formRef = useTemplateRef('formRef')
const formData = defineModel<Record<string, any>>({ required: true })

const componentMap = {
    input: Input,
    number: InputNumber,
    select: Select,
}

const getComponent = (item: Record<string, any>) => {
    return componentMap[item.componentType as keyof typeof componentMap]
}

// 1. isHidden 为函数每次更新表单时都会执行
// 2. 对fields使用 computed 缓存
// const filterFields = computed(() => {
//     return props.fields.filter((item) => {
//         return !(typeof item.isHidden === 'function' ? item.isHidden() : item.isHidden)
//     })
// })

const filterFields = computed(() => {
    return props.fields.filter((item) => !item.isHidden)
})

const getSpan = (item: Record<string, any>) => {
    return item.span || 12
}

type ValidateFieldFnReturn = Promise<undefined | Record<string, ValidatedError>>
defineExpose({
    validate: () => formRef.value?.validate() as ValidateFieldFnReturn,
    validateField: ((field: string | string[]) => formRef.value?.validateField(field) as ValidateFieldFnReturn)
})
</script>

<style lang="scss" scoped></style>
  1. useFormBuilder.ts hooks文件
import FormBuilder from "@/components/FormBuilder.vue";
import type { SetupContext } from "vue";
type FormBuilderProps = InstanceType<typeof FormBuilder>["$props"];

export default function useFormBuilder(props: Record<string, any>) {
    const formRef = ref<InstanceType<typeof FormBuilder>>();
    const Component = (_: Record<string, any>, { slots }: SetupContext) => {
        return h(FormBuilder, { ...(props as FormBuilderProps), ref: formRef }, slots);
    };
    return {
        FormBuilder: Component,
        getRef: () => formRef,
    };
}

  1. 最终的演示效果
    在这里插入图片描述

网站公告

今日签到

点亮在社区的每一天
去签到