返回博客

工作流配置页 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 是漂移的起点。