DirectSurface UIDirectSurface UI
开始使用
文档/业务指南

编辑会话

import {  DataGridEditSession,  EditSession,  FormSession,} from 'ds-ui'

编辑会话保存“进入编辑时的原值”和“当前草稿”之间的差异。它适合维护资料、录入申请单、弹窗编辑和其他需要保存、取消、脏状态判断的页面。

EditSession 只管理值和编辑状态;FormSession 在它上面组合校验、错误和提交。两者都不扫描组件树,不代理业务对象,也不依赖某个前端框架。

创建会话

interface OrderDraft {  drugCode: string  drugName: string  specification: string  unit: string  price: number} const session = new EditSession<OrderDraft>({  drugCode: '',  drugName: '',  specification: '',  unit: '',  price: 0,}) session.setValue('drugCode', 'MED001')const draft = session.snapshot()

构造参数会被复制为 baseline 和 draft。默认复制是浅拷贝,字段相等判断使用 Object.is

如果模型包含数组或嵌套对象,调用方应把它们当成不可变值,或者提供符合业务语义的 cloneequals

interface TagDraft {  name: string  tags: string[]} const session = new EditSession<TagDraft>(  { name: '常用模板', tags: ['内科'] },  {    clone: value => ({ ...value, tags: [...value.tags] }),    equals: (field, left, right) => (      field === 'tags'        ? JSON.stringify(left) === JSON.stringify(right)        : Object.is(left, right)    ),  },)

EditSession 不解析 patient.address.city 这样的字符串路径。复杂对象可以拆成页面需要的扁平草稿,或者由业务提供明确的读写函数。

一次修改多个字段

药品选择通常会同时回填编码、名称、规格、单位和价格。使用 setValues() 后,会话只向外通知一次完整变化:

interface MedicationDraft {  drugCode: string  drugName: string  specification: string  unit: string  price: number} const session = new EditSession<MedicationDraft>({  drugCode: '',  drugName: '',  specification: '',  unit: '',  price: 0,}) session.setValues({  drugCode: 'MED001',  drugName: '阿莫西林胶囊',  specification: '0.25g × 24粒',  unit: '盒',  price: 18.5,})

多个不同操作也可以放在同步批次中:

interface Draft {  value: string  confirmed: boolean} const session = new EditSession<Draft>({  value: '',  confirmed: false,}) session.batchUpdate(() => {  session.setValue('value', '已复核')  session.setValue('confirmed', true)  session.markTouched('value')})

批次内的最新值立即可读,最外层批次结束后才通知订阅者。回调必须同步执行,不能返回 Promise 或跨越 await

dirty 和 touched

interface Draft {  name: string} const session = new EditSession<Draft>({ name: '原名称' }) session.setValue('name', '新名称')session.markTouched('name') session.isDirtysession.isFieldDirty('name')session.isTouchedsession.isFieldTouched('name')
  • dirty 表示当前字段值与 baseline 不同。
  • touched 表示页面确认用户已经操作或离开过该字段。
  • 值改回 baseline 后,字段 dirty 自动消失。
  • touched 不会因为值改回原值自动消失,可用 markTouched(field, false)resetField()reset() 清除。

会话不会自行判断“点击”和“失焦”。页面根据输入组件的交互时机显式调用 markTouched()

恢复和接受修改

方法 作用
resetField(field) 把一个字段恢复到 baseline,并清除该字段 dirty 和 touched。
reset() 恢复整份 baseline,并清除全部 dirty 和 touched。
acceptChanges() 把当前 draft 设为新 baseline。
acceptChanges(value) 用保存后返回的值同时替换 baseline 和 draft。
snapshot() 返回当前草稿的副本。

保存成功后应显式接受服务端返回值:

interface Draft {  id: string  name: string} const session = new EditSession<Draft>({ id: '', name: '新项目' })const saved = { id: 'P001', name: '新项目' } session.acceptChanges(saved)

临时事务

事务适合“打开一个局部编辑器,确定后一次写回,取消时直接丢弃”的场景:

interface Draft {  code: string  name: string} const session = new EditSession<Draft>({ code: 'A', name: '原名称' })const transaction = session.beginTransaction() transaction.setValues({ code: 'B', name: '新名称' })transaction.commit()

事务维护独立的 staged draft。事务内通过 transaction.getValue() 可以立即读取暂存值,session.getValue()session.snapshot() 在 commit 前仍返回正式草稿。commit 会一次性写回值、dirty 和 touched,并最多发出一次最终变化;rollback 直接丢弃暂存值,不会通知。

事务有明确边界:

  • 不支持嵌套。
  • 不能在 batchUpdate() 内开启。
  • 事务存续期间,正式会话的写方法会拒绝执行;所有暂存修改都应通过 transaction 对象完成。
  • 调用方应在同一段同步流程内 commit 或 rollback,不要让事务跨越 await
  • 它是页面内的草稿检查点,不代表数据库事务。

订阅

interface Draft {  name: string  amount: number} const session = new EditSession<Draft>({ name: '', amount: 0 }) const stopAll = session.subscribe(change => {  if (change.valueChanged) {    const current = session.snapshot()    void current  }})const stopName = session.subscribeField('name', value => {  void value}) stopName()stopAll()session.dispose()

整体订阅一次接收一批变化及其字段集合,字段订阅只在该字段的值、dirty 或 touched 发生变化时执行。取消函数和 dispose() 都可以重复调用。

