
简介UniEdit电子病历编辑器是一款面向医疗信息化场景的ActiveX组件可快速集成到C/S或B/S架构的电子病历系统中帮助开发者实现病历录入、编辑、模板套用、数据保存等核心功能有效替代传统纸质病历的繁琐流程。压缩包共38个文件大小约9.65MB主要包含xml配置、html演示页面、bat注册脚本、doc接口说明以及dll控件等xml文件用于定义病历模板、ICD编码及字典选项html示例展示网页端调用方式doc文档则为二次开发提供必要的接口参考。目前已有1172人学习下载。借助包内示例项目与SDK文档读者能够掌握网页端编辑器的注册、调用与传参流程理解多页眉页脚、ICD编码字典等特性的配置方法从而降低电子病历系统的开发门槛提升医疗文书处理的规范性与效率为医院信息化建设提供可靠支撑。 做医疗信息化的朋友应该都有印象电子病历编辑器是整个HIS系统里最难啃的一块骨头。我第一次接触UniEdit这个电子病历编辑器时就意识到它不是普通富文本编辑器加几个按钮那么简单而是一个从数据模型、模板引擎、质控规则到签名留痕全覆盖的深水区。这篇文章我会以实际集成开发和使用的视角把UniEdit的核心原理、关键功能、部署踩坑和二次开发思路一次讲清楚适合正在选型或准备接手电子病历编辑器开发的同学参考。1. 医院为什么要单独的编辑器通用富文本满足不了病历1.1 病历的本质是一份带语义的法律文书很多人第一次听说电子病历编辑器时第一反应是不就是个文本框吗 但实际上病历的真正身份是法律文书。它不只是记录医生想法的一段文字而是纠纷发生时医学鉴定的核心依据也是医保控费、医疗质控的数据源头。一份完整住院病历里有哪些东西主诉、现病史、既往史、体格检查、辅助检查、初步诊断、治疗意见——这些不是随意排版的普通文章每个段落背后都有明确的医学语义。比如主诉必须简洁、聚焦、符合症状时间的表述规范现病史必须按时间顺序展开。如果这些内容是纯文本系统根本没法判断医生有没有漏项、写没写全、诊断编码是否对得上。所以电子病历编辑器必须做到把文字内容和结构语义绑定在一起而不只是保存一段HTML或Word内容。这也是UniEdit这类专业编辑器存在的根本原因。1.2 通用编辑器的三个致命缺陷我早期做过一个项目为了省事直接在系统里嵌了流行的富文本编辑器结果上线三个月就被临床科室骂回来了。踩过的坑可以总结成三条格式自由但数据失控医生可以用换行、空格硬生生把版面搞得像手写病历加粗、下划线满天飞但系统完全不知道哪些内容属于哪个字段。病历质控部门要统计主诉是否超过50字根本无从下手。模板形同虚设虽然富文本编辑器也支持插入模板但模板嵌套、占位符替换、动态数据拉取这些需求靠通用编辑器里的预制HTML片段是做不扎实的。最常见的场景是把患者姓名、年龄、入院时间做成占位符通用编辑器替换一次可以但要做到每次打开不同病人都自动替换就得自己套大量后处理代码非常容易错。无法满足医疗合规要求修改留痕、上级医师签名、数据不可篡改、审计日志这些强制要求通用富文本编辑器一个都没有。说白了通用编辑器解决的是人怎么打字而电子病历编辑器要解决的是病历怎么写才合规、数据怎么存才能用。1.3 电子病历编辑器该有的样子真正能落地的电子病历编辑器至少需要具备四个能力结构化模板能力模板不是写死的HTML而是可以嵌套、可配置数据元、支持条件显隐的文档骨架。数据与展示分离同一份病历页面看到的是一版样式底层保存的是结构化数据。同一份数据可以渲染成打印版、移动端预览版或归档XML。业务规则嵌入必填校验、值域约束、评分卡规则、逻辑判断都要在编辑器内部跑起来。审计与签名通道每一次修改都有留痕签名位与CA体系对接。UniEdit正是按照这个思路设计的。接下来我从数据和内核两头拆解。2. UniEdit的数据模型与核心架构2.1 文档树每个节点都绑定数据元如果你打开UniEdit的文档存储结构会发现它托管的内容不是一段单纯的HTML字符串而是一棵文档树。树的每个节点都对应一个医学数据元比如主诉是一个节点现病史是一个节点患者基本信息是另一个节点。每个节点上挂载的属性大致包括节点类型文本、段落、表格、选择框、复合块等数据元标识如chief_complaint、present_illness必填标记为true时文档保存前会校验是否为空值域规则可选的医学枚举值或格式约束显示样式字体、缩进、是否只读这种节点即字段的设计带来的直接利好是保存病历的时候系统可以精确拿到每一个字段的值然后直接映射到数据库表的对应列。做检索、统计、质控就很轻松不需要再去解析一大段文本找规律。在实际集成中我们会在编辑器初始化时传入一份数据元字典它告诉UniEdit这套模板里有哪些字段、每个字段的类型和约束是什么。UniEdit负责把字段渲染成可编辑区域再把用户输入的内容同步回数据元字典里。2.2 存储格式XML还是JSON很多人会问UniEdit底层存的是什么格式。从卫生行业规范来看电子病历共享文档通常采用基于HL7 CDA的XML结构但从Web前端处理效率来说JSON比XML更顺手。UniEdit的做法是外部交换用XML格式内部运行用JSON格式。这里有一个很关键的为什么病历归档时需要符合行业交换标准所以对外导出用XML但浏览器里处理JSON树增删改节点、绑定事件、动态渲染都方便得多。因此UniEdit内核维护一份JSON树在保存或归档时再序列化成XML文档。如果你要跟他对接需要知道有哪些常用转换接口。比如获取当前编辑器中的结构化数据接口会返回JSON数组每个元素都带有meta_oid数据元标识和value当前值。提交到后端后由后端服务把JSON转成XML存档这一层逻辑在网关里做比较合理。2.3 编辑内核DOM渲染与数据层如何协同UniEdit在内核上采用的是数据层与渲染层双向绑定的架构。编辑区域依然基于浏览器的contenteditable能力但用户每次输入都会先经过一个拦截层把输入事件转换为针对文档树节点的变更操作然后由渲染引擎重新绘制对应区块。这样做的好处是无论用户怎么操作文档树始终是可靠的数据源。渲染层再花哨也不会污染数据结构。有一段时间我担心这种架构会不会导致输入延迟实测下来只要控制好节点粒度也就是不要把一个几百行的大表格当做一个节点而是拆成行级、单元格级节点输入响应还是很跟手的。对了UniEdit还内置了一套节点类型注册机制。文本节点、数值节点、日期节点、单选节点、复合段落节点……每种节点有自己的编辑器组件和校验逻辑。你要增加一个自定义节点类型只需要实现统一的接口往注册表里注册即可。后面第5章我会给一个简单的扩展示例。3. 结构化录入、留痕与质控的实现路径3.1 模板引擎从Word病历到页面模板医院里的大量病历模板最初都是Word文档UniEdit的模板引擎支持把Word内容导入后通过标记工具把需要结构化的文本位置圈选出来绑定数据元。这一步在实际部署中非常重要因为要求临床科室从零开始用空页面写病历几乎不可能。必须做模板迁移。模板文件本身也是一棵文档树。模板树和实例文档树的区别是模板里的节点值可以是空值、占位符或默认值实例文档则是模板的填充结果。每次医生新建病历时UniEdit会根据当前病历类型加载对应模板再通过数据填充模块把HIS里的患者基本信息自动填入。模板里的条件显隐比如性别为女才显示月经史段落是在模板节点上配置规则表达式实现的。医生打开编辑器的瞬间UniEdit会先运行一条规则链把不需要的节点直接隐藏避免医生被无关字段干扰。3.2 数据自动填充HIS数据怎么进文档病历里大量内容是重复劳动比如患者姓名、床位号、入院时间、诊断列表。UniEdit的数据填充模块通过占位符映射来搞定这件事。模板节点上定义了一个data_source属性比如{ oid: patient_name, data_source: patient.name }集成方需要注册一个数据提供函数。医生打开病历时UniEdit会调用这个函数把从HIS取回的病历头数据包传进来然后按data_source路径自动把值填到对应节点。这里有一个容易踩坑的点不要把数据提供函数做成阻塞同步接口。医院网络环境下HIS接口响应经常几百毫秒甚至更久如果编辑器的初始化和数据填充都等接口返回页面白屏时间会非常明显。我们集成时的做法是编辑器先渲染模板骨架患者数据异步回填回填过程中字段显示为灰色加载状态数据到了再高亮刷新。医生感知到的就是打开病历很快数据稍微等一下跳出来体验比整体卡住好得多。3.3 修改留痕与电子签名的交互逻辑修改留痕是电子病历编辑器区别于普通编辑器的硬性能力。UniEdit里的留痕不是简单记录谁在什么时间改了什么而是在文档树节点层面维护一个变更历史列表。当上级医师修改下级医师写的病历时具体流程是这样的下级医师保存的版本成为基础版本该版本对应的节点值被标记为原始值。上级医师在编辑器里修改内容变更监听器捕获到节点值变化生成一条变更记录包含数据元标识、原始值、新值、操作者、操作时间。变更记录会以修订气泡或批注样式的形式渲染在原文中新值用高亮颜色显示原始值用删除线或浅色背景显示。上级医师完成修改后需要对本次修改进行签名确认。UniEdit提供了签名面板可对接CA或手写签名设备签名动作会触发整份文档的哈希锁定。哈希锁定的逻辑是把当前文档树所有节点的有序值拼接计算HASH值后与签名信息一起存储。之后如果有人绕过编辑器直接改数据库里的字段重新计算HASH就会发现对不上。这个设计虽然不是国家级防篡改标准但对医院内审计来说已经够用。我做集成时额外补了一步把留痕记录单独同步一份到ES或日志系统方便医务科做统计分析。原因很简单编辑器里的留痕记录一旦被管理员误删或清理还能从日志系统追溯回来。4. 部署接入时踩过的坑和解决方案4.1 老浏览器与旧内核的兼容问题医疗行业最让人头疼的不是业务逻辑而是终端环境。很多基层医院的办公电脑还停留在Windows 7 IE11有的甚至用国产浏览器老版本内核。UniEdit虽然整体兼容性不错但以下几个点必须提前确认。拖拽上传图片老内核不支持或不完全支持拖拽API需要考虑降级为文件选择框。MutationObserver部分老内核对这个API支持不稳定会影响节点变更监听。如果现场浏览器版本太低建议引导医院信息科做浏览器升级或者启用UniEdit兼容模式改用定时轮询差异来兜底。CSS Grid布局打印和编辑器样式用了大量Grid布局老内核解析会崩。解决办法是在打印视图切换到Table布局的兼容样式。我们实际交付时给医院提供的标准终端配置里明确写了推荐浏览器版本同时沟通了最低可用版本的边界。这一步最好在项目启动时就谈清楚否则后期全是运维工单。4.2 大病历文档的性能瓶颈一份重危病人的病历如果整租了历史病程记录可能会有几百个节点。如果全部一起渲染打开和输入时都会明显变慢。UniEdit对这种情况有懒渲染机制初期只渲染当前视窗范围内的节点滚动时动态挂载和销毁节点组件。但懒渲染解决不了另一个问题文档树全量同步带来的内存占用。特别是包含大量图片的文档每张图片base64字符串会吃不少内存。我们在实际使用中发现把图片单独传到文件服务节点里只保留图片ID和URL内存占用能降一半以上。还有一个性能细节UniEdit在保存时会全量计算文档树的HASH和必填校验。如果文档确实很大保存按钮可能卡一下。建议前端在保存时禁用按钮并显示进度提示不要异步重复点击。4.3 打印分页与版式问题医院病历打印有严格版式要求A4纸、固定页边距、页码、页眉页脚。UniEdit的打印部分是通过单独的打印模板来控制的编辑视图和打印视图是两个样式体系。我踩过最大的坑是表格跨页断行。病历里的检验结果表经常一行跨到下一页打印出来中缝处会截断表格边框。UniEdit里需要给表格节点配置重复表头和禁止行内分页两个属性同时打印样式中用break-inside: avoid来避免行内容被切开。另一个坑是页眉页脚在不同科室有不同要求。比如手术科室需要打印手术同意书页眉要带医院名称和医疗文书水印。UniEdit支持页眉模板变量在打印模板中可以绑定文档元数据比如医院名称、病历类型、患者ID这样一套打印程序可以应对多个模板。4.4 与HIS系统对接的数据往返UniEdit并不是独立存在的产品它一定要嵌到HIS或EMR系统里。常见的集成方式有三种iframe嵌入UniEdit作为独立Web应用通过postMessage与宿主页面通信。前端组件集成把UniEdit的npm包直接引入HIS前端工程。后端接口对接通过REST API完成病历数据的创建、读取、更新、归档。我们的项目最终用的是第一种第三种混合页面里iframe嵌入编辑器通过postMessage通信控制加载哪份病历、保存同时后端通过接口做数据归档和签名锁定。这里必须提醒一句跨域环境下postMessage的权限校验一定要做。不要只看event.origin的前缀直接startsWith(https://example.com)这种写法人一多就会漏洞。要精确匹配完整origin并且对消息里的指令做白名单校验不然有心人可以在浏览器控制台伪造消息调用保存接口。5. 二次开发实操从配置到上线5.1 初始化一个可用的UniEdit实例先说明一下UniEdit的发行包会区分前端SDK和服务端组件。前端SDK负责渲染编辑器服务端组件负责模板管理、文档归档和签名验证。初始化一个前端编辑器的核心代码大概长这样import UniEdit from uniedit-sdk; const editor new UniEdit({ container: document.getElementById(emr-editor), config: { // 编辑器主题 theme: material, // 是否开启修订留痕 trackChanges: true, // 是否开启自动保存 autoSave: false, // 数据元字典定义各字段的约束 metaDictionary: /api/meta/dictionary, // 数据提供者用于自动填充患者信息 dataProvider: window.fetchPatientData, // 打印模板地址 printTemplate: /templates/print/admission.html } }); editor.on(ready, () { console.log(UniEdit 初始化完成); });config里的每一项都值得细看。尤其是metaDictionary它指定了数据元字典的获取地址编辑器启动时会请求这个接口把当前病历类型对应的字段约束拉下来。这相当于告诉UniEdit这次要编辑哪些字段、每个字段有什么规则。5.2 加载病历模板与数据初始化完成后要做两件事加载模板、回填数据。假设我们进入的是入院记录编辑界面// 根据病历类型获取模板ID const templateId await fetchTemplateIdByType(ADMISSION); // 加载模板 const template await editor.loadTemplate(templateId); // 请求患者数据 const patientData await fetchPatientData({ patientId: P001234 }); // 将患者数据注入文档 editor.fillData(patientData); // 渲染文档 editor.render();fillData这个接口不是简单的属性赋值它会根据节点上的data_source配置把患者数据包中的对应字段写入文档树同时触发一次校验把不符合值域规则的数据用红色问号标记出来。比如患者年龄是未知但字段约束要求整数就会在界面上给出提示医生可以手动更正。5.3 保存与提取结构化数据保存病历的时候我们一般不会直接把编辑器里的HTML回传后端而是先调用UniEdit的结构化提取接口拿到JSON数据。async function saveDocument() { // 校验文档完整性 const validation editor.validate(); if (!validation.passed) { alert(存在必填项未填写 validation.relatedMetaOidList.join(, )); return; } if (editor.isDirty()) { // 提取结构化数据 const structData editor.getStructuredData(); // 请求签名可选 const signInfo await requestSignature(); // 提交保存 await saveToHis({ templateId: currentTemplateId, data: structData, signInfo }); // 标记编辑器已保存 editor.markSaved(); } }getStructuredData()返回的结构是按文档树的层次组织的嵌套JSON。比如现病史节点下如果有症状列表会表现为一个数组。后端拿到这个JSON后可以按需将其转换成归档XML或插入关系表。这里有一个踩坑提醒保存时不要连同编辑器内部状态一起存比如trackChanges的临时标记、光标位置、撤销栈等。这些状态只属于前端会话一旦重新打开病历撤销栈应该清空不然医生会莫名其妙按一次撤销把别人的字段回滚了。UniEdit的接口设计上其实已经做了隔离但如果你自己扩展了一些临时状态务必在getStructuredData()时排除掉。5.4 性能调优与扩展自定义控件最后聊两个二次开发中最实用的点。第一个是性能调优。前面说过懒渲染但如果你还是觉得在低配电脑上卡可以试试调整UniEdit的渲染节流阈值const editor new UniEdit({ container: el, config: { performance: { // 设置视窗外预渲染缓冲区比例 renderBufferRatio: 0.5, // 启用变更合并每200ms合并一次高频输入 changeMergeInterval: 200, // 文档节点数超过2000时自动进入轻量模式 lightModeThreshold: 2000 } } });第二个是自定义控件扩展。比如医院需要一个疼痛评分尺点击弹出一个0-10分滑竿选择后把结果写进对应字段。这个控件本质上是一个新的节点编辑器import { NodeKindRegistry } from uniedit-sdk; // 注册一个自定义节点类型 NodeKindRegistry.register(pain_score, { // 默认值 defaultValue: null, // 创建DOM render(value, props) { const el document.createElement(div); el.innerHTML input typerange min0 max10 step1 value${value ?? 0}; el.querySelector(input).addEventListener(change, (e) { props.onChange(Number(e.target.value)); }); return el; }, // 校验 validate(value) { return typeof value number value 0 value 10; } });注册之后模板节点里只要把type设为pain_score就自动使用这个滑竿控件了。通过这种方式UniEdit可以接入体表面积计算、危重度评分、过敏史标签选择等医院个性化的输入需求。最后再分享一个小技巧也算是我集成UniEdit时最省心的一步把编辑器的校验规则和数据元字典在后端也维护一份前端校验失败了能良好提示后端在归档前再校验一次。两边的规则版本可能不完全同步但至少要保证后端是最新版本。因为前端被绕过或出现Bug时后端这层校验就是守住病历数据质量的最后一道门绝对不能省。本文还有配套的精品资源点击获取