尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UniEdit电子病历编辑器:从富文本到医疗级结构化文档的实践

UniEdit电子病历编辑器:从富文本到医疗级结构化文档的实践 简介UniEdit电子病历编辑器是一款面向医疗信息化开发者的ActiveX组件可快速集成到C/S或B/S架构的电子病历系统中解决传统病历录入效率低、多媒体支持不足等问题。压缩包共38个文件包含18个xml配置字典、7个html调用示例、3个doc开发文档以及注册脚本、dll控件和独立rar示例工程整体约9.65MB目录结构清晰。其中UniEditorforWeb示例项目展示了网页端调用编辑器的方法配合SDK文档和ICD10编码、选项字典等配置可帮助开发者完成控件注册、病历在线编辑与数据交互并支持模板加载、智能提示等功能。已有1172人学习下载适合医院信息系统开发人员、医疗软件实施工程师作为集成参考。1. 电子病历编辑器到底在做什么1.1 它不是“把Word搬到网页上”我第一次接到UniEdit这个项目时产品经理丢过来一句话“我们要做一个医院用的电子病历编辑器像Office那样就行。”当时我差点就信了。真正做完才知道电子病历编辑器跟普通富文本编辑器完全是两码事。先说它是什么。UniEdit本质上是一个跑在浏览器里的医学文书编辑组件医生在电脑上打开住院医生工作站写入院记录、病程记录、出院小结用的基本都是这个东西。它要支持段落固定格式、特殊医学符号、上下标、表格、图片插入、模板引用、电子签名、打印归档甚至还要把“患者主诉胸闷气促3天”这种自然语言拆成后续质控系统能看懂的结构化数据。它的使用场景比我之前做的资讯后台编辑器窄得多但要求高得多。资讯编辑器写错了顶多改一版病历写错了涉及的是医疗质量和法律追溯。所以我后来总结一句话普通编辑器追求的是“能打字、能排版”电子病历编辑器追求的是“写得出合规文书、存得下结构化数据、出得了打印件、经得起时间拷问”。如果你也是做医疗信息化、HIS医院信息系统或EMR电子病历相关开发的或者你正在调研市面上的医学编辑器组件这篇文章值得看完。里面是我在UniEdit从设计到落地过程中总结出来的核心思路和踩坑记录。1.2 为什么“结构化”不是可选项很多人第一反应是编辑器嘛我用现有的富文本库不就行了弄个textarea配合Markdown不是更快这里就碰到了一个非常关键的词——结构化。普通富文本编辑器的产物是HTML比如p患者今日神志清精神可/p。这个HTML用来展示完全没有问题但用在后端系统里就很麻烦。你可以试着想一下如果医院信息科要把病历内容按照卫健委的规范文档格式导出或者科研系统想统计某个科室近三年病历里“深静脉血栓”这个诊断出现的频次纯HTML没法可靠地提取这种结构化字段。所以在UniEdit内部数据模型用的是类JSON的树状结构每个节点都有明确的类型。比如“段落节点”“标题节点”“表格节点”“图片节点”“待填变量节点”每个节点上还可以挂属性。展示层负责把JSON渲染成可视界面持久层负责把JSON存进数据库中间再加一个双向转换器。这样前端只是一个“翻译官”真正的核心是背后那份可校验、可检索、可二次加工的病历数据。顺带说一句很多人会把“编辑器”和“编译器”混在一起其实这是两码事。编译器是把程序员写的源码翻译成机器能执行的指令编辑器是给人提供书写、修改内容的交互界面。UniEdit属于后者它不修改任何程序逻辑它改的是病历文书本身。做这个项目时我一直提醒团队我们做的不是编译器是“内容加工厂”而且是带医疗规范约束的那种。2. UniEdit整体设计思路拆解2.1 编辑器内核选型现成轮子改还是自己造市面上成熟的富文本编辑器不少TinyMCE、Quill、WangEditor、ProseMirror、Slate每个都有自己的拥趸。我当时做的第一件事不是写代码而是把这些轮子全部拉出来做了一轮对比最后才确定方向。先看TinyMCE和WangEditor这类开箱即用的产品。它们确实开箱即用工具栏、图片上传、代码高亮都有现成方案。但问题恰恰出在“开箱”这个地方——它们默认生成的HTML结构太自由了。医生可能不小心把“主诉”两个字套进了五级标题格式或者粘贴了一段网页内容进来后整个文书的样式全乱了。医学文书对格式一致性、段落约束要求极高自由度过大的编辑器在病历场景里反而是灾难。再看Slate和ProseMirror。它们不是“开箱即用”的编辑器而是“编辑器框架”核心思路是你自己定义文档模型插件系统负责处理操作和渲染。这意味着你可以规定哪些节点允许出现、节点之间如何嵌套、哪些操作被允许非常契合医疗文书需要强约束的场景。我最终采用的是类ProseMirror的方案。它把文档建模成节点树每次按键、粘贴、拖拽都转换为事务操作操作可以被记录、合并、回退天然支持协作和操作留痕。这对UniEdit太重要了后面做操作日志时几乎是白捡的便宜。UniEdit的底层贡献很大程度上要归功于选对了这个内核。对比经验整理成了下面这张表方案二次开发成本结构化能力模型自由定制适用场景TinyMCE / WangEditor低较弱输出以HTML为主有限普通后台管理、资讯发布Quill中有Parchment模型但复杂结构麻烦一般简单富文本、协作编辑ProseMirror中高强节点树模型天然结构化高复杂文档应用、在线协同办公Slate高强React生态好高但方案灵活到需要自制很多约束深度定制、数据模型复杂的编辑器2.2 模块划分与数据流转UniEdit的逻辑架构我分成三层来看展示层、操作层、数据层。展示层就是医生面前那块编辑区域负责渲染节点树、光标的样式、选中态等等。操作层是工具栏、快捷键、上下文菜单以及各种修改节点树的命令。数据层则是纯粹的数据管理负责JSON的校验、版本生成、自动保存、历史记录。这三个层之间通过一套规范的事件总线通信。比如医生点了一下“插入体温单”操作层发出insertVitalSignsTable命令数据层先在文档树里插入一个vitalSignsTable节点再触发documentChanged事件展示层收到事件后只重新渲染那一块区域而不是整个页面。这样既能保证长文档编辑时不卡顿又能让操作日志和撤销重做等功能自然而然地在数据层统一处理。数据流转路径是编辑操作 - 生成事务 - 更新JSON文档 - 触发变更事件 - 自动保存模块快照到本地 - 按策略同步到后端。这套流程保证了即使浏览器崩溃医生写的病程记录也不会彻底丢失。3. 实操过程与关键实现细节3.1 模板系统把医生最常用的“半篇文章”做成积木医生写病历有一个特点重复内容特别多。入院记录里的主诉、现病史、既往史不同患者的差别往往只集中在几个关键变量上。所以UniEdit里最受欢迎的功能不是酷炫的排版工具而是模板。模板不能简单做成“插入一段预制好的HTML”否则医生改起来还是头疼。UniEdit的模板是分层的整篇模板、章节模板、段落模板、短语模板。比如“入院记录”是整篇级模板它包含了主诉、现病史、既往史、体格检查等章节而“既往史”里又有“高血压病史”“糖尿病史”“手术外伤史”等段落模板医生点一下就能插入到当前光标位置。这里有个关键设计模板里的变量用占位符表示比如{{患者姓名}}、{{入院日期}}、{{主诉}}。当模板被插入时UniEdit不是简单替换字符串而是在节点树上生成带有variable标记的节点。这样后续可以弹出侧边栏统一填写、校验未填项甚至从HIS系统自动带入。举个最简单的节点JSON片段{ type: paragraph, children: [ { type: text, text: 患者 }, { type: variable, name: patientName, placeholder: 姓名 }, { type: text, text: 因 }, { type: variable, name: chiefComplaint, placeholder: 主诉 }, { type: text, text: 入院。 } ] }落到界面上的效果是能看到带高亮底色的变量槽点一下就能填写填完高亮消失但鼠标悬停时还能看到原始字段名。这套机制的坑点在于模板插入后必须让医生可以继续在变量周围自由输入和删除不能把变量“锁死”。我当时的做法是变量节点在键盘操作时被当作“一个不可分割的原子”Backspace只能删除整个变量但光标可以停放在变量前或后。想要删除变量本身得通过侧边栏的“移除变量”操作。这样既保护了结构化数据也保留了编辑的灵活性。3.2 图片与检查报告插入一切都要留痕和可追溯病历里图片是少不了的胸部CT截图、心电图、检验报告拍照都有可能被贴进文书。最初我想省事直接调用浏览器原生粘贴事件把图片转成base64放进文档里。测到一半发现不对一张手机拍的照片动辄两三MBbase64编码后再膨胀三分之一存进数据库简直要命。而且病历系统对影像资料有留痕要求不能只在前端显示必须落到独立的文件存储服务里。我重新设计了图片插入流程所有图片无论是通过工具栏上传、CtrlV粘贴、还是从其他文档拖拽进来都先走到统一的上传接口。前端拿到文件后先做压缩超过2MB的图片按比例缩到最长边1920px质量压到0.8再发到服务端。服务端返回一个JSON包含文件URL、文件名称、字节大小、MD5值。编辑器拿到返回值后在文档树里插入image节点并把服务端返回的元数据一并保存在节点属性里。这里最常见的问题其实对应了网上很多人搜的“ueditor上传图片提示上传成功但服务器返回错误”。我排查过类似问题根因基本在三处一是接口返回的数据格式和编辑器预期不一致比如编辑器要求{url: ...}后端给的是{path: ...}二是上传接口做了登录校验但编辑器实例请求时没有带正确的鉴权Header三是跨域配置没放行导致浏览器拦截了响应。解决方式也很简单前、后端先约定一个明确的响应协议统一用{code, data, message}包裹编辑器在上传插件里只认这个协议所有上传请求都走统一的带鉴权HTTP客户端Nginx或网关层把上传域名加入跨域白名单。UniEdit的图片模块稳定之后几乎没再因为这个出过问题。3.3 打印输出病历最终要落纸和归档我一开始严重低估了打印在医疗场景里的地位。后来被临床科室反复催“打印出来排版不对”才意识到电子病历不只是给屏幕看的它最终要打印成纸质文书归档进入病案室甚至在医疗纠纷时作为证据。所以打印功能不是“锦上添花”而是“生死线”。UniEdit的打印实现没有走“调用浏览器打印按钮”这条最简单路线。浏览器原生打印会把页面上的工具栏、菜单栏都打出来而且分页效果完全不可控。我方案是单独开一个打印预览页面只渲染当前病历的只读视图并引入专门的打印样式。为了保证打印效果我在CSS里做了一批只作用于打印预览页的样式用media print控制。关键点是分页控制每个章节标题前面强制分页所有正文段落设置page-break-inside: avoid避免出现跨页断裂表格设置重复表头页边距设为2cm符合医院病案归档的基本格式。还有一个细节是颜色屏幕上用于提醒的高亮底色、标记边框打印时全部置为黑白否则打印出来的病历会有大面积灰块既难看又浪费墨。医院环境里还有一批老旧的Windows电脑医生用的是Chrome 49甚至更低版本的内核这时候Canvas渲染图片还好说但一些现代CSS属性会失效。所以UniEdit的打印模块内置了降级策略检测到内核版本过低时自动切换到兼容模式用更保守的表格布局替代Flex布局。测试打印时不能只看预览效果最好实际生成一份PDF再拿PDF与纸质效果比对。我们当时就是通过这种方法发现了好几个不同浏览器之间字体渲染差异导致的换行错位问题。3.4 兼容性与性能医院里的电脑比你想象的旧医疗信息化项目跟互联网公司内部项目最大的区别是终端环境完全不可控。有些医院科室电脑是七八年前的配置内存4GB系统还是Windows 7浏览器五花八门360极速、QQ浏览器、IE内核兼容模式什么都有。UniEdit要在这堆环境里跑得动性能优化是硬指标。我做了几件事。第一编辑器主体按需加载工具栏里几十个按钮对应几十个功能模块不能全部打包进初始JS里。我按模块做了代码分割只有用户点击“插入表格”时才去加载表格插件的代码。第二长文档渲染不做全量更新。编辑过程中每个按键都会触发文档变化最开始我把整个文档树重新渲染一遍文档到5000字时就明显感到卡。后来改成按区块渲染只有发生变化的节点及其父节点重新计算实测在5000字以内、10MB级图片不超过5张的前提下输入延迟控制在了可接受范围。第三自动保存不能太频繁。每打一个字就向后端发一次请求既不现实也没必要我做了本地缓冲先用IndexedDB自动保存草稿再以30秒为周期同步到服务端同时保留最近3个历史版本。医生没感觉但数据安全系数高了很多。4. 常见问题与排查技巧实录4.1 高频问题速查表项目上线后我把现场人员反馈最多的问题整理成了一张速查表开发时对照排查效率提升很明显问题现象常见原因解决方案插入图片后界面不显示上传接口返回格式与编辑器不匹配或图片URL被服务端鉴权拦截统一上传响应协议图片URL使用带签名、可过期的临时链接从Word粘贴过来的表格样式全乱Word的HTML包含大量内联样式和专有标签与编辑器schema冲突自定义粘贴处理器只保留表格结构和文字丢弃其他样式打印时分页错乱一个段落被拦腰截断段落标签没有设置page-break-inside: avoid打印CSS中加上该属性并对表格、图片等块级元素单独约束分页行为输入法选词时候选词上屏后文字丢失编辑器在compositionend事件处理中清空了内容或重置了选区监听compositionstart避免在输入法组合期间拦截输入法自身的文本替换逻辑老的IE内核浏览器打开白屏现代JavaScript语法和DOM API不兼容构建时额外输出ES5版本关键API做polyfill检测到低版本内核时给出升级浏览器提示多人同时编辑同一份病历时互相覆盖缺少并发控制后保存的人覆盖先保存的内容采用文档层级悲观锁编辑前锁定关闭或保存时释放同时记录操作日志支持追溯4.2 我在落地中踩过的几个坑第一个坑是光标位置丢失。插入模板、图片或者在文末加一个变量节点时如果只是简单替换内容原来选中的Range会失效用户的光标会莫名其妙跳到文首。解决方法是所有会修改文档结构的操作执行前先保存当前选区再基于旧的Range位置算出新内容的偏移量操作完成后用新的Range恢复光标。这里看起来简单但涉及光标在节点树和视图层之间的位置映射处理不当很容易出现“点一次工具栏光标跳到开头”的诡异现象。第二个坑是样式隔离。医院的项目一般不是孤立系统病历编辑器经常嵌在OA、HIS、集成平台等多个系统里。这些系统自带一整套全局CSS弹窗、表格、甚至button的默认样式都有可能把编辑器内部样式冲掉。我最初只靠给UniEdit外层加了一个高优先级类名来兜底结果还是架不住各种!important。后来直接把编辑器内部样式全部收拢到Shadow DOM里让编辑器内部变成一个独立渲染环境才彻底解决互相污染的问题。代价是有些弹层组件需要手动挂载到外部容器这算可以接受的技术债。第三个坑是自动保存的“历史版本”策略。我之前只保留了最新一份草稿结果有一次医生写了很久的病程记录不小心把一大段删掉了自动保存又在他删除后触发了崩溃得不行。后来我改成保留最近3份快照并且每次手动点击“保存”时生成一个不可覆盖的归档版本。这样误操作后至少还能从上一个版本里找回大部分内容。这个改动在医生群体里好评度极高远远超过了一些炫酷功能。5. 一点收尾经验整个UniEdit从零到一我最大的体会是做医疗编辑器技术和业务至少各占一半。一开始如果只顾着研究富文本技术而忽略了医生到底怎么写病历、病历最终要流向哪里做出来的东西很可能好看但不好用。建议做类似项目的朋友先把你们医院或者目标医院的病历文书结构完整收集一遍把“入院记录”“首次病程记录”“出院小结”这些高频文书的段落组成、必填项、常见附件类型全部列出来再回到编辑器里设计数据模型整个架构会清晰很多。根据我个人经验最后再分享一个细节不要等到快上线了才找医生试用最好在一开始就让有经验的临床医生参与模板设计和录入测试。医生嘴上说的“随便能用就行”千万别信他们真正用起来时会给你提一大堆你完全没想过的问题。这些小问题积累得够多你的编辑器才真的算立住了。本文还有配套的精品资源点击获取
返回列表