实现功能
- 条件渲染(字段的展示隐藏)
- 动态验证(必填、正则、自定义校验)
- 数据联动(拼接、拆分、包含、…)
- 类型安全(TypeScript)
项目结构
/src/dynamicForm
/components
DynamicForm.vue
FormField.vue
FormValidator.vue
/utils
conditionParser.ts
dataLinkage.ts
formRules.ts
formTypes.ts
index.vue
示例代码
-
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>;
-
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); }
-
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; } }
-
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; }
-
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>
-
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>
-
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>
演示
源码下载
前端接口防止重复请求
上一篇