Skip to content

083. 列默认值与行编辑状态管理

学习目标

这一节把通用表格的列配置继续往前推进,并开始进入行编辑功能。

学完后,你应该能理解:

  • 为什么列类型要能生成完整的列配置;
  • 列默认值为什么要放到各自列模块里;
  • getRowsMapper 这类工具函数解决什么问题;
  • 下拉、多选、开关列的显示和编辑逻辑有什么差异;
  • 行编辑状态为什么需要独立的 ID 映射;
  • FormInstance 为什么要和行数据建立对应关系。

从列类型到完整列配置

前面已经设计了可扩展的列类型。

核心流程是:

  1. 每种列在自己的模块里定义扩展类型;
  2. 通过扩展入口把类型合并起来;
  3. 给每种列补上 type
  4. 再叠加基础列属性;
  5. 最后得到完整的列联合类型。

对刚接触 TypeScript 的同学,可以先把它理解成“列配置注册表”。

不同列都把自己的类型放进注册表里,通用表格只面向这个统一结果工作。

单独取某一种列类型

完整列类型是联合类型。

例如:

ts
InputColumn | SelectColumn | ToggleColumn

有时还需要单独拿到某一种列。

例如只取输入框列、下拉列、开关列。

这样做的价值是:

  • 下拉列必须有 options
  • 开关列必须有真假值配置;
  • 输入框列不需要这些额外字段。

类型系统会提醒你某一类列应该具备哪些属性。

这比全都写成 any 更适合长期维护。

列默认值

列配置不能只靠业务页面手写。

通用表格要自动补充默认行为。

例如:

  • 索引列默认标题是序号;
  • 输入框列默认宽度可以是 100
  • 输入框列默认显示原始值;
  • 输入框列行内编辑时默认渲染输入框;
  • 日期列、开关列可以有不同默认宽度。

这些默认值组成一个映射对象。

它的 key 对应列类型。

它的 value 是一个函数,负责接收原始列配置,再返回补全后的列配置。

可以把它理解成:

ts
const defaultColumnMap = {
  input: fillInputColumnDefaults,
  select: fillSelectColumnDefaults,
  toggle: fillToggleColumnDefaults,
}

真正写代码时,key 和函数参数都要跟列类型系统对齐。

默认值为什么放回列模块

如果所有默认值都写在一个总文件里,后面新增列类型就会不断改总文件。

这会让通用组件变得很难扩展。

更合适的方式是:

  • 输入框列自己定义输入框列类型;
  • 输入框列自己注册输入框列默认值;
  • 下拉列自己处理下拉选项;
  • 开关列自己处理真假值。

通用表格的工具层只负责组织流程。

它不应该知道每种业务列的全部细节。

选项数组转换

下拉列的 options 可能有两种来源。

一种是字符串数组:

ts
["启用", "停用"]

另一种是对象数组:

ts
[
  { label: "启用", value: "Y" },
  { label: "停用", value: "N" },
]

为了后面统一读取,需要先把它们转成稳定结构。

否则显示、编辑、多选、查询都会重复写判断。

getRowsMapper

课程里写了一个工具函数,用来把数组转换成映射对象。

常见用途包括:

  • 根据 idcode
  • 根据 valuelabel
  • 根据某个字段找完整对象。

例如:

ts
const valueToLabel = getRowsMapper(options, {
  key: "value",
  value: "label",
})

对于 JS 开发经验来说,这就是把数组预处理成对象。

好处是后面查值不用每次 find

单选与多选

下拉列分为单选和多选。

单选时,值可以直接通过 valueToLabel 找到显示文本。

多选时,课程里先约定后端保存逗号分隔字符串。

例如:

ts
"read,write,admin"

显示时要拆成数组,再逐个找到 label。

编辑时组件可能返回数组,但提交给后端前要再转回字符串。

这里的重点不是逗号分隔一定最好,而是前后端必须有一致的数据协议。

开关列

开关列不是简单的 truefalse

很多业务系统会用别的值表示真假。

例如:

ts
trueValue: "Y"
falseValue: "N"

也可能是:

ts
trueValue: 1
falseValue: 0

所以开关列要自己维护真假值映射。

非编辑状态下,开关组件只负责展示。

编辑状态下,开关变化后要把业务值写回表单。

TypeScript 类型保护与 any

根据 column.type 分支判断时,TypeScript 可以收窄类型。

例如:

ts
if (column.type === "select") {
  // 这里可以安全读取 select 列字段
}

这种方式类型最安全。

但它有一个扩展问题:每新增一种列,总文件就要新增一个分支。

课程里选择在确认运行路径可控的位置使用 any

这里不是鼓励随便写 any,而是在“列类型和默认值注册机制已经保证对应关系”的前提下,避开 TypeScript 暂时无法推导的地方。

实际项目里可以记住一个判断:

如果 any 是为了偷懒,风险很高。

如果 any 是在明确的注册机制内部使用,而且外层类型已经约束好,风险会小很多。

行编辑状态

列默认值处理完后,课程开始进入表格编辑功能。

行编辑需要维护几个状态。

第一个是正在编辑的行 ID 映射。

例如:

ts
updateIdMapper[id] = true

它表示某一行正在编辑。

这种写法比只存一个 editingId 更灵活,因为未来可能支持多行同时编辑。

新增行状态

新增行还没有后端 ID。

但 Ant Design Table 要求每一行都有稳定 ID。

所以新增行会先生成一个临时 ID。

例如用 new 作为前缀。

保存时,如果检测到这是临时 ID,就不要把它当作真实 ID 传给后端。

新增行状态也会用一个映射记录。

它表示这条数据还没有真正落库。

计算编辑状态

真正决定某一行是否进入编辑态时,会综合两个来源:

  • 已有数据正在编辑;
  • 新增数据正在编辑。

所以会有一个计算出来的 ID 映射。

只要某个 ID 出现在编辑映射或新增映射里,这一行就应该进入编辑状态。

这也是前端状态管理的一个常见思路:

原始状态分开保存,界面需要的状态再计算出来。

FormInstanceManager

行编辑不是只改一份普通对象。

每一行内部会有一个 Ant Design Form。

Form 实例负责:

  • 触发表单校验;
  • 读取当前编辑值;
  • 设置字段值;
  • 重置编辑状态。

所以需要把“行数据”和“Form 实例”关联起来。

课程里使用 WeakMap

它的 key 可以是行对象,value 是对应的 Form 实例。

这样每一行渲染时把自己的 Form 注册进去,卸载时再删除。

为什么用 WeakMap

普通对象的 key 只能稳定使用字符串或 Symbol。

而这里要用行对象作为 key。

WeakMap 正好适合这种场景。

它能表达:

ts
record -> formInstance

对刚接触这块的同学,可以先不用记所有细节。

先记住用途:通过当前行数据,找到这一行自己的表单控制对象。

这一节的核心

通用表格不是把 Ant Design Table 包一层就结束。

它要自己补齐一整套协议:

  • 列类型;
  • 列默认值;
  • 选项映射;
  • 显示渲染;
  • 编辑渲染;
  • 新增行状态;
  • 编辑行状态;
  • 行和表单实例的关系。

这些协议看起来比普通页面复杂。

但它们带来的结果是:后面业务页面只需要声明字段,通用表格就能自动生成展示、编辑和保存流程。

AI Agent 课程学习文档。