【论术】计算大批量复杂表单实践
你越是愿意认识到自己的脆弱,就越不会害怕。 ---洛莉·戈特利布
本月在写计算类的功能需求。
针对不仅需要实时计算且要求实时检验的需求,依靠后端是不现实的。不可能通过用户输入一次随即调后端计算,当前场景下后端需要做的仅仅是保存用户须提交时当时的数据,这也表示针对用户输入的数据前端都需要自己来算,也是基于此,不仅要保证数据的准确性,也要保证数据的严密校验,在完成需求的基础上尽可能的将函数封装成公用的,即便是偷懒也要有意义的偷懒。
然而,前端处理数据计算时也有显著的问题:
- 
用户输入数据不可控 
- 
前端js精度容易丢失 
- 
联动计算容易出现意外值 
鉴于上述问题,便须对整个流程都有清晰的把控
整个流程为: 初始化数据回填 -> 用户输入强校验 -> 数据联动 -> 提交回填联动值。
- 数据回填校验
在数据回填或者输入数据时,要保证数据的准确性, 对于后端要求只允许传入字符类的数字,如果不合法的数据则以0兜底。
// 通用的计算函数,用于将字符格式的number转换为真实nunber,如果是 NAN返回0
const checkNumber = (str) => {
  // console.log("初值 :>> ", str);
  // console.log("Number(str) :>> ", Number(str));
  // console.log("Number.isNaN(Number(str)) :>> ", Number.isNaN(Number(str)));
  // console.log("object :>> ", Number.isNaN(Number(str)) ? 0 : Number(str));
  return Number.isNaN(Number(str)) ? 0 : Number(str);
};
在渲染数据的时候,基于后端传来的数据初值可能为null或者为空字符的格式,要在SET回填值的时候进行二次检验,如果是数额类的不合法值则应赋0
const filterForm = (form) => {
  let baseObj = {};
  for (const key in form) {
    if (form[key] === null  || form[key] === '') {
      baseObj[key] = "0";
    } else {
      baseObj[key] = form[key];
    }
  }
  return baseObj;
};
实例:
// 设置数据回填时,添加一次校验 
     setFormData(
        filterForm({
          ...auditForm,
          ...pageFormInstanceDetail.value,
        })
      );
- 用户输入强校验
此处有两种模式,一种是允许有校验结果的展示,另一种则是没有校验结果的展示。
有校验结果的展示的情况下,可以根据formRules来根据输入值来判断:
//检验数字是否合法: 两位内小数,等等等等
export function validateNumber(input) {
  const trimmed = input.trim();
  if (trimmed === "") return false;
  const regex = /^(?:0(?:\.\d{1,2})?|[1-9]\d*(?:\.\d{1,2})?)$/;
  return regex.test(trimmed);
}
export const validateVal = {
  validator: validateNumber,
  trigger: "change",
  message: "请输入合法值",
};
// 批量生成校验字段
export const genereterValiRule = (bool) => {
  let result = {};
  for (const key in valitObjVals) {
    result[key] = [
      {
        required: bool,
        message: `请输入${valitObjVals[key]}`,
        trigger: "blur",
      },
      validateVal,
    ];
  }
  return result;
};
本质上是通过formRules的规则,根据input的blur/change动作实时校验数据的合法性。
在没有校验结果展示的情况下,则需要另外想办法处理,此时就要根据input本身的输入事件来进行数据约束,较粗暴
在大批量数据场景下,就须封装成公共指令来做(网上看来的,比较合适就拿来用了)
// 指令 ipt输入校验
const vFormatNumber = {
  mounted(el, binding) {
    el.addEventListener("input", (event) => {
      let newVal = event.target.value;
      const precision = binding.value.precision;
      newVal = newVal.replace(/[^\d.]/g, ""); // 能数字和小数点
      newVal = newVal.replace(/^\./g, ""); // 去掉开头的点
      newVal = newVal
        .replace(".", "$#$")
        .replace(/\./g, "")
        .replace("$#$", "."); // 处理多个点的情况
      if (precision && Number(precision) > 0) {
        const d = new Array(Number(precision)).fill("\\d").join(""); // 构建正则表达式
        const reg = new RegExp(`^(\\-)*(\\d+)\\.(${d}).*$`, "ig");
        newVal = newVal.replace(reg, "$1$2.$3"); // 限制小数位数
      }
      if (newVal && !newVal.includes(".")) {
        newVal = newVal.replace(/^0+/, "0"); //
      }
      // 更新输入框的值
      event.target.value = newVal;
      // 同步 v-model 的值
      el.querySelector("input").dispatchEvent(new Event("input"));
    });
  },
};
// 实例:
     <t-input
          v-format-number="{ precision: 2 }"
           maxlength="16"
    />
使用此指令,所在的input实例将仅允许用户输入数字以及小数点(一次)
- 表单计算(数据联动)
 在某些复杂表单场景中,会存在输入了a,则b自动计算,从而c自动计算,则d自动计算,因而导致整个表单的终值发生变动,在更复杂的计算中,甚至会导致n多个值发生变动。
而在最开始做此类表单时,通常的做法是根据某个输入框的change动作从而触发setOtherValue的函数,但这种方式不适合复杂表单,如果在大批量表单的场景下,光是change事件就得写几百行,即便可以确保set的结果绝对不会错,也会导致可读性差,而且容易导致打地鼠的情况,这并不合理(对于大批量数据表单而言)。
此种情况下,则更适合使用计算属性 + 计算函数联合处理。
以下是该方式的实践。
定义公共计算函数:
// 通用的计算函数,用于将字符格式的number转换为真实nunber,如果是 NAN返回0
const checkNumber = (str) => {
  // console.log("初值 :>> ", str);
  // console.log("Number(str) :>> ", Number(str));
  // console.log("Number.isNaN(Number(str)) :>> ", Number.isNaN(Number(str)));
  // console.log("object :>> ", Number.isNaN(Number(str)) ? 0 : Number(str));
  return Number.isNaN(Number(str)) ? 0 : Number(str);
};
/*  公共计算函数  resourceA,resourceB默认是在formData中的key,
type区分为 add/sub/mul/div  加减乘除
*/
const publicCalc = (resourceA, resourceB, type = "add") => {
// 判断数据是否在formData中
  const includesInFormData =
    typeof resourceA === `string` && typeof resourceB === `string`;
  const DataA = includesInFormData
    ? checkNumber(formData[resourceA])
    : checkNumber(resourceA);
  const DataB = includesInFormData
    ? checkNumber(formData[resourceB])
    : checkNumber(resourceB);
  switch (type) {
    case "add":
      return DataA + DataB;
    case "sub":
      return (DataA * 10000 - DataB * 10000) / 10000; // 规避js精度问题 分别将数据扩大N倍再相减,最后将值再缩小对应n倍
    case "mul":
      return DataA * DataB;
    case "div":
      return DataB === 0 ? 0 : DataA / DataB; // DataB不可以为0
  }
};
// 公共加函数 
const publicPlus = (...params) => {
  const list = params;
  console.log("list :>> ", list);
  return list.reduce((last, pre) => (last += checkNumber(pre)), 0);
};
在拿到后端返回的数据后,由于最开始便使用
filterForm过滤了初值,所以初值是合法的输入值,针对输入的变动,使用vue指令严格约束数据的输入,因而也不会有不合法数存在,而最后则是对计算数据的处理。
在大批量数据场景下,可以直接根据计算属性来做:
// 约定计算函数的名字为原有值 + `calc`
<t-col :span="3" class="flex_center h46 borderR borderB">
     <!-- {{
                    formData.travelExpensesApproved  
                  }}自动计算 -->
    {{ travelExpensesApprovedCalc }}
 </t-col>
    <div class="borderL borderB h46">扣减额</div>
                  <div class="borderL borderB h46">
                    <!-- {{ formData.deductionItems || 0 calc }} -->
                    {{ deductionItemsCalc }}
 </div>
// -----------------------------------------计算逻辑
// 计算属性  使用publicCalc进行数据相减
const travelExpensesApprovedCalc = computed(() => {
  return publicCalc(`travelExpenses`, `travelExpensesFeedback`, `sub`);
});
const deductionItemsCalc = computed(() => {
  // const valueAddedTax = checkNumber(+formData.valueAddedTax);
  // const landCost = checkNumber(+formData.landCost);
  // const interestApproved = checkNumber(+formData.interestApproved);
  // const otherItems = checkNumber(+formData.otherItems);
  // return valueAddedTax + landCost + interestApproved + otherItems;
  return publicPlus(
    formData.valueAddedTax,
    formData.landCost,
    formData.interestApproved,
    formData.otherItems
  );
});
- 提交设置联动值
而在保存动作中,只需将对应的计算属性数据保存在对应的值里,然后发给后端即可:
    // 设置值,随即发给后端
    setFormData({
      ...formData,
      deductionItems: deductionItemsCalc.value,
      ...other values 
    });
    await apis.xx.xxxApi({
      ...formData,
      id ,
    });
上述就是此类计算表单的实现流程,总的来说需要对整个界面的输入流程进行精密控制,确保流程可控,且可读性高,避免后期维护吃力。
以上。
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号