【论术】计算大批量复杂表单实践
你越是愿意认识到自己的脆弱,就越不会害怕。 ---洛莉·戈特利布
本月在写计算类的功能需求。
针对不仅需要实时计算且要求实时检验的需求,依靠后端是不现实的。不可能通过用户输入一次随即调后端计算,当前场景下后端需要做的仅仅是保存用户须提交时当时的数据,这也表示针对用户输入的数据前端都需要自己来算,也是基于此,不仅要保证数据的准确性,也要保证数据的严密校验,在完成需求的基础上尽可能的将函数封装成公用的,即便是偷懒也要有意义的偷懒。
然而,前端处理数据计算时也有显著的问题:
-
用户输入数据不可控
-
前端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号