Skip to content

feat(plugin-matrix): 可编辑交叉表 / 动态列矩阵视图(平台能力) #1905

Description

@xuyushun441-sys

Repo: objectui ・ 类型: 新增 renderer 包 @object-ui/plugin-matrix ・ 一期仅前端组件能力(不动 framework spec;声明式 matrix 视图类型留二期 ADR)

背景 / 平台缺口

objectui 现有两个相关组件都覆盖不了"动态列 + 可编辑 + 多字段单元格"这一类:

现有件 单元格 可编辑
ObjectGrid(plugin-grid) schema 固定 单字段 ✅ 内联编辑
PivotTable(plugin-dashboard) 数据驱动动态列 ✅ 单个聚合值 ❌ 只读
缺口 数据驱动动态列 多字段结构 ✅ 可编辑

这是一类反复出现的需求,不是某个业务专属:排班(人×日,每格 班次/工时/状态)、定价(产品×地区,每格 价格/折扣/生效期)、产能(资源×周期)、计划矩阵(任务×节点)。当前只能每个项目各写一个一次性组件。本 issue 把它沉淀成平台组件。

目标

新增 @object-ui/plugin-matrix,提供一个通用、配置驱动的可编辑交叉表组件:行来自一个对象、列在运行时由列轴对象的数据动态生成、每个交叉格映射一条"单元格对象"(结/明细对象)记录并可编辑多字段,写入全部经平台受治理数据层(校验/RLS/字段权限/hook/summary 重算)。任何业务(含图纸×机位)只需写配置即可使用。

范围

