级联字段靠配置行识别,不靠 type 字段——一次多节点审批表单的踩坑实录
在一个后台管理系统里,表单字段支持多级级联下拉(省市区、职称系列-评审类别-评审专业等)。某次迭代修了一处级联 bug,验收通过。发版后用户反馈"另一套级联也坏了"——两个级联树用的是完全相同的渲染路径,为什么只修了一个?
排查发现,根因不在代码逻辑,而在对"什么是级联字段"的识别方式完全错了。
现象
- 修复某三级级联选择器(A 树)后上线,A 树正常。
- 同一个表单里的另一套三级级联(B 树:省/市/区)随即出现子级永远禁用、提示"请先选择上级字段"。
- 在多节点审批流程中,节点 2 的表单打开后,级联子字段全部禁用,但父字段的值明明在节点 1 已经填好。
两个症状来自两个独立的坑,却都指向同一个认知误区。
根因
坑一:按 type 搜索级联字段,搜不到任何结果
系统的字段类型枚举只有 8 个值:
type ColumnType =
| 'string' | 'number' | 'date' | 'enum'
| 'preset_enum' // 所有级联树都用这个
| 'percent' | 'currency' | 'formula'没有 cascade,没有 linked_enum,没有 region_cascade。
所有级联树——无论是省市区还是职称三级——都用 type = 'preset_enum'。判断一个字段是否参与级联的唯一依据是配置行的形状:parent_field_key 非空 AND linked_enum_level 非空。
当我只修了 A 树的渲染逻辑,没有顺手查 B 树,是因为我以为"按 type 能圈住全部级联字段"——这个前提根本不成立。
系统里没有专属的级联 type,所有级联字段 type 都是 preset_enum。按 type 定修复范围,必然漏掉同类字段,发版后用户投诉"另一个也坏了"。
正确的审计查询是直接打数据库配置表:
SELECT
rc.report_id,
rc.field_key,
rc.parent_field_key,
rc.linked_enum_level
FROM report_columns rc
WHERE rc.parent_field_key IS NOT NULL
AND rc.linked_enum_level IS NOT NULL
AND rc.is_deleted = 0;这条 SQL 返回的才是系统里所有参与级联的字段,一条不漏。这次查出了两组——A 树 2 个字段、B 树 2 个字段——修复必须覆盖全部。
代码里识别级联字段也应该用配置形状,而非 type:
export function isCascadeField(field: CascadeFieldSpec): boolean {
return !!(field.presetEnumId?.startsWith('db:') && field.linkedEnumLevel)
}四个不变量:presetEnumId 以 db: 开头(从数据库枚举拉数据)、linkedEnumLevel 非空(多级树)、parentFieldKey 可空(根节点 vs 子节点)、type 对识别毫无用处。
坑二:多节点工作流中父字段值被节点过滤器静默丢弃
多节点审批流场景:节点 1(填报)包含父字段 titleSeries(可编辑);节点 2(审批)包含子字段 reviewCategory、reviewMajor(可编辑),但不含 titleSeries(节点 2 的人不能改它)。
CascadeGroupProvider 接收 values prop,子字段在渲染时读 values['titleSeries'] 来决定自己的候选项。节点 2 表单初始化时,代码只对"当前节点可编辑字段"赋初值:
// 反模式——只初始化当前节点可编辑的字段
const editableKeys = new Set([...requiredFields, ...optionalFields])
const init: Record<string, unknown> = {}
for (const col of columns) {
if (editableKeys.has(col.key)) { // titleSeries 不在这里
init[col.key] = row[col.key] ?? ''
}
}titleSeries 不在节点 2 的 editableKeys 里,于是 formValues['titleSeries'] === undefined,Provider 读到 undefined,将子级下拉标为禁用。但 titleSeries='工程技术' 明明就躺在 approval.formData 里。
仅按"当前节点可编辑"初始化 formValues,会把前序节点填写的父字段值悄悄丢掉,导致级联子字段在后续节点永远处于禁用状态,且没有任何错误提示。
正解
修复一:按配置形状定作用域
修任何一棵级联树之前,先跑上面那条 SQL,拿到系统内所有级联字段清单,验证本次修复覆盖了全部相关行。
修复二:跨节点父字段值继承算法
初始化 formValues 时,对每个级联组:只要组内有任意字段在当前节点可编辑,就把该组所有非可编辑成员的值也纳入初始化。
export function computeCascadeInheritedParentKeys(input: {
cascadeGroups: Map<string, CascadeFieldSpec[]>
editableKeys: Set<string>
}): Set<string> {
const parentKeys = new Set<string>()
for (const [, groupFields] of input.cascadeGroups) {
const hasEditableInGroup = groupFields.some(
(gf) => input.editableKeys.has(gf.key)
)
if (!hasEditableInGroup) continue
for (const gf of groupFields) {
if (!input.editableKeys.has(gf.key)) {
parentKeys.add(gf.key) // 继承父字段的值,但不允许编辑
}
}
}
return parentKeys
}最终 formValues 的 key 集合 = editableKeys ∪ cascadeInheritedKeys,Provider 读到的父值完整,子级正确联动。
还有一个细节:originalFormValues 必须和 formValues 完全镜像(同一批 key,同一批初始值),否则提交时 dirty-diff 会把父字段值当"已修改"提交,触发后端"无权编辑此字段"的 403 报错。
多节点级联场景需审计三条轴:(1) 渲染范围 = 当前节点可编辑字段;(2) Provider.fields = 整组级联字段定义,不能按节点截断;(3) Provider.values = 可编辑字段值 ∪ 跨节点父字段继承值。只修其中一条,剩下两条必再出问题。
一句话外卖
级联能力藏在配置行的形状里(
parent_field_key + linked_enum_level),不藏在type字段里;修一棵级联树之前先查配置表,圈出系统内所有同类字段,否则"另一个也坏了"的投诉是必然结局。