与 FormSession 的关系

需要规则校验、服务端错误、首错定位或提交状态时,直接创建 FormSession<T>。它通过 form.edit 暴露同一个编辑会话,也代理了常用的值读写、dirty、touched、批次、事务、恢复和接受修改方法。

详细用法见表单校验与提交

DataGrid 编辑会话

EditSession<T> 管理一份字段草稿,DataGridEditSession<TRow> 管理表格行的新增、修改和删除。后者直接订阅 GridView 的公开数据变更事件,不会为每个单元格创建一份响应式对象。

import {  DataGridEditSession,  RenderDataGrid,  type GridColumnDef,} from 'ds-ui' interface OrderRow {  id: string  itemCode: string  quantity: number} const columns: GridColumnDef<OrderRow>[] = [  { key: 'itemCode', title: '项目', type: 'text', editable: true },  { key: 'quantity', title: '数量', type: 'number', editable: true },]const rows: OrderRow[] = [  { id: 'O-001', itemCode: 'MED-001', quantity: 1 },]const grid = new RenderDataGrid({ columns, rows, editable: true })const orderGridEdits = new DataGridEditSession(grid) grid.setCellValue(rows[0]!, 'quantity', 2) orderGridEdits.isDirtyorderGridEdits.getRowState(rows[0]!)const changes = orderGridEdits.getChanges()

getChanges() 返回三个集合:

  • added:当前仍在表格中的新增行及其源数据索引。
  • modified:原有行、当前源数据索引,以及每个变更字段的 previousValue 和当前 value
  • deleted:被删除的原有行、原始源数据索引,以及删除前已经产生的字段修改。

新增行随后删除不会留下变更;字段改回原值后,对应字段修改会消失。resetRow(row) 只恢复一行,reset() 恢复整个表格,acceptChanges() 把当前行集合和值建立为新 baseline。

行身份

默认身份就是行对象本身,不要求 rowKey。这适合桌面业务组件常用的模式:页面拿到一组业务对象,表格排序、过滤、编辑和删除始终围绕这些对象工作。

const objectRows = [{ id: 'O-001', quantity: 1 }]const objectGrid = new RenderDataGrid({  columns: [{ key: 'quantity', title: '数量', type: 'number' }],  rows: objectRows,})const objectIdentityEdits = new DataGridEditSession(objectGrid)

只有业务会用一批新对象整体替换 grid.rows,并且还要保留尚未保存的本地修改时,才启用按 key 对账:

const replacementRows = [{ id: 'O-001', quantity: 1 }]const replacementGrid = new RenderDataGrid({  columns: [{ key: 'quantity', title: '数量', type: 'number' }],  rows: replacementRows,  rowKey: 'id',})const replacementEdits = new DataGridEditSession(replacementGrid, {  rowKey: 'id',  preserveChangesOnRowsReplace: true,})

启用后 rowKey 必须稳定且唯一。新的数据集合到达时,会话把本地修改重放到同 key 的新行对象上。未启用时,替换 rows 表示业务已经重新绑定数据,新集合直接成为 baseline,旧变更被清空。

数据写入边界

编辑会话只跟踪经过 GridView 数据 API 的变化:

  • 单元格写入使用 setCellValue()
  • 行结构变化使用 addRows()insertRowsBefore()insertRowsAfter()removeRows()
  • 用户在单元格编辑器中的提交会自动进入同一条变更链路。

直接执行 row.quantity = 2 再调用 refreshRow(row) 只是在通知表格重绘,会话无法知道旧值。业务字段联动也应逐项调用 setCellValue(),这样保存变更集和撤销都能覆盖主字段与联动字段。

baseline 默认只保存字段旧值的引用。字段值是可变数组或对象时,可通过 cloneValueequals 提供业务自己的复制与比较语义。

与 FormSession 合并

页面同时有普通字段和明细表格时,用 bindDataGridToForm() 把表格作为一个复合编辑、校验参与者接入:

import {  DataGridEditSession,  FormSession,  RenderDataGrid,  bindDataGridToForm,} from 'ds-ui' const detailGrid = new RenderDataGrid({  columns: [{ key: 'quantity', title: '数量', type: 'number' }],  rows: [{ quantity: 1 }],})const detailForm = new FormSession({ remark: '' })const detailGridEdits = new DataGridEditSession(detailGrid)const unbindDetailGrid = bindDataGridToForm(detailForm, detailGrid, {  editSession: detailGridEdits,})

接入后:

  • form.isDirtyform.isTouched 同时包含普通字段与表格。
  • form.reset() 会恢复表单字段和表格行变更。
  • form.acceptChanges() 会同时接受两部分的新 baseline。
  • form.validate()form.runSubmit() 会先收尾活动单元格,再执行 Grid 全表校验;失败时 form.focusFirstError() 可定位首个表格错误。

Grid 仍保留逐单元格错误和定位,FormSession 只保存一条汇总问题,不复制每个单元格错误。绑定不拥有 form、grid 或 edit session,页面释放时应先取消绑定,再分别释放会话和组件。

生命周期

会话应由当前页面、弹窗或工作区文档持有:

interface Draft {  name: string} const session = new FormSession<Draft>({ name: '' }) class PageOwner {  dispose(): void {    session.dispose()  }}

不要把页面草稿会话放进应用级单例。会话释放后会清理订阅;继续写入、开启事务或新增订阅会抛出错误,也不应依赖释放后的只读快照。

相关文档