DirectSurface UIDirectSurface UI
开始使用
文档/企业架构

病历编辑器接入

本文示例使用以下公共入口;业务类型和服务仍由你的应用定义:

import {  MedicalRecordEditorController,  RenderMedicalRecordEditor,  type MedicalRecordDocument,} from 'ds-ui'

病历编辑器是 DirectSurface UI 中最复杂的业务组件之一。它不是普通富文本控件,而是面向电子病历场景的文档编辑器,包含分页排版、数据元、表格、批注、留痕、审阅侧栏、弹层编辑、打印 SVG 和大文档性能优化。

本文面向业务系统开发者,说明如何把编辑器接入页面、如何组织文档数据、哪些能力由编辑器负责、哪些规则应由业务系统负责。

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

能力边界

编辑器负责:

  • Canvas 实时编辑和绘制。
  • 病历文档分页、缩放、滚动和可视页绘制。
  • 文本输入、中文 IME、粘贴、拖选、复制和光标。
  • 段落格式、字体、颜色、项目符号、编号、缩进。
  • 表格插入、合并、拆分、行列增删、边框和斜线。
  • 日期、下拉、多选、数据组等数据元的弹层编辑。
  • 批注和质控问题的创建、展示、状态流转和锚点状态。
  • 留痕显示、留痕提示、审阅项导航和筛选。
  • 打印模式下输出 SVG 节点。

业务系统负责:

  • 登录用户、当前患者、当前就诊、文书状态和权限判断。
  • 是否允许编辑、是否启用留痕、是否显示批注和审阅块。
  • 文档 JSON 的加载、保存、归档、版本和服务端审计。
  • 模板库、病历分类树、文书实例列表和接口错误处理。
  • 打印流程、打印预览、PDF 生成、归档签名和外部质控流程。

编辑器本身不判断病历业务状态。业务应根据流程状态和权限设置 controller/view options,而不是把“已归档不能编辑”“上级医生才能审核”等规则写进底层编辑器。

最小接入

const controller = new MedicalRecordEditorController()const editor = new RenderMedicalRecordEditor({  controller,})

页面关闭时必须释放 controller。推荐页面持有 controller,编辑器只作为页面 render tree 的子节点:

class MedicalRecordPage {  readonly controller = new MedicalRecordEditorController()  readonly editor = new RenderMedicalRecordEditor({    controller: this.controller,  })   dispose(): void {    this.controller.dispose()  }}

如果页面本身也是 RenderObject,通常在页面 dispose() 里先清理页面创建的 DOM、订阅和异步请求,再调用 super.dispose(),最后释放 controller。不要把 controller 放到应用级单例里长期复用。

页面结构

典型病历编辑页面由“模板树 + 工具栏 + 编辑器”组成:

RenderStackPanel vertical
  RenderToolbar
  RenderSplitter horizontal
    left: 模板树 / 文书目录 / 大纲
    right: RenderMedicalRecordEditor

实现要点:

  • 病历编辑器自己管理滚动,不要再套一层同方向 ScrollViewer
  • 左侧模板树适合用 RenderTreeView,长文本要有省略和 tooltip。
  • 工具栏应读取 controller.getToolbarState(),并在 controller change 事件后刷新。
  • 大文档下不要在每次输入后重建整个页面 render tree。

文档数据

MedicalRecordDocument 可以是:

输入类型 说明
已反序列化的病历 JSON 对象 业务接口和模板库最常用。
JSON 字符串 适合本地文件或旧接口返回字符串。
底层文档模型对象 高级扩展场景使用,普通业务优先传入已反序列化的病历 JSON 对象。

加载文档:

async function loadMedicalRecord(url: string): Promise<void> {  const controller = new MedicalRecordEditorController()  const response = await fetch(url)  if (!response.ok) throw new Error(`HTTP ${response.status}`)  const document = await response.json() as MedicalRecordDocument  controller.loadDocument(document)}

加载本地 JSON:

async function loadLocalMedicalRecord(file: File): Promise<void> {  const controller = new MedicalRecordEditorController()  const text = await file.text()  const document = JSON.parse(text) as MedicalRecordDocument  controller.loadDocument(document)}

保存文档:

const controller = new MedicalRecordEditorController()const schema = controller.serialize()const json = JSON.stringify(schema)

推荐保存口径:

  • 页面只保存 controller.serialize() 的结果。
  • 不保存临时可视状态,例如滚动条位置、当前 hover、popup 状态。
  • 是否保存留痕、批注、审计轨迹取决于文档 JSON 结构和业务服务约定。
  • 保存前可以先执行 controller.validate(),把必填数据元或业务校验问题展示给用户。

模板库接入

模板库通常不是编辑器能力,而是业务页面组合:

模板索引接口
  -> TreeView roots
  -> 点击模板节点
  -> fetch 模板 JSON
  -> controller.loadDocument(document)

推荐模板索引字段:

字段 说明
id 模板唯一标识。
name 模板显示名称。
children 子分类或子模板。
hasDocument 当前节点是否能加载文档。
documentPath 静态模板 JSON 或接口地址。
parseError 模板解析失败时用于提示。

