工作流配置页 Badge 数字对不上:分子分母来自不同候选集
产品说「设置页的字段统计数字看起来不对」。一看,Badge 写着「本节点需填写字段 9/67」,但页面上可点击的按钮只有 4 个。打开实际的审批填写页,红色必填项又变成了 5 个。
三个数字(4 / 9 / 5),全部错误,全部来自不同的来源。
现象
一个多阶段审批工作流的节点配置界面:
[本节点需填写字段 9/67] ← Badge
──────────────────────────
[✓] 审核主类
[✓] 审核分类
[✓] 公司名称
[✓] 备注
(页面上只有 4 个可点击的按钮)
打开对应的审批表单,出现了 5 个红色的「必填」输入框,其中有一个「流水号」字段——而流水号明明是系统在前置节点自动生成的,根本不需要用户手填。
根因
三处计算各用了一套不同的过滤规则:
- 分母 67:做了过滤——剔除了前置节点自动生成的字段(
priorAutoKeys)、当前节点 auto 字段、可选字段,只统计「合法可设为必填」的候选集 - 分子 9:裸用
node.requiredFields.length——包含了 1 个本应被前置 auto 抑制的字段(serialNo/流水号),以及 4 个field_key引用的行已经被软删除的孤儿字段 - 按钮列表 4 个:过滤了孤儿字段,但没有过滤前置 auto 抑制字段,恰好差 1
- 审批表单 5 个:又是另一套逻辑,没有经过 auto 抑制
只要 UI 里存在「X/Y」形式的计数 Badge,分子和分母就必须来自同一份候选集的 .filter() 结果,而不是各自独立计算。任何一处使用 xxxFields.length 裸读数组长度,就埋下了漂移的种子——软删除累积、新增前置 auto 字段、字段移动节点,任何一次配置变更都会让三个数字再次错开。
正解
一个函数,三个输出——分子、分母、渲染列表都从同一次候选集计算里取:
// field-badge-stats.ts
export function computeFieldCandidateStats(input: {
nodeAvailableFields: FieldCandidateRef[]
requiredFields: string[] | undefined
optionalFields: string[] | undefined
autoFields: FieldCandidateAutoRef[] | undefined
}) {
const required = new Set(input.requiredFields || [])
const optional = new Set(input.optionalFields || [])
const auto = new Set((input.autoFields || []).map((af) => af.fieldKey))
// 候选规则:可用字段中,去掉 auto 和 optional,剩下才能设为 required
const reqCandidates = input.nodeAvailableFields.filter(
(f) => !auto.has(f.key) && !optional.has(f.key)
)
return {
requiredSelected: reqCandidates.filter((f) => required.has(f.key)).length, // 分子
requiredTotal: reqCandidates.length, // 分母
reqCandidates, // 渲染列表
}
}Badge 和按钮都从这个函数的返回值取:
// Badge 用 stats
<Badge>本节点需填写字段 {stats.requiredSelected}/{stats.requiredTotal}</Badge>
// 按钮列表也用同一份 reqCandidates
{stats.reqCandidates.map((f) => (
<Button selected={required.has(f.key)}>{f.title}</Button>
))}前置节点 auto 字段必须向下抑制
另一个独立的坑:节点 A 把「流水号」声明为 autoFields(系统生成),但节点 B 的 requiredFields 里又把它列了一遍。渲染时就出现「请填写流水号」的红色必填框,但用户根本没有输入权限。
修法:在解析当前节点字段配置时,先收集所有前置节点的 auto key,再用它过滤掉当前节点的 required/optional 声明:
// workflow-node-fields.ts
const priorAutoKeys = new Set<string>()
for (let i = 0; i < nodeIdx; i++) {
for (const af of nodes[i].autoFields || []) {
priorAutoKeys.add(af.fieldKey)
}
}
const allAutoKeys = new Set([...priorAutoKeys, ...currentAutoKeys])
const requiredFields = (node.requiredFields || []).filter((k) => !allAutoKeys.has(k))经过这个变换后,审批表单和配置设置页看到的是同一份过滤后的视图,前置 auto 字段不再出现在任何下游节点的必填/可选列表中。
把 Badge 的 numerator/denominator 和渲染列表全部收进一个 computeCandidateStats 函数,三个输出从同一次过滤结果中取。前置 auto 字段的抑制逻辑放在 resolveCurrentNodeFieldConfig 里做一次,所有消费点(设置页 + 审批页)共享这个 SSoT,从根本上断掉漂移路径。
排查提示
想快速定位项目里是否还存在同类问题:
grep -rn "Fields.length" src/ | grep -v ".test."凡是看到 {required.length}/{total.length} 而 total 在另一处独立计算的,就是潜在的漂移点。
一句话外卖
UI 里任何「X/Y」计数器,分子和分母必须从同一份候选集的
.filter()里取,裸读数组.length是漂移的起点。