In scope

  • 新包 plugin-matrix,注册 matrix(presentational/value 数据)+ object-matrix(数据绑定,async 拉数)两个组件
  • 运行时从列轴对象数据生成动态列;冻结左侧固定列 + 横向滚动
  • 单元格多字段、按字段类型渲染编辑控件(复用 @object-ui/fields
  • 稀疏矩阵:空格=无单元格记录;填值→建记录、清空→删记录
  • 整表脏标记 + 批量保存(POST /api/v1/batch 原子事务)+ 乐观锁(ifMatch
  • 字段权限(FLS)/校验(400)/并发(409) 的格级回显
  • 只读模式;inputs 配置暴露给 Studio designer

Out of scope(二期)

  • framework spec 里的声明式 matrix 视图类型(需 ADR,另开)
  • 实时协同(realtime 订阅)
  • 单元格内"加列即建列轴记录"(先只在列轴维护页加)

包结构(沿用 plugin-grid / plugin-dashboard 范式)

packages/plugin-matrix/
├── src/
│   ├── index.tsx              # ComponentRegistry.register('matrix'|'object-matrix', …)
│   ├── ObjectMatrix.tsx       # 数据绑定 async 包装(仿 ObjectPivotTable)
│   ├── Matrix.tsx             # 渲染:动态列组 + 冻结列 + 稀疏格
│   ├── MatrixCell.tsx         # 单格:多字段渲染 + 编辑(包 InlineEditing)
│   ├── hooks/
│   │   ├── useDerivedColumns.ts   # 列派生(抽自 PivotTable 165-228)
│   │   ├── useDirtyTracking.ts    # 脏标记
│   │   └── useMatrixSave.ts       # 保存:upsert-by-key + batch + OCC + 错误归位
│   ├── components/{MatrixToolbar,CellError}.tsx
│   ├── utils/{deriveColumns,formatValue}.ts   # formatValue 复用 PivotTable
│   └── __tests__/…
├── package.json ・ vite.config.ts ・ README.md

app-shell 以 peerDependency 引入 → side-effect 触发注册(同 plugin-dashboard 接法)。

注册与配置(generic inputs

ComponentRegistry.register('object-matrix', ObjectMatrix, {
  namespace: 'plugin-matrix', label: '可编辑交叉表', category: 'plugin', icon: 'grid-3x3',
  inputs: [
    { name: 'cellObject',   type: 'string', required: true }, // 单元格/结对象,如 timesheet_entry
    { name: 'rowAxis',      type: 'object', required: true }, // { object, filter, keyField, labelFields, ref }  行轴 + 明细中指向行的字段
    { name: 'columnAxis',   type: 'object', required: true }, // { object, filter, keyField, labelField, ref }   列轴(动态) + 明细中指向列的字段
    { name: 'cellFields',   type: 'array',  required: true }, // [{ field, editable }]  每格展示/可编辑的字段
    { name: 'frozenCount',  type: 'number', defaultValue: 1 },
    { name: 'allowEmptyCreate', type: 'boolean', defaultValue: true }, // 空格填值→建记录
    { name: 'deleteOnClear',    type: 'boolean', defaultValue: true }, // 清空→删记录
    { name: 'readOnly',     type: 'boolean', defaultValue: false },
    { name: 'saveMode',     type: 'enum', enum: ['batch','onBlur'], defaultValue: 'batch' },
  ],
});

单元格 / 编辑 / 保存语义(通用)

  • 格 ↔ 记录:每格唯一映射 cellObject(rowAxis.ref = rowKey, columnAxis.ref = colKey) 的一条记录;(ref, ref) 应有唯一索引。
  • 稀疏:无记录的格显示空;allowEmptyCreate 时填值 → createdeleteOnClear 时清空全部 cellFieldsdelete
  • 改已有update(id, patch, { ifMatch })(OCC)。
  • 保存saveMode:'batch' 收集脏格 → POST /api/v1/batch(原子,支持 $ref 接新建 id);onBlur 即时存。无原生 upsert → useMatrixSave 内做 find-then-create/update(已知 PARTIAL,属预期成本)。
  • 受治理:所有写经 ObjectQL → 校验/RLS/FLS/hook/summary 重算照常(framework/.../objectql/src/engine.ts:2069)。组件不裸写,只调 DataSource

明确复用(不要重造)

复用件 位置 用法
列派生逻辑 plugin-dashboard/src/PivotTable.tsx:165-228 抽成 useDerivedColumns
内联编辑器(校验/异步存/错误/键盘) plugin-grid/src/InlineEditing.tsx 包住每个格字段
字段类型控件(date/select/currency…) @object-ui/fields getCellRenderer(fieldMeta) cellFields 字段类型渲染
保存回调范式(onCellChange/onRowSave/onBatchSave) plugin-grid/src/ObjectGrid.tsx:144-157 useMatrixSave 仿之
数据契约 types/src/data.ts:170(find/create/update[ifMatch]/delete/bulk) 组件取 useSchemaContext().dataSource
数据加载 hook react/src/hooks/useViewData.ts ObjectMatrix 拉行/列/明细三路
async 包装范式 plugin-dashboard/src/ObjectPivotTable.tsx ObjectMatrix 仿之(含 value→label 映射)

边界情况

  • 列数为 0 / 极多(横向滚动 + frozenCount 冻结左列;列虚拟化可二期)
  • 稀疏矩阵正确渲染空格
  • 字段无写权限(FLS) → 该格字段只读/脱敏,不报错
  • 校验失败(400) → 归位到具体格+字段
  • 并发(409) → 提示并给当前值,支持重载该格
  • batch 部分失败 → 整体回滚,逐格标错

验收标准

  • 列由列轴对象数据运行时生成,数据变则列数自动变
  • 改格 → 保存 → 刷新持久化正确;空格填值建记录、清空删记录
  • 写入触发父对象 summary 重算(证明走治理通道,非裸写)
  • FLS / 校验 / 409 均格级正确回显;readOnly 纯只读
  • 泛化验证 1(通用):排班场景 timesheet_entry(人×日,格含 班次/工时)仅配置跑通
  • 泛化验证 2(制造业实例):图纸×机位(cellFields=需求/计划/实际/状态)仅改配置即跑通——证明非业务耦合
  • inputs 在 Studio designer 可视化可配
  • 单测覆盖:列派生 / 脏标记 / 保存 upsert / 并发回滚

任务拆解(~6-9 人天)

  • 脚手架包 + 注册 + inputs schema(0.5d)
  • useDerivedColumns(抽离 PivotTable 列派生)+ 行/列/明细三路加载(1.5d)
  • Matrix 渲染:动态列组 + 冻结列 + 横向滚动 + 稀疏多字段格(1.5d)
  • MatrixCell 编辑:包 InlineEditing + @object-ui/fields 类型控件 + 脏标记(1.5d)
  • useMatrixSave:upsert-by-key + batch + OCC + 建/删格 + 错误归位(1.5d)
  • FLS/校验/409 回显 + readOnly + Toolbar(0.5d)
  • 单测 + 两个泛化验收场景 + README(1.5d)

与 PivotTable 的边界(设计决策)

新建 plugin-matrix,不扩展 PivotTable:聚合只读 vs 事务可编辑是两种心智;格结构(单聚合值 vs 多字段)和保存管线差异大。仅复用其列派生 useMemoformatValue 抽成 util。

Open Questions / 二期

  1. 列虚拟化(机位/周期很多时)——先普通滚动,性能不够再上
  2. 单元格状态着色(按某字段值)是否纳入通用配置
  3. 声明式 spec matrix 视图类型(让元数据直接 author,不写 componentId)→ 二期 framework ADR
  4. 实时协同 / 单元格级订阅

背景溯源:源于一个制造业计划管理系统评估(钢结构/海工,图纸×机位动态列矩阵)。该业务需求是本平台能力的首个验证实例,但组件本身按通用交叉表设计,不与业务耦合。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions