实现功能

  1. 条件渲染(字段的展示隐藏)
  2. 动态验证(必填、正则、自定义校验)
  3. 数据联动(拼接、拆分、包含、…)
  4. 类型安全(TypeScript)

项目结构

/src/dynamicForm
  /components
    DynamicForm.vue
    FormField.vue
    FormValidator.vue
  /utils
    conditionParser.ts
    dataLinkage.ts
    formRules.ts
  formTypes.ts
  index.vue

示例代码

  1. formTypes.ts

    import { FormValidateCallback } from "element-plus";
    
    // 基础字段类型
    export type FieldType = "text" | "number" | "select" | "checkbox" | "radio";
    
    // 字段选项
    export interface FieldOption {
      value: string | number;
      label: string;
    }
    
    // 条件操作符
    export type ConditionOperator =
      | "=="
      | "!="
      | ">"
      | "<"
      | ">="
      | "<="
      | "includes"
      | "in"
      | "notIn"
      | "startsWith"
      | "endsWith";
    
    // 单个条件
    export interface FieldCondition {
      field: string;
      operator: ConditionOperator;
      value: any;
    }
    
    // 组合条件
    export interface CombinedConditions {
      and?: FieldCondition[] | CombinedConditions[];
      or?: FieldCondition[] | CombinedConditions[];
    }
    
    // 验证规则
    export interface ValidationRule {
      required?: boolean;
      pattern?: RegExp;
      minLength?: number;
      maxLength?: number;
      validator?: (rule: any, value: any, callback: FormValidateCallback) => Promise<void>;
      message?: string;
      trigger?: string | string[];
    }
    
    // 动态规则
    export interface DynamicRule {
      conditions?: FieldCondition | CombinedConditions;
      rule: ValidationRule;
      trigger?: string | string[];
    }
    
    // 数据联动类型
    export type LinkageType = "direct" | "mapping" | "function" | "concatenate";
    
    // 数据联动配置
    export interface FieldLinkage {
      type: LinkageType;
      sourceField?: string;
      mapping?: Record<string, any>;
      fn?: (sourceValue: any, formData: Record<string, any>) => any;
      fields?: string[];
      separator?: string;
    }
    
    // 表单字段定义
    export interface FormField {
      name: string;
      label: string;
      type: FieldType;
      defaultValue?: any;
      options?: FieldOption[];
      conditions?: FieldCondition | CombinedConditions;
      required?: boolean;
      requiredMessage?: string;
      rules?: ValidationRule;
      dynamicRules?: DynamicRule[];
      linkage?: FieldLinkage;
      validateOnChange?: boolean;
    }
    
    // 表单数据
    export type FormData = Record<string, any>;
    
  2. conditionParser.ts

    /**
     * 根据条件判断是否展示该字段
    * @param conditions
    * @param formData
    * @returns Boolean
    * @description 解析条件表达式,支持多条件与/或的组合
    */
    export function evaluateConditions(conditions, formData) {
      if (!conditions) return true;
    
      if (conditions.and) {
        return conditions.and.every((cond) => evaluateCondition(cond, formData));
      }
    
      if (conditions.or) {
        return conditions.or.some((cond) => evaluateCondition(cond, formData));
      }
    
      return evaluateCondition(conditions, formData);
    }
    
    /**
     * 根据条件判断是否展示该字段
    * @param condition
    * @param formData
    * @returns Boolean
    * @description 解析单个条件表达式
    */
    function evaluateCondition(condition, formData) {
      const { field, operator, value } = condition;
      const fieldValue = getNestedValue(formData, field);
    
      switch (operator) {
        case "==":
          return fieldValue == value;
        case "!=":
          return fieldValue != value;
        case ">":
          return fieldValue > value;
        case "<":
          return fieldValue < value;
        case "includes":
          return (fieldValue || "").includes(value);
        case "in":
          return (value || []).includes(fieldValue);
        default:
          return false;
      }
    }
    
    /**
     * @param obj
    * @param path
    * @returns value
    * @description 通过路径获取嵌套对象的值,支持多层嵌套
    */
    function getNestedValue(obj, path) {
      return path.split(".").reduce((o, p) => o?.[p], obj);
    }
    
  3. dataLinkage.ts

    export function applyLinkage(linkage, sourceValue, formData) {
      if (!linkage) return null;
    
      switch (linkage.type) {
        case "direct":
          return sourceValue;
    
        case "mapping":
          return linkage.mapping[sourceValue] || "";
    
        case "function":
          return linkage.fn(sourceValue, formData);
    
        case "concatenate":
          return linkage.fields
            .map((f) => formData[f] || "")
            .join(linkage.separator || " ");
    
        default:
          return null;
      }
    }
    
  4. formRules.ts

    import { FormItemRule } from "element-plus";
    import { FormData, FormField } from "../formTypes";
    import { evaluateConditions } from "./conditionParser";
    
    /**
     * @param fieldSchema
    * @param formData
    * @returns rules
    * @description 获取表单字段的校验规则
    */
    export function getValidationRules(fieldSchema:FormField, formData: FormData): FormItemRule[] {
      let rules: any = [];
    
      // 基础必填规则
      if (fieldSchema.required) {
        rules.push({ required: true, message: fieldSchema.requiredMessage || `${fieldSchema.label}不能为空`, trigger: 'blur' });
      }
    
      // 静态规则
      if (fieldSchema.rules) {
        if (fieldSchema.rules.pattern) {
          rules.push({ pattern: fieldSchema.rules.pattern, message: fieldSchema.rules.message || `${fieldSchema.label}格式不正确`, trigger: 'blur' });
        }
      }
    
      // 动态规则
      if (fieldSchema.dynamicRules) {
        fieldSchema.dynamicRules.forEach((ruleDef) => {
          if (!ruleDef.conditions || evaluateConditions(ruleDef.conditions, formData)) {
            rules.push({...ruleDef.rule, trigger: ruleDef?.trigger || 'blur' });
          }
        });
      }
    
      return rules;
    }
    
  5. DynamicForm.vue

    <template>
        <el-form ref="ruleFormRef" :model="formData" label-position="top" @submit.prevent="handleSubmit"
            :scroll-to-error="true" class="dynamic-form">
            <el-form-item v-for="field in visibleFields" :key="field.name" :label="field.label" :prop="field.name"
                :rules="getValidationRules(field, formData)">
                <FormField :field="field" v-model="formData[field.name]" :form-data="formData" />
            </el-form-item>
    
            <el-form-item>
                <el-button type="primary" native-type="submit">提交</el-button>
            </el-form-item>
    
        </el-form>
    </template>
    
    <script setup lang="ts">
    import { ElMessage } from 'element-plus';
    import { FormInstance } from 'element-plus/es/components/form';
    import { computed, ref, watch } from 'vue';
    import type { FormData as FormDataType, FormField as FormFieldType } from '../formTypes';
    import { evaluateConditions } from '../utils/conditionParser';
    import { applyLinkage } from '../utils/dataLinkage';
    import { getValidationRules } from '../utils/formRules';
    import FormField from './FormField.vue';
    
    const props = defineProps<{
        formSchema: FormFieldType[]
    }>();
    
    const emit = defineEmits<{
        (e: 'submit', formData: FormDataType): void
    }>();
    const ruleFormRef = ref<FormInstance>();
    const formData = ref<FormDataType>({});
    
    // 初始化表单数据
    const initializeFormData = () => {
        props.formSchema.forEach(field => {
            formData.value[field.name] = field.defaultValue || '';
        })
    }
    
    // 计算可见字段
    const visibleFields = computed(() => props.formSchema.filter(field => !field.conditions ? true : evaluateConditions(field.conditions, formData.value)));
    
    // 表单提交
    const handleSubmit = async () => {
        // 验证所有可见字段
        if (!ruleFormRef.value) return
        ruleFormRef.value.validate((valid) => {
            if (valid) {
                console.log('表单提交成功:', formData.value);
                emit('submit', formData.value);
            } else {
                ElMessage.error('表单验证失败');
            }
        })
    }
    
    // 初始化
    initializeFormData();
    
    // 监听表单数据变化,处理联动
    watch(formData, (newVal) => {
        props.formSchema.forEach(field => {
            if (field.linkage && field.linkage.type === 'concatenate') {
                formData.value[field.name] = applyLinkage(field.linkage, null, newVal);
            }
        })
    }, { deep: true });
    
    defineExpose({
        formData,
        handleSubmit
    });
    </script>
    
    <style scoped>
    .dynamic-form {
        max-width: 600px;
        margin: 0 auto;
    }
    
    .form-data {
        margin-top: 20px;
    }
    
    pre {
        white-space: pre-wrap;
        word-wrap: break-word;
    }
    </style>
    
  6. FormField.vue

    <template>
        <el-input v-if="field.type === 'text'" v-model="formData[field.name]" :placeholder="`请输入${field.label}`" />
    
        <el-input-number v-else-if="field.type === 'number'" v-model="formData[field.name]"
            :placeholder="`请输入${field.label}`" />
    
        <el-select v-else-if="field.type === 'select'" v-model="formData[field.name]" :placeholder="`请选择${field.label}`">
            <el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" />
        </el-select>
    
        <!-- 扩展... -->
    </template>
    
    <script setup lang="ts">
    import { defineEmits, defineProps } from 'vue';
    import type { FormData, FormField } from '../formTypes';
    
    const emit = defineEmits<{ (e: 'update:modelValue', value: any): void }>();
    defineProps<{
        field: FormField
        modelValue: any
        formData: FormData
    }>();
    </script>
    
    <style scoped>
    .error-message {
        margin-top: 5px;
    }
    </style>
    
  7. test.vue

    <template>
        <div class="dynamic-form">
            <h1>动态表单演示</h1>
            <section style="height: calc(100% - 50px)">
                <el-scrollbar>
                    <DynamicForm ref="dynamicFormRef" :formSchema="formSchema" @submit="submitHandler" />
                </el-scrollbar>
                <el-scrollbar>
                    <el-card class="form-data">
                        <template #header>
                            <h3>当前表单数据:</h3>
                        </template>
                        <pre>{{ dynamicFormRef?.formData }}</pre>
                    </el-card>
                </el-scrollbar>
            </section>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ElMessage } from 'element-plus';
    import DynamicForm from './components/DynamicForm.vue';
    import type { FormField } from './formTypes';
    
    const dynamicFormRef = ref();
    const formSchema = ref<FormField[]>([
        {
            name: 'userType',
            label: '用户类型',
            type: 'select',
            defaultValue: 'personal',
            options: [
                { value: 'personal', label: '个人用户' },
                { value: 'business', label: '企业用户' }
            ],
            validateOnChange: true,
        },
        {
            name: 'companyName',
            label: '公司名称',
            type: 'text',
            conditions: {
                field: 'userType',
                operator: '==',
                value: 'business'
            },
            required: true
        },
        {
            name: 'idNumber',
            label: '证件号码',
            type: 'text',
            required: true,
            dynamicRules: [
                {
                    conditions: { field: 'userType', operator: '==', value: 'personal' },
                    rule: { pattern: /^\d{17}[\dXx]$/, message: '请输入有效的身份证号码', trigger: 'blur' }
                },
                {
                    conditions: { field: 'userType', operator: '==', value: 'business' },
                    rule: { pattern: /^[A-Z0-9]{18}$/, message: '请输入有效的统一社会信用代码', trigger: 'blur' }
                }
            ]
        },
        {
            name: 'email',
            label: '电子邮箱',
            type: 'text',
            required: true,
            rules: {
                pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                message: '请输入有效的邮箱地址'
            }
        },
        {
            name: 'confirmEmail',
            label: '确认电子邮箱',
            type: 'text',
            required: true,
            dynamicRules: [
                {
                    rule: {
                        validator: (rule, value, callback) => {
                            return value === dynamicFormRef.value.formData.email ? callback() : callback(new Error('邮箱地址不匹配'));
                        },
                    }
                }
            ]
        },
        {
            name: 'province',
            label: '省份',
            type: 'text'
        },
        {
            name: 'city',
            label: '城市',
            type: 'text'
        },
        {
            name: 'fullAddress',
            label: '完整地址',
            type: 'text',
            linkage: {
                type: 'concatenate',
                fields: ['province', 'city'],
                separator: ' '
            }
        }
    ]);
    
    const submitHandler = (formData) => {
        ElMessage.success('表单提交成功! 查看控制台输出数据!');
        // 这里可以进行进一步的处理,比如发送到服务器等
    };
    </script>
    
    <style lang="scss" scoped>
    .dynamic-form {
        height: 100%;
        margin: 0 100px;
        padding: 20px;
    
        section {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
    }
    
    .error-message {
        color: red;
        font-size: 0.8em;
        margin-top: 4px;
    }
    
    .form-field {
        margin-bottom: 15px;
    }
    
    label {
        display: block;
        margin-bottom: 5px;
    }
    
    input,
    select {
        width: 100%;
        padding: 8px;
        box-sizing: border-box;
    }
    
    button {
        padding: 10px 15px;
        background: #42b983;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
    }
    
    button:hover {
        background: #3aa876;
    }
    </style>
    

演示

源码下载

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 实现功能
  2. 2. 项目结构
  3. 3. 示例代码
  4. 4. 演示
  5. 5. 源码下载