ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HIS病历编辑器:CKEditor粘贴Word图片自动上传实现

HIS病历编辑器:CKEditor粘贴Word图片自动上传实现 开头医院信息系统HIS里用CKEditor做病历编辑器看起来是个很常规的组合但真到了实施现场十个医生里至少有八个会问你同一个问题“我在Word里把检查图片、出院小结截图复制过来怎么粘进去就不显示了”这个问题看着小踩过的坑可一点都不少。今天这篇就把“HIS系统CKEDITOR粘贴病历WORD图片”这个需求彻底拆开从原理到可落地的示例代码全部讲清楚。先说清楚这篇文章能帮你解决什么如果你是HIS实施工程师或者刚接手医院项目的开发人员需要在CKEditor里实现“从Word粘贴内容包括图片图片自动上传并正常显示”的功能那这篇文章可以直接当参考文档用。我会把方案选型、核心代码、踩坑记录都写出来按着做基本能跑通。1. 先搞清楚为什么Word里的图片粘贴不进HIS病历编辑框1.1 HIS场景的特殊性病历编辑器不是普通的Web页面很多刚接触医院项目的朋友容易忽略一个前提HIS系统运行的环境和普通互联网项目差别非常大。医院内网里的电脑浏览器版本参差不齐老的科室还在用IE内核的兼容模式新一点的可能装了Edge或者Chrome 80再加上医院信息安全要求高编辑器资源往往全部本地化部署CKEditor想用的官方云服务和CDN一概不生效。在这样的环境下CKEditor一般用的是4.x版本因为它定制性强、插件体系成熟符合病历文书那种“段落、标题、表格、图片混排”的排版需求。但4.x版本有一个非常典型的短板它默认的粘贴处理对“从Word复制过来的图片”支持得很一般官方文档里虽然有paste插件但示例更多是针对拖拽上传图片而不是针对Word内嵌图片粘贴。另外HIS系统里的病历数据通常要落库、要归档、要打印甚至要做病案质控这意味着编辑器里所有图片都不能只是“临时在页面上显示”必须持久化存储并且能在打印、翻拍、归档等后续环节被可靠引用。这就决定了我们不能走“前端临时显示”的路必须设计一个完整的“从Word粘贴图片到服务器存储再回显”的链路。1.2 从Word复制到浏览器图片到底经历了什么要解决图片丢失先得知道图片在剪贴板里是怎么“走”的。你在Word里选中一段文字加一张CT影像图按CtrlC此时剪贴板里不是只有一份数据而是同时存在好几种格式纯文本、HTML片段、RTF格式、位图数据等。当你在浏览器编辑器里按CtrlV时CKEditor默认拿到的HTML片段里图片可能以两种形式存在第一种图片以Base64编码的data URI出现在img标签的src里类似data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...这种超长字符串这种形式如果图片小还好大图就会出现页面卡顿、数据量爆炸的问题。第二种图片并不在HTML片段里而是作为独立的文件对象挂在剪贴板的DataTransfer对象上此时编辑器的默认粘贴逻辑根本不会主动去“读取”这个文件于是图片直接就丢了。更麻烦的是Word的粘贴HTML里还裹着一堆“Office专用标签”比如o:p、!-- [if !supportLists] --这种注释性结构以及大量mso-开头的内联样式。CKEditor的过滤规则会把这些乱七八糟的东西清洗掉但清洗过程中如果img标签本身被判定为“不安全来源”也会一并被丢弃。所以很多时候医生粘贴完后文字还在图片却成了一片空白原因就在这里。理解了这条链路你就能明白为什么网上搜“ckeditor粘贴word图片示例在哪”总是一堆答非所问的结果——因为CKEditor本身只提供了一个“粘贴事件”至于怎么把剪贴板里的图片文件抠出来、传到哪里、什么时候回插都需要开发者自己实现官方并没有一个开箱即用的Word图片粘贴方案。2. 方案选型处理Word图片粘贴的几种主流思路2.1 三条路线对比上传、转Base64、本地路径针对“Word粘贴图片进编辑器”这个需求行业内大致有三种处理思路我先把它们的优缺点列出来你评估的时候可以直接对着表看。方案实现方式优点缺点方案A拦截粘贴并上传服务器回插URL监听paste事件从剪贴板取出图片文件异步提交到HIS附件接口完成后用返回的URL替换或插入img标签图片真正落库符合医院归档要求打印/翻拍都能取到大图可控需要后端配合写上传接口前端逻辑略复杂方案B转Base64塞进img标签读取图片文件转成data URI直接插入编辑器实现最简单不需要后端接口图片存入数据库会导致表体积暴涨大图会让编辑器卡死打印和归档阶段很难处理方案C教医生先存本地再手动上传医生从Word把图片另存为文件再通过系统“上传图片”按钮提交前端几乎零开发量操作繁琐临床科室根本不愿意用会被护士长投诉到信息科如果你只是做个内部小工具方案B短期内可以应付但在HIS这种对数据质量和长期稳定性要求很高的场景里方案B可以说是埋雷。我自己见过一个项目医生把一份20多页的病程记录带图粘贴进去Base64图片连同正文一起塞进了CLOB字段结果病历保存接口直接超时后面查问题查了一整天才定位到是图片太大导致序列化和网络传输双双卡死。2.2 为什么最终选择“拦截粘贴异步上传回插链接”最终在HIS项目里我选择的是方案A理由特别实际。第一病历正文在系统里是一段HTML图片如果以文件形式存在数据库或者文件服务器上那么后续无论是打印、送病案室翻拍还是做科研数据提取都可以通过图片URL重新组装页面不会因为数据库字段大小限制而翻车。第二Word里贴进来的检查图片往往都是高清大图一张几MB很常见。如果走Base64数据体积还会膨胀约33%数据库压力大不说编辑器页面渲染也会被这些超长字符串拖慢。而上传到附件服务后编辑器里只存一个相对路径或文件ID页面加载时浏览器只需要请求一个图片地址性能体验完全不一样。第三也是最重要的一点医院的信息科和病案科对“图片必须能追溯、能随病历一起归档”是有硬性要求的。方案B把图片模糊地嵌在HTML里真到了导出电子病历或病案质控系统要读取图片的时候Base64数据很难被其他系统直接解析而方案A天然解决了这个问题。这里要强调一点选择方案A不代表后端很复杂哪怕你只有一个简单的文件上传接口只要能接收multipart/form-data并返回一个可访问的图片URL整个方案就能成立。下面一节我就把前端的核心代码一步步写出来。3. 核心实现示例代码与配置步骤3.1 前提准备CKEditor初始化配置开始写代码前先把编辑器初始化到位。我以一个HIS系统里最常见的“病历内容编辑”场景为例textarea的id假设是emrContent后端上传接口假设是/his/attach/upload返回格式统一为JSON{code:0,data:{fileId:xxx,url:/his/attach/view?idxxx}}。CKEditor 4.x的初始化代码如下CKEDITOR.replace(emrContent, { height: 500, // 保留尽可能多的格式避免Word样式被过度清洗 allowedContent: true, // 粘贴内容时会经过pasteFilter这里设置为完全信任内容具体清洗后面自己处理 pasteFilter: null, // 关闭编辑器默认的图片拖拽上传避免和我们的自定义逻辑冲突 filebrowserUploadUrl: , // 额外插件按需引入这里用默认配置即可 extraPlugins: });这里两个点很容易踩坑第一个是allowedContent如果保持默认值CKEditor会把Word粘贴过来的很多标签和style属性过滤掉图片有时也会因为“不允许data URI”而被删。设成true意味着不过滤这样我们能拿到尽量原始的HTML再由自己在paste事件里做定向清洗。第二个是pasteFilter如果线上环境已经有别人配了过滤规则记得要覆盖掉否则你前脚从剪贴板拿到图片后脚就被编辑器的过滤链给吞了。3.2 监听粘贴事件从剪贴板里“抢”图片关键代码在这里核心逻辑就是监听CKEditor的paste事件然后从e.data.dataTransfer这个原生DataTransfer对象里把类型为image/*的文件一个个取出来。为什么不用直接读取粘贴HTML里的img标签因为很多场景下图片并不在HTML数据里而是作为独立文件对象存在不走DataTransfer.items你是拿不到这个文件的。editor.on(paste, function(e) { // 获取原生剪贴板数据对象 var dataTransfer e.data.dataTransfer; if (!dataTransfer) { return; } // 遍历剪贴板中的所有条目找出图片文件 var files []; var items dataTransfer.items; if (items) { for (var i 0; i items.length; i) { var item items[i]; if (item.type item.type.indexOf(image) ! -1) { var file item.getAsFile(); if (file) { files.push({ file: file, // 记录序号后面按顺序回插防止多图顺序错乱 index: files.length }); } } } } if (files.length 0) { return; } // 阻止编辑器默认的粘贴行为避免图片被丢弃或按默认方式处理 e.cancel(); // 逐张上传上传完成后再把图片插到光标位置 uploadWordImages(editor, files); });这段代码最核心的机制在于item.getAsFile()。以Chrome和Edge为例你复制Word里的一段带图内容时剪贴板里的图片会以DataTransferItem中的image/png或image/jpeg形式出现调用getAsFile()就能还原成真正的File对象可以扔进FormData传给后端。Firefox稍微有点差异但大体逻辑一致只是它有时会把图片作为多个image条目出现遍历时需要注意去重。还要注意一个问题paste事件在CKEditor 4.x中触发时e.data.dataTransfer只有在支持ClipboardEvent的浏览器上才可用旧版IE里这个对象可能不存在。如果你要兼容IE11需要额外监听beforepaste事件处理逻辑类似但取文件的路径是event.clipboardData。我建议先把Chrome/Edge跑通再回头兼容IE。3.3 将图片上传到HIS附件服务并回插编辑器文件拿到了接下来就是上传和回插。考虑到很多HIS老项目还在用jQuery我这里的示例就用jQuery的$.ajax配合FormData兼容性最好。function uploadWordImages(editor, files) { // 在光标位置插入一个占位提示告诉医生图片正在上传避免他以为粘贴失败了 var placeholder span stylecolor:#999;【图片上传中...】/span; editor.insertHtml(placeholder); // 按顺序处理每张图片都走同样的上传流程 var uploadNext function(index) { if (index files.length) { return; } var item files[index]; var fd new FormData(); fd.append(file, item.file); fd.append(source, ckeditor-word-paste); $.ajax({ url: /his/attach/upload, type: POST, data: fd, processData: false, contentType: false, dataType: json, success: function(res) { if (res res.code 0) { var imgHtml img src res.data.url stylemax-width:100%; classword-upload-img /; // 这里不用insertHtml而是先把占位符替换掉确保图片显示在粘贴的位置 replaceLastPlaceholder(editor, imgHtml); } else { showUploadError(item.index, res res.msg ? res.msg : 上传失败); } // 继续处理下一张 uploadNext(index 1); }, error: function(xhr, status, error) { showUploadError(item.index, 接口请求异常: error); uploadNext(index 1); } }); }; uploadNext(0); }你可能要问为什么不直接用editor.insertHtml按顺序插入因为异步上传的返回时间不确定如果医生一次粘贴了5张图片并行上传的情况下先返回的后到最终图片顺序就会和Word里的顺序不一致。上面这段代码用的是串行上传——上一张成功或失败后再传下一张确保图片插入顺序和剪贴板里的顺序完全一致。对于病历来说检查图片的前后顺序往往代表检查时间的先后这个细节不能省。replaceLastPlaceholder这个函数的作用是把上一步插入的“图片上传中”占位符替换成真正的图片。具体实现可以直接通过正则或遍历编辑器DOM找到最后一个占位文本function replaceLastPlaceholder(editor, imgHtml) { var body editor.document.getBody(); // 找出所有包含占位符文本的元素取最后一个 var placeholders body.find(span).$; var target null; for (var i placeholders.length - 1; i 0; i--) { if (placeholders[i].innerText placeholders[i].innerText.indexOf(图片上传中) ! -1) { target placeholders[i]; break; } } if (target) { var tempDiv editor.document.createElement(div); tempDiv.setHtml(imgHtml); var imgNode tempDiv.getFirst(); target.replaceWith(imgNode); } else { // 如果占位符找不到兜底直接插到末尾 editor.insertHtml(imgHtml); } }3.4 处理粘贴后Word格式残留图片问题解决后另一个经常让医生皱眉的问题是粘贴进来的文字带了一堆奇怪的样式字体一会儿宋体一会Calibri颜色深浅不一还有大量的空格和空行。这些都是Word HTML的典型“污染”。如果不想让病历界面显得乱糟糟需要在粘贴事件里额外做一次轻量清洗。我常用的方案是在paste事件里对e.data.dataValue做字符串替换和标签归一化处理。注意不能在getAsFile()分支里做因为这个分支已经e.cancel()了HTML数据不会进入编辑器。所以更合理的做法是在事件处理函数的开头先对文本HTML做清洗再走图片上传逻辑。editor.on(paste, function(e) { // 第一步清洗Word样式残留 if (e.data.dataValue) { e.data.dataValue cleanWordHtml(e.data.dataValue); } // 第二步处理图片略见3.2 });cleanWordHtml的关键清理点主要有这几个把o:p标签及注释块删掉把style里的mso-*属性清掉统一font-family为系统默认列表里的字体把span lang...这种语言标记去掉再处理Word常见的多余空行p classMsoNormalspan.../span/p。可以按实际情况写正则function cleanWordHtml(html) { return html // 去掉Word的命名空间标签 .replace(/\/?o:p[^]*/gi, ) // 去掉条件注释块比如 [if !supportLists] .replace(/!--\[if[^]*?!\[endif\]--/gi, ) // 去掉 mso 开头的样式属性 .replace(/style[^]*mso-[^]*/gi, ) // 去掉 Firefox 等浏览器复制的多余 meta 标签 .replace(/meta[^]?/gi, ) // 去掉继承自Word的 lang 属性 .replace(/\slang[^]*/gi, ); }这里有个忠告清洗正则写得再全也不可能覆盖所有Word版本和浏览器组合产出的HTML所以上线前一定找几种Word真实试一遍把实际粘贴出来的HTML导出看一眼再针对性地补充清洗规则。别过度清洗否则会误伤表格样式和段落缩进到时候格式问题又会成为新的一轮投诉热点。3.5 变体场景从网页、网页版Outlook粘贴开发中还会遇到另一个情况医生不从Word复制而是从浏览器里打开的PDF预览、网页版邮箱、OA系统里直接复制内容再粘到病历里。这种情况下图片的src往往是一个外部链接比如https://mail-hospital.example.com/xxx/image.png。内网环境或者没有外网访问权限的机器上这类图片同样会裂开显示成一个小红叉或空白。处理思路和Word图片其实一样观察粘贴事件得到HTML里的img标签凡是src不是data:开头、也不是本站地址的都可以考虑抓取下载到HIS附件服务。这个逻辑要做得严谨一点可以通过后端的附件接口接收一个fileUrl参数由后端代理下载并转存前端只需在清洗HTML时把这些img的src替换成相对路径。不过这个功能要特别注意安全只允许抓取白名单域名或内网地址否则会有SSRF风险。在医院的等保环境下这个需求建议先和运维确认清楚再实现。4. 实施过程中的常见问题与排查实录4.1 场景一粘贴后图片一片空白只有文字还在这是HIS实施群里被问得最多的现象。排查第一步按F12打开开发者工具查看编辑器里img标签的状态。如果img标签压根不存在说明图片在粘贴之前就被过滤规则吞了重点检查allowedContent和pasteFilter两个配置。如果img标签存在但src是一串data:开头的Base64且图片没有显示这通常是因为图片太大导致浏览器无法渲染或者编辑器所在的页面有CSP内容安全策略限制禁止了data:image类型。这种情况建议无论如何都走上传方案绕开data URI。在实际项目里我还遇到过一种隐蔽情况医院的杀毒软件或上网行为管理设备会拦截含大段Base64字符的网页请求导致粘贴时页面直接卡死这类问题在门诊医生站的多台电脑上集中出现排查时一度怀疑是代码问题最后发现是安全策略误伤。4.2 场景二图片变成了file://C:/Users/xxx/Desktop/image.png这种问题常见于老版IE和部分旧内核浏览器。原因很简单剪贴板里的图片被解析成一个本地文件路径生成的img标签引用了本地绝对路径这个路径只有那台电脑自己访问有效其他任何机器、任何服务器都打不开。解决办法也很直接在粘贴事件处理时发现img的src以file://开头直接把这个img标记为“待处理”并把src置空或换成占位图同时从剪贴板DataTransfer里尝试获取对应文件走正常上传流程。有一点要提醒很多开发者在这种场景下会试图用正则去从HTML里提取file://路径然后上传这在现代浏览器里是拿不到文件内容的因为浏览器出于安全限制不允许网页读取本地文件路径对应的文件内容。正确做法还是回到DataTransfer对象里去拿File对象而不是去解析路径。4.3 场景三上传接口返回401、405或跨域错误HIS系统里上传接口经常遇到两个地方。第一是鉴权很多HIS系统用session保持登录但上传接口如果走了不同的网关或者做了请求转发可能session拿不到提示未登录。处理办法一般是把附件上传接口加入到白名单或者在使用fetch时带上credentials: include。如果用jQuery的ajax注意设置xhrFields: { withCredentials: true }。第二是文件大小限制Word里贴的CT、MRI影像图原图经常超过2MB如果后端的Spring配置没有调大max-file-size上传时会直接在服务器端被拒绝前端却一脸茫然。建议上传接口的文件大小上限设为10MB同时在filedialog和paste流程中都要做前端文件大小校验超过预期就直接弹提示而不是闷头传半天再失败。4.4 场景四粘贴多张图片后图片顺序乱了这个问题前面提过根因是异步上传的并发乱序。解决方案有两种一种是串行上传我上面给的示例就是这种。另一种是并行上传但给每个img占位标记编号等所有图片返回后再统一按编号排序回填。串行的好处是实现简单缺点是5张以上的大图粘贴时会明显变慢医生体感不太好。如果能接受其实可以改成“前2张并行后面的排队等”这类定制策略不过对于病历场景医生一次粘贴的图片一般不超过5张串行完全够用。4.5 避坑清单速查表我在实施过程中整理了一份检查清单你遇到问题时可以按顺序快速过一遍。检查项操作路径常见坑编辑器配置确认allowedContent和pasteFilter没有误伤img默认过滤会丢图片剪贴板读取Chrome调试断点看dataTransfer对象结构不同浏览器items结构不同上传接口用Postman模拟multipart请求确认返回格式与预期一致返回的url如果是相对路径要确认前端能正确拼接域名回插图片检查插入的img标签是否在预期位置样式是否正常replaceWith时容易丢失父节点结构前端压缩大图先压缩再上传减少带宽和存储压力压缩不能糊病历影像要保留可辨识度浏览器兼容至少覆盖Chrome/Edge/IE11三个环境IE11事件对象没有dataTransfer需要单独处理结尾我在实际HIS项目实施中体会最深的一点是这个功能的技术难点不在“上传图片”本身而在“你永远不知道医生是从哪个版本的Word、哪种浏览器、哪台有安全策略的电脑上复制过来的”。代码写完之后务必让科室的医生真实用一周把反馈收集起来再迭代一轮。最后再分享一个小技巧——给上传成功的图片统一加一个CSS类名比如word-upload-img这样病案打印和归档导出的样式调整只需要改一段样式就能全盘生效省后续很多麻烦。
返回列表