模板树只负责选择模板。真正加载文档后,应调用 controller.loadDocument(),然后把焦点交回编辑器。

工具栏和状态同步

工具栏不要自己猜当前选区状态,应使用:

const controller = new MedicalRecordEditorController()const state = controller.getToolbarState()

常用状态:

字段 说明
canUndo / canRedo 撤销重做是否可用。
fontName / fontSize 当前输入或选区字体。
color / background 当前文字颜色和背景色。
bold / italic / underline / linethrough 当前文本样式。
subscript / superscript 上下标状态。
textAlign 当前段落对齐。
paragraphNumberType noneulol
enableTrackChanges 是否启用留痕。
showTrackChanges 是否显示留痕块。
showTrackChangesTip 是否显示留痕提示。
showComment 是否显示批注。
documentMode 当前文档模式。

监听变化:

const controller = new MedicalRecordEditorController()const subscription = controller.on('change', () => {  const state = controller.getToolbarState()  state.canUndo}) subscription.unsubscribe()

页面 dispose 时要释放订阅。也可以直接在页面 dispose 中调用 controller.off('change') 清理页面注册的 handler。

常用编辑 API

文档和历史

API 说明
loadDocument(document) 加载病历文档,重建分页、清理历史基线。
loadDoc(data, { autoScrollToTop? }) 兼容旧 API,默认加载后滚动到顶部。
serialize() 序列化当前文档。
serializeString() 序列化为字符串。
validate() 执行文档校验。
undo() / redo() 撤销、重做。
canUndo() / canRedo() 历史状态判断。
beginHistoryStep() / commitHistoryStep() 需要把一组外部操作合并成一步历史时使用。

文本和段落

API 说明
setTextFormat(props) 设置选区或当前输入文字属性。
setTextFormatByFn(setter) 按函数修改文字属性。
adjustFontSize(delta) 增减字号。
setParaStyle(props) 设置段落属性。
setParaStyleByFn(setter) 按函数修改段落属性。
setParagraphNumberType(type) 设置项目符号、编号或取消。
insertSoftBreak() 插入行内换行符。
insertPageBreak() 插入分页符。

表格

API 说明
insertTable(rows, cols) 插入表格。
setCellBorder(command) 设置当前选区表格边框。
mergeCellsBySelection() / combineCell() 合并当前表格选区。
splitCell(rows, cols) 拆分当前单元格。
restoreMergeCellsAtCursor() 还原合并单元格。
insertRowAboveAtCursor() / insertRowBelowAtCursor() 插入行。
insertColToLeftAtCursor() / insertColToRightAtCursor() 插入列。
removeCurrRowAtCursor() / removeCurrColAtCursor() 删除当前行或列。
removeTableAtCursor() 删除当前表格。
setCellDiagonal(diagonal) 设置单元格斜线。
setTableCellBgColorAtSelection(color) 设置选区单元格背景。

批注和质控

API 说明
insertComment(text, options?) 对当前选区插入批注。
insertQualityIssue(text) 插入质控问题。
updateCommentText(id, text) 更新批注内容。
setCommentStatus(id, status, remark?) 更新批注状态。
removeComment(id) 删除指定批注。
removeCurrentComment() 删除当前光标所在批注。
clearAllComments() 清空批注。

状态包括:

状态 语义
open 未处理。
addressed 已处理,待确认。
resolved 已解决。
reopened 重新打开。
closed 已关闭。
anchorChanged 关联正文发生变化。
orphaned 关联正文锚点丢失。

数据元

API 说明
insertElementSchema(schema) 插入一个序列化元素,例如文本数据元、日期数据元、列表数据元。
getCurrentDataElement() 获取当前数据元。
getCurrentDataGroupElement() 获取当前数据组。
setCurrentDataElementValue(value) 设置当前数据元值。
moveFocusToNextDataElement() 跳到下一个数据元。
getDataElementPosition(element) 获取数据元装饰位置,用于弹层或按钮定位。

日期、下拉、多选、树形选择等数据元的编辑 UI 由编辑器内置弹层处理。业务如果需要自定义弹层,应基于实际 render rect 定位,不能基于理想文本宽度定位。

视图和打印

API 说明
setDocumentMode(mode) 设置 designeditformview
setTrackChangesEnabled(enabled) 启用或关闭留痕。
setShowTrackChanges(enabled) 显示或隐藏留痕块。
setShowTrackChangesTip(enabled) 显示或隐藏留痕提示。
setShowComment(enabled) 显示或隐藏批注。
scale(scale) 设置缩放,范围由编辑器限制。
switchPageLayout(mode) 切换页面布局模式。
setPaperOrient(orientation) 设置纸张方向。
setPaperSize(name) / setPaperSize(width, height) 设置纸张大小。
setDocumentMargin(margin) 设置页边距。
printSvg(ranges?) 生成打印 SVG 页面。

文档模式

模式 用途
design 模板设计,允许插入数据元、表格、批注等结构。
edit 业务编辑,适合医生书写。
form 表单填写,强调数据元录入。
view 只读查看。

业务系统应根据文书状态切换模式。例如新建模板用 design,医生书写用 editform,归档查看用 view

留痕和批注

留痕和批注是两个不同概念:

类型 作用
留痕 记录正文内容变更。
批注 对一段正文提出意见、质控问题或协作说明。

推荐业务规则:

  • 是否启用留痕由业务状态决定,例如归档后修改、上级医生修改或特定流程节点。
  • 同一操作员连续修改同一字段时,可以在业务层合并保存轨迹,避免轨迹膨胀。
  • 批注创建后,当前打开会话内继续编辑批注正文可合并到创建记录;不同打开会话再次修改应形成修改审计。
  • 如果批注关联正文被修改,应标记 anchorChanged;如果锚点被删除,应标记 orphaned
  • 批注状态流转应走业务命令,例如处理、解决、重开、关闭。

审阅侧栏由编辑器渲染,业务页面可以通过:

const controller = new MedicalRecordEditorController()const editor = new RenderMedicalRecordEditor({  controller,}) editor.setReviewFilter({  type: 'comment',  status: 'active',})editor.activateNextReview()

筛选类型:

类型 说明
all 全部审阅项。
comment 普通批注。
quality 质控问题。
track 留痕。

性能策略

病历编辑器的大文档性能依赖三点:

  1. 编辑器自己管理滚动和可视页绘制。
  2. 输入时优先尝试快速 patch,当前段落高度不变时不全量分页。
  3. 选区绘制基于全局范围坐标投影到可视页,不展开全量选区树。

接入时不要破坏这些前提:

  • 不要在外层套同方向滚动容器。
  • 不要每次输入都 loadDocument()
  • 不要 hover 时刷新整个业务页面。
  • 不要把大文档、图片缓存或控制器放到全局上下文。
  • 不要在 popup 或批注卡片里创建无法释放的订阅。

大文档问题优先用 Runtime Diagnostics 观察 layout、paint、frame 和事件耗时,再看编辑器自己的 debug state。

内存释放

页面关闭时建议执行:

class MedicalRecordPage {  private localInput: HTMLInputElement | null = null  readonly controller = new MedicalRecordEditorController()   dispose(): void {    if (this.localInput) {      this.localInput.onchange = null      this.localInput.remove()      this.localInput = null    }    this.controller.dispose()  }}

释放检查:

  • 页面是否还被 workspace、tab manager 或业务缓存引用。
  • controller 是否 dispose。
  • 本地文件 input、popup、context menu 是否移除。
  • 图片、条码、二维码缓存是否属于文档上下文,而不是全局。
  • 异步模板加载返回后是否判断页面已关闭。

打印和 SVG

Canvas 模式用于实时编辑;打印模式可以走 SVG。二者是不同输出路径:

模式 目标
Canvas 编辑 输入、拖选、弹层、审阅交互必须流畅。
SVG 打印 分页、纸张、条码、二维码、留痕和批注输出必须稳定。

使用:

const controller = new MedicalRecordEditorController()const pages = controller.printSvg()

printSvg() 返回每页 SVG 字符串。业务系统可以继续做打印预览、PDF 转换、盖章、签名或上传归档。

调试入口

常用调试方法:

方法 说明
editor.debugState() 查看页数、视窗、滚动、popup、审阅筛选等状态。
controller.revision 任意 UI 状态或选区变化版本。
controller.documentRevision 文档刷新版本。
controller.getToolbarState() 当前工具栏状态。
controller.getSelectedText() 当前选中文本。
controller.getSelectedJSON() 当前选区 JSON。

调试建议:

  • 光标、hover、popup 卡顿先看运行时诊断面板。
  • 输入卡顿先区分是 layout、paint、输入承接控件还是业务页面重建。
  • 关闭页面后内存不下降,生成 heap snapshot 检查 controller、document、canvas、popup、input 是否仍被引用。

常见错误

把编辑器放在外层 ScrollViewer 里

病历编辑器自己管理滚动。外层同方向滚动会导致滚轮、popup 定位、审阅块和横向滚动范围不稳定。

每次切换模板都复用旧页面状态

加载新文档后应刷新工具栏、审阅筛选和焦点;如果业务希望滚动到顶部,可以调用 loadDoc(data)nativeScrollTop(0)

把页面草稿和当前文书放到应用级上下文

当前文书应放页面或 tab 上下文。应用级上下文只放登录用户、租户、权限服务、字典服务等长期共享对象。

批注只当作黄色背景

批注是业务协作信息,应该有作者、时间、状态、审计、锚点状态和后续处理动作。

日期数据元允许任意键盘输入

日期数据元应通过日期弹层或明确的日期输入段录入,避免普通文本输入破坏数据结构。

忽略打印模式验证

Canvas 编辑正常不代表打印正常。条码、二维码、分页、批注、留痕都要在打印 SVG 路径单独验证。

相关文档