基于form-generator的复杂表单引擎设计挑战
Situation(情境)
在为我们“产品规划部门”开发市场调研分析平台时,业务方要求实现动态配置1000+问题项数据申报表单,需满足:
业务人员通过可视化界面自主搭建表单(类似form-generator设计理念)
- 支持跨表单级联校验(如A字段值影响B字段的可视化规则)
- 实现毫秒级实时预览,配置变更后立即渲染效果
- 实现计算功能,某项可通过其他几项配置公式实时计算
- 兼容移动端对问卷提交
原form-generator开源方案存在以下局限性:
- 无逻辑控制功能,无法关联字段控制显示
- 无公式配置功能,无法实现通过关联字段实现自动计算结果
- 只能编辑表单,生成json sechme,无法预览
Task(任务)
作为前端负责人,需要完成:
- 设计跨表单通信协议,实现字段级联动
- 开发沙箱预览系统,隔离配置与运行环境差异
- 开发公式配置功能,实现关联字段自动计算功能
Action(行动)
- 通过开发逻辑配置界面,实现给某些题型增加显示控制逻辑,在渲染表单时,通过遍历表单item项,找出itemId所控制的logic
// 逻辑控制函数 logicShowHandle(value, field, item) { seqNo = this.formConfCopy.startSeqNo || 0 // 值被包装过 需要取出原始值 const { logicShowRule } = this.formConfCopy if (!logicShowRule) { return } // 找到该问题需要触发显示的问题 判断逻辑是否成立 const rules = _.get(logicShowRule, field) if (rules && Array.isArray(rules)) { rules.forEach((r) => { // 成立让该对应的问题显示出来 const flag = evalExpression(this.formModel, r.logicExpression) if (flag) { // 显示加上值必填等校验 let triggerFormItem = this.formConfCopy.fields[this.fieldIdIndex[r.triggerFormItemId]] // 重新渲染组件 不然必填红点不显示 if (triggerFormItem) { triggerFormItem.key = new Date().getTime() this.buildRule(triggerFormItem, this.formRules) // 设置默认值 this.$set(this.formModel, r.triggerFormItemId, triggerFormItem.config.defaultValue) } // 防止表单重新渲染 display被刷新 this.logicTriggerItemList.push(r.triggerFormItemId) let triggerEle = document.querySelector(`div[cid="${r.triggerFormItemId}"]`) if (triggerEle) { triggerEle.style.display = '' } } else { _.remove(this.logicTriggerItemList, (n) => n === r.triggerFormItemId) // 移除表单的必填校验 let triggerFormItem = this.formConfCopy.fields[this.fieldIdIndex[r.triggerFormItemId]] if (triggerFormItem) { this.removeRule(triggerFormItem) } // 移除已经填写的值 delete this.formModel[r.triggerFormItemId] let triggerEle = document.querySelector(`div[cid="${r.triggerFormItemId}"]`) if (triggerEle) { triggerEle.style.display = 'none' } } }) } }
/** * 执行表达式是否成立 */ export function evalExpression(context, expression) { const exArray = expression.split(/and|or/) // 获取是& 还是| const and = expression.indexOf('and') > -1 let flag = false // console.log(exArray) for (let i = 0; i < exArray.length; i++) { const itemExpArr = exArray[i].trim().split(' ') // console.log(itemExpArr) // 截取字段名 const varName = itemExpArr[0] // 条件 等于 不等于 const sp = itemExpArr[1].trim() // 值 const value = itemExpArr[2] // 比较是否成立 // console.log(varName) const fieldValue = _.get(context, varName) // console.log(fieldValue) flag = expressionOperator[sp](fieldValue, value) // console.log(flag) // & 一个不成立直接调出循环 返回失败 if (and && !flag) { break // | 一个成立直接调出循环 返回成功 } else if (!and && flag) { break } } return flag }
- 通过Proxy + iframe实现隔离环境,实现编辑完成即可预览
const sandbox = new Proxy(window, { get(target, key) { return key in target ? target[key] : previewWindow[key]; } });
- 通过给item设置公式表达式,在初始化表单时,遍历找出绑定了公式及公式计算所需要值的item项,watch所有的form value如果匹配到计算项的值发生变化,使用正则替换掉公式对应的插值,使用用户填入的值,然后在利用mathjs的数学计算功能,将表达式提交置入mathjs evaluate方法中,从而得到正确的值,并赋值对应的item项,完成自动计算功能
Result(结果)
业务价值
- 配置人员效率提升300%,新建表单平均耗时从3天缩短至4小时
- 业务部门的市场调研报告生成效率从原来的 收集数据 -> 建立表单 -> 手动计算关联项 -> 手动撰写生成报告,转变配置表单项、现场调研问答、提交数据、一键生成报告(结合大模型总结调研的数据的趋势信息),报告生成周期由原来的15-20天提升至5天。
留言