【EMR 编辑器 · 数据源与录入域详解】
模板与数据分离的核心能力 · 数据占位 · 录入填空 · 列表循环 · 占位图片
这是编辑器最重要的专题,做「模板化文书」必读。先讲清楚思想,再逐个讲四样东西:数据源(只读占位)、录入域(正文填空)、列表循环(多条数据展开)、占位图片(图片留位)。
【一、为什么需要「模板与数据分离」】
想象一个场景:医院有一份「入院记录」模板,每天要给几十个患者生成各自的记录。笨办法是为每个患者复制一份文档手改;聪明办法是——模板里埋好占位符(姓名、性别、主诉…),程序把患者数据「灌」进去,一键出稿。灌完还能把医生手工补充的内容「抽」出来入库。编辑器把这套机制做成了四个组件:
组件
像什么
典型用途
数据源
印好的表格里印死的字段
患者信息等系统已有数据(只读回显)
录入域
表格里留白的填空题
主诉、现病史等医生现填内容(可编辑)
列表循环
一行模板 N 条数据
检验结果多条记录、多段病程记录
占位图片
贴照片的虚线框
胸片、签名等后传图像
所有定义(数据源 / 录入域 / 占位图片)都随文档 JSON 持久化——模板本身就是一个 JSON 文件,不存在外部注册表。
【二、数据源:系统数据的只读占位】
第 1 步:定义
右侧「数据源」面板可视化定义(中文名 + 英文 key + 字段列表),或代码创建。key 用于程序赋值时的索引,label 是无值时正文显示的文字:
api.addDataSource({
key: 'patient', name: '患者基本信息', // key: 程序用;name: 给人看
fields: [
{ key: 'name', label: '姓名' },
{ key: 'sex', label: '性别' },
{ key: 'age', label: '年龄' },
]
});
第 2 步:往正文插占位符
光标放到正文要出现「姓名」的位置,数据源面板点选「姓名」(或代码 api.insertDataField('patient', 'name'))。正文出现一个原子占位符片段:整体只读不可拆分——模板的「骨架」不会被误改坏。
第 3 步:赋值(程序灌数据)
api.setDataValues({ patient: { name: '张三', sex: '男', age: '45' } });
有值的字段:显示「张三」+ 浅蓝底;
无值的字段:显示中文名「姓名」+ 灰底——一眼看出哪里没填;
赋值的同时数据也内嵌进文档(dataValues),保存 JSON 后打开仍在;
提取与清除:api.getDataValues() / api.clearDataValues()(清除后占位符回到无值态,定义与绑定保留)。
定义变更怎么办?改字段用 api.updateDataSource(def);改名 / 改 key 用 api.replaceDataSource(oldKey, def)——正文所有引用和数据值会自动迁移到新 key;删除用 api.removeDataSource(key)——占位符退化为普通文本。
【三、录入域:正文里直接填空】
数据源是「系统给、正文只读显示」;录入域正相反——「正文里留给医生打字的空」。它看起来像占位符(空域显示中文名 + 琥珀色底),但光标进去就能直接输入,无需弹表单。适合主诉、现病史、查体所见这类没法预填的内容。
// 定义录入域组(结构和数据源一样)
api.addEntrySource({
key: 'chief', name: '主诉与现病史',
fields: [
{ key: 'chiefComplaint', label: '主诉' },
{ key: 'presentIllness', label: '现病史', minLines: 3 },
]
});
// 光标定位后插入:正文出现可填写的空域
api.insertEntryField('chief', 'chiefComplaint');
minLines ≥ 2 时:该域独占段落的话,自动补出 N 行空白占位行(虚线框 + 行底色)——纸面上的「填写区」,行高撑开不塌陷;与其他文字混排时只影响单行;
空域退格会整域删除(不是逐字删),删空自动回占位态——防误触;
域内文字受「录入域身份」保护:提取时按组 / 字段归属,不会被普通排版操作破坏。
提取与回填(归档的核心闭环):医生填完后,getEntryValues() 把全部域内容按「组 → 字段 → 文本」抽成 JSON 入库;下次打开病历,setEntryValues(values) 把库里的值回填进各域(空值自动回到占位态)。覆盖正文、表格单元格、页眉页脚里的所有域。
const values = api.getEntryValues();
// { chief: { chiefComplaint: '发热3天', presentIllness: '...' } }
await saveToDb(values); // 入库
api.setEntryValues(values); // 下次打开时回填
【四、列表数据源:多条数据怎么办】
普通数据源一个字段一个值;列表数据源(list: true)一个字段可以是一数组——比如「生命体征」里有 5 条测温记录。它绑定到表格行或段落组后,按记录条数自动展开。定义时用 links 声明聚合哪些普通数据源的字段(也可自带字段):
api.addDataSource({
key: 'vitals', name: '生命体征记录', list: true,
fields: [
{ key: 'date', label: '日期' },
{ key: 'temp', label: '体温' },
{ key: 'pulse', label: '脉搏' },
]
});
【用法 A:表格行循环(检验单、医嘱单)】
先做好一行「模板行」(各列插入字段占位符),光标放进该行,执行绑定。赋值后模板行按记录数复制展开:5 条记录就 5 行;清除数据时副本行收起,只剩模板行。
// 界面操作:右键 → 数据绑定 → 绑定列表数据源
api.bindTableDataSource('vitals'); // 光标所在行为模板行
api.setDataValues({ vitals: {
records: [
{ date: '2026-08-20', temp: '36.5', pulse: '78' },
{ date: '2026-08-21', temp: '37.1', pulse: '82' },
]
} } }); // 表格立即展开两行数据
示例里的 records 是「被链接的普通数据源」的 key:定义列表源时 links: ['records'] 聚合其字段,赋值时该源 key 下挂记录数组;列表源自身的单值字段(如 count 总条数)直接写字符串;
每行可绑定不同列表源(一张表混排多种记录);解绑 api.unbindTableDataSource(),查询 api.getTableRepeat();
展开的副本行带 copy 标记(内部使用),用户正常编辑即可,数据刷新时自动重建。
【用法 B:段落组循环(病程记录整段重复)】
不只表格:连续几个段落(如「日期 + 治疗措施 + 签名」三段)也能作为模板组循环——3 条病程记录就展开 3 组。使用前先把段落划进「数据段落区域」(bindDataArea,正文里的视觉分组标记),然后选中模板段绑定:
// 1. 选中若干段落 → 标记数据区域(右键或工具栏)
api.bindDataArea();
// 2. 选中区域内作为模板的段落组 → 绑定列表源
api.bindBlockRepeat('course'); // course 为列表数据源 key
// 3. 赋值后整组按记录条数循环展开
api.setDataValues({ course: { records: [...] } });
【五、完整实战:从模板到归档】
把本篇内容串成一条完整业务流(模板设计师 + 业务系统两个角色):
模板设计师在编辑器里排版「入院记录」:定义患者信息数据源、主诉录入域、检验表格行绑定、胸片占位图片,正文插好占位符;
导出 JSON 存为模板(api.getJSON()),发布到业务系统;
业务系统打开病历时:api.setDoc(templateDoc) → api.setDataValues({ 患者 + 检验记录 + __images__ }) → api.setEntryValues(上次保存的录入内容);
医生在正文里补写录入域、修改内容;
保存时:api.getJSON() 存文档快照 + api.getEntryValues() 存结构化字段(两张表各存一份,报表可直接查结构化数据);
归档时:api.getPDFFile({ fonts }, '入院记录.pdf') 直接上传文件服务器。
提示:建议把「模板」与「实例」分开存(模板只读保护),实例每次从模板生成——避免用户改坏模板。