
生成式 UI 表单校验契约动态联动规则与 Zod/JSON Schema 双向绑定在企业级中后台、政企审批流以及低代码搭建平台中动态表单生成Dynamic Form Generation一直是生成式 UIGenerative UI最具商业价值的杀手级场景。业务人员输入一句“帮我生成一个支持跨境电商报关的申报表单要求企业用户填报海关代码个人用户仅需护照”大模型瞬间就能输出几十个字段的输入结构。然而表单生成的“快乐”往往止步于点击提交的那一刻。前端工程师面对的真实业务表单从来不是若干个输入框的简单堆叠而是充斥着错综复杂的条件联动Conditional Dependencies与动态校验规则当用户将“注册类型”切换为“企业”下方的“统一社会信用代码”必须从隐藏变为显示且必填校验规则动态生效当“申报金额”超过 5000 美元时必须动态追加“外汇审批凭单号”字段同时触发正则合法性校验。很多初级实现为了图省事允许大模型在 JSON 里返回诸如visibleWhen: model.type company这样的内联 JavaScript 代码字符串然后在前端使用eval或new Function动态求值。这不仅直接撕开了灾难性的 XSS 安全口子而且在大规模表单重排时会让主线程性能彻底崩溃。打造工业级的生成式表单体系核心在于建立一套安全、声明式、支持双向绑定的动态校验协议Validation Contract。声明式联动协议设计解耦代码执行与逻辑表达我们要绝对禁止任何动态执行字符串的注入将所有联动逻辑降维为可被静态解析的声明式表达式结构。我们使用 Zod 来构建这份强类型的协议蓝图import{z}fromzod;// 基础比较操作符枚举exportconstConditionOperatorEnumz.enum([equals,notEquals,greaterThan,lessThan,in,notIn]);// 单个联动触发条件声明exportconstVisibilityConditionSchemaz.object({targetField:z.string().describe(依赖的目标字段名),operator:ConditionOperatorEnum.describe(比较操作符),expectedValue:z.union([z.string(),z.number(),z.boolean(),z.array(z.string())]),});// 动态字段元数据契约exportconstDynamicFormFieldSchemaz.object({name:z.string().min(1),label:z.string().min(1),component:z.enum([Input,Select,NumberInput,DatePicker,Switch]),defaultValue:z.any().optional(),placeholder:z.string().optional(),// 选项数据源针对下拉选框options:z.array(z.object({label:z.string(),value:z.union([z.string(),z.number()])})).optional(),// 核心声明式联动规则满足该条件时当前字段才可见visibleCondition:VisibilityConditionSchema.optional(),// 动态校验规则必填与正则约束validation:z.object({required:z.boolean().default(false),requiredWhen:VisibilityConditionSchema.optional(),// 满足条件时才动态变为必填pattern:z.string().optional().describe(正则表达式字符串),errorMessage:z.string().optional(),}).default({required:false}),});exportconstDynamicFormSchemaz.object({formId:z.string(),title:z.string(),fields:z.array(DynamicFormFieldSchema),});exporttypeDynamicFormFieldz.infertypeofDynamicFormFieldSchema;exporttypeDynamicFormz.infertypeofDynamicFormSchema;请仔细注意VisibilityConditionSchema的设计它完全由纯数据组成。大模型只能组合使用合法的操作符如equals、in和目标字段名从根源上杜绝了任何脚本注入的可能性。安全的客户端条件求值器在客户端我们编写一个零依赖、零eval的纯函数求值器负责在表单数据流转时纳秒级判断某个字段是否应当显示或必填exportclassFormConditionEvaluator{// 核心求值入口staticisConditionMet(condition:z.infertypeofVisibilityConditionSchema|undefined,formData:Recordstring,any):boolean{if(!condition)returntrue;// 无条件则默认满足constactualValueformData[condition.targetField];constexpectedcondition.expectedValue;switch(condition.operator){caseequals:returnactualValueexpected;casenotEquals:returnactualValue!expected;casegreaterThan:returntypeofactualValuenumbertypeofexpectednumberactualValueexpected;caselessThan:returntypeofactualValuenumbertypeofexpectednumberactualValueexpected;casein:returnArray.isArray(expected)expected.includes(actualValue);casenotIn:returnArray.isArray(expected)!expected.includes(actualValue);default:returnfalse;}}}没有动态作用域查找没有 AST 解释开销即便表单包含上百个联动字段单次全量条件判定的耗时也严格控制在 0.1 毫秒以内。Vue 3 动态表单组件的响应式绑定与脏数据清理在表单呈现层最容易被忽视的隐患是**“隐藏字段的脏数据残留”**当用户选了“企业”填了税号随后又后悔改成了“个人”。此时税号输入框虽然在界面上隐藏了但响应式表单对象里依然残留着刚才输入的税号。如果不加处理直接提交给后端就会引发严重的业务脏数据污染。我们实现一个具备隐藏自愈清理机制的动态表单驱动器template form submit.preventhandleSubmit classdynamic-form-wrapper h3 classform-title{{ schema.title }}/h3 div v-forfield in activeFields :keyfield.name classform-item-row label :class{ is-required: isFieldRequired(field) } {{ field.label }} /label !-- 动态原子输入组件挂载 -- input v-iffield.component Input v-modelformModel[field.name] :placeholderfield.placeholder classform-control / select v-else-iffield.component Select v-modelformModel[field.name] classform-control option v-foropt in field.options :keyopt.value :valueopt.value {{ opt.label }} /option /select !-- 行内错误信息呈现 -- span v-ifvalidationErrors[field.name] classerror-tip {{ validationErrors[field.name] }} /span /div button typesubmit classsubmit-btn立即提交/button /form /template script setup langts import { reactive, computed, watch } from vue; import { type DynamicForm, type DynamicFormField } from ./schema; import { FormConditionEvaluator } from ./evaluator; const props defineProps{ schema: DynamicForm; }(); const emit defineEmits{ (e: submit, validData: Recordstring, any): void; }(); // 响应式表单存储底座 const formModel reactiveRecordstring, any({}); const validationErrors reactiveRecordstring, string({}); // 初始化默认值 props.schema.fields.forEach((field) { formModel[field.name] field.defaultValue ?? ; }); // 计算当前满足可见性条件的活跃字段子集 const activeFields computed(() { return props.schema.fields.filter((field) { return FormConditionEvaluator.isConditionMet(field.visibleCondition, formModel); }); }); // 动态必填判定 function isFieldRequired(field: DynamicFormField): boolean { if (field.validation.required) return true; return FormConditionEvaluator.isConditionMet(field.validation.requiredWhen, formModel); } // 核心防御当某字段从可见变为隐藏时自动擦除其数据与校验报错防止脏数据泄漏 watch(activeFields, (newActiveList) { const activeNameSet new Set(newActiveList.map(f f.name)); props.schema.fields.forEach((field) { if (!activeNameSet.has(field.name)) { // 字段已被隐藏主动清理残留数据 if (formModel[field.name] ! undefined) { formModel[field.name] field.defaultValue ?? ; } delete validationErrors[field.name]; } }); }, { deep: true }); function handleSubmit() { // 清理上一轮报错 Object.keys(validationErrors).forEach(key delete validationErrors[key]); let hasError false; // 仅对当前可见的活跃字段执行校验 activeFields.value.forEach((field) { const value formModel[field.name]; const isReq isFieldRequired(field); if (isReq (value || value null || value undefined)) { validationErrors[field.name] ${field.label}不能为空; hasError true; return; } if (field.validation.pattern value) { const reg new RegExp(field.validation.pattern); if (!reg.test(String(value))) { validationErrors[field.name] field.validation.errorMessage || 格式不正确; hasError true; } } }); if (!hasError) { // 仅提取可见字段的清洁数据向外派发 const cleanPayload: Recordstring, any {}; activeFields.value.forEach(f { cleanPayload[f.name] formModel[f.name]; }); emit(submit, cleanPayload); } } /script style scoped .dynamic-form-wrapper { max-width: 540px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; } .form-item-row { margin-bottom: 16px; display: flex; flex-direction: column; gap: 6px; } .form-control { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; } .is-required::after { content: *; color: #ff4d4f; } .error-tip { font-size: 12px; color: #ff4d4f; } .submit-btn { padding: 10px 20px; background: #1677ff; color: white; border: none; border-radius: 4px; cursor: pointer; } /style两项生产落地的架构避坑指南防范深层级联引发的循环依赖Dependency Loops在大模型生成复杂的四五层级联时可能会发生 A 依赖 BB 依赖 CC 反向依赖 A 的荒唐闭环。在解析大模型返回的 Schema 时必须在前端运行时先对所有的targetField运行一次有向无环图DAG拓扑检测一旦发现闭环强制将后置依赖断开防止computed触发无限响应式递归。正则表达式的安全防线ReDoS 拦截大模型偶尔会输出存在回溯炸弹的低效正则表达式如^(a)$。在前端将字符串转为new RegExp()之前应当使用轻量级的正则安全性检测工具或者对正则匹配过程设置超时限制防止用户在输入一个特定字符串时瞬间锁死浏览器主线程。用确定的数据结构驯服不确定的生成内容把复杂的联动逻辑沉淀为干净的声明式契约。这才是前端手艺人在生成式 UI 时代不可替代的工程壁垒。