ARTICLE DETAIL

资讯详情

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

医院HIS系统富文本编辑器截图粘贴插件选型与实现

医院HIS系统富文本编辑器截图粘贴插件选型与实现 做医院信息系统HIS这行的几乎都撞上过同一个需求病历、会诊单、检验报告说明里要贴截图。外部系统的检验结果没有接口能对接患者把手机拍的检查报告发到微信群里最省事的办法就是把图直接粘进编辑器。但医院环境不是互联网项目内网部署、浏览器老旧、数据安全要求高网上那些“装个富文本编辑器再配个粘贴插件”的思路到现场基本翻车。这篇文章专门回答一个问题医院信息系统到底需要哪种编辑器截图粘贴插件。我会从选型思路、技术原理、核心代码到排坑实录一次性讲透项目刚启动、正在做编辑器选型或改造的朋友可以直接参考。先把结论放这儿医院场景最稳的组合是“成熟富文本编辑器 自研截图粘贴上传插件”。编辑器本体按项目环境选——老系统优先UEditor新建的内网Web项目可以用WangEditor但截图粘贴、图片压缩、内网上传这三个环节尽量自己写不要指望现成插件能满足压缩、改尺寸、权限校验这些医院特有的要求。很多人搜“编辑器”时容易和代码编辑器、PDF编辑器搞混这里先说明白HIS里的编辑器指的是电子病历、报告录入、文书编辑用的富文本编辑器不是写代码的IDE。1. 医院信息系统的真实约束选型先看环境再看功能1.1 内网环境掐死了“在线方案”医院核心业务系统绝大多数跑在院内网或专网上互联网不能随便访问CDN、npm registry、国外字体和插件源统统用不了。很多项目连构建打包都只能在开发机完成产物拷进内网再部署。这意味着所有依赖在线加载资源的编辑器方案几乎可以一票否决。选型第一件事就是把JS、CSS、语言包、图标字体全部本地化保证断网环境下首屏加载、粘贴、上传全部正常。我在一个客户现场亲眼见过实施同事图省事引了一条CDN的UEditor地址打开页面直接白屏查了半天才发现是内网安全策略把外网请求全拦了。所以所有静态资源必须下载好放进项目目录这个规矩没有商量余地。内网环境的浏览器更是五花八门。老旧办公电脑上跑的是IE11Win7升不了系统的只能用360企业版、奇安信浏览器这种带Chrome内核的“套壳”浏览器新一点的科室主任电脑装了Chrome 90护士站那台可能还是老Edge。编辑器选型不能只看官方文档写着“支持Chrome、Firefox、Safari”得实际拿现场浏览器逐个跑一遍才算数。1.2 数据安全红线影响上传链路医疗数据涉及患者隐私图片本身就是病历的一部分。院内对数据流通常有明确要求图片不能走公网上传接口要带鉴权存储位置要和业务系统在同一安全域内。这些要求直接决定了截图粘贴插件的设计方式。具体来说上传接口不能是裸奔的POST端点至少要有会话校验或token鉴权文件名不能沿用剪贴板里的原始文件名中文、空格、特殊字符都要重写避免路径注入和乱码图片存储目录要做访问控制病历相关图片不能允许任意路径遍历读取。架构上图片要跟主库数据一起备份大型医院还会要求保留上传日志便于追溯所以插件前端要把操作记录也加上。很多现成的截图粘贴插件完全没考虑这些直接在开放接口收文件这种方案在医院环境过不了验收。1.3 常见截图场景决定了功能需求我梳理过医院里实际会用到截图粘贴的典型场景大致分这么几类门诊医生把患者通过微信或其他渠道发来的外院检验报告、病历照片粘贴到门诊病历作为诊断参考。住院医生在病程记录里贴手术照片、伤口护理照片并追加文字说明。会诊场景把外院光盘翻拍的影像图片、远程会诊平台的截图贴进会诊意见。体检中心在总检报告里贴异常项截图、历史对比图。医务科/质控不良事件上报时贴证据截图、系统操作界面截图。这些场景有几个共同点图片来源杂微信、QQ、系统截图工具、相机翻拍都有图片质量参差不齐尺寸大、倾斜、反光而且需要快速插入并保存到服务器。也就是说插件必须能处理剪贴板里的PNG位图也能处理从文件管理器复制的JPG图片同时要压缩到合理大小否则一年下来存储和数据库都会吃不消。2. 编辑器本体的选型主流方案的横向对比2.1 UEditor医院老系统的“常青树”UEditor是百度开源的老牌富文本编辑器最后稳定版本停留在1.4.3.3。虽然官方早就不维护了但在医院系统里依然是出现频率最高的编辑器之一原因就一条兼容性好。UEditor对IE11表现稳定对各类国产浏览器内核适配也不错自带粘贴Word内容、本地图片上传、远程图片抓取等一整套功能开箱即用部署时把代码拷进内网就能跑。缺点是代码体积大、源码老、依赖jQuery打包优化费劲。更重要的是安全问题老版本有一些已知漏洞部署前要做一次代码审计把后端上传接口的校验补齐比如限制文件类型、校验文件大小、对文件名做白名单处理。如果项目是传统的JSP、ASP.NET MVC或者基于jQuery的老管理系统选UEditor基本不会错。2.2 WangEditor轻量集成的新选择WangEditor是国产编辑器的后起之秀v5版本用TypeScript重写体积小、API清爽官方提供完整的自定义菜单和插件机制集成Vue、React都很顺手。对医院新建的、跑在现代化内网Web框架里的项目来说WangEditor是个好选择。但有两个坑要提前注意。第一官方配置示例里大量使用CDN地址内网部署时要手动把所有资源本地化。第二它默认粘贴图片是转成base64内联进内容的这在医院是灾难——一张图几MB一条病历记录可能几十MB数据库直接爆炸。必须配置成上传到服务器并返回URL这一点后面代码部分会详细说。另外WangEditor对IE基本放弃现场还有IE11的话不要选它。2.3 TinyMCE、CKEditor和Quill各有各的门槛TinyMCE功能强大插件生态丰富但体积偏大自托管配置复杂语言包和主题资源要处理干净中文资料相对少医院那种追求“少折腾”的维护团队用起来压力不小。CKEditor 4兼容性不错、配置灵活但架构偏老而且官方已停止4.x的功能更新CKEditor 5彻底重构后对IE和旧浏览器支持没了API与4不兼容迁移成本高。Quill走的是现代、简洁路线API优雅但默认没有强大的图片上传体系粘贴表格、复杂排版场景要写大量自定义模块开发成本不算低。这几个方案不是不好而是对医院项目来说要么学习成本偏高要么兼容性赌注太大。除非团队有很强的前端自研能力否则我一般不推荐作为首选。2.4 一张表看清选型方向我把几个主流编辑器在关键维度上的表现整理成表格方便直接对照决策。维度UEditor 1.4.xWangEditor v5TinyMCE 5/6CKEditor 4Quill内网离线部署容易容易需要完整打包容易容易IE11兼容好差一般好差中文资料多多中等中等少图片上传能力自带需自配需插件需插件需自研代码体积大小大中小维护状态停滞活跃活跃停更(4.x)活跃医院项目推荐度高老系统高新系统中中低一句话概括选型方向改造老项目、浏览器环境差优先UEditor新建内网项目、技术栈较新优先WangEditor有专门前端团队做深度定制再考虑TinyMCE。不管选哪个截图粘贴插件都要单独写编辑器自带的粘贴能力多半不够用。3. 截图粘贴插件核心原理与实现思路3.1 剪贴板里到底有什么paste事件与clipboardData浏览器在用户执行粘贴操作CtrlV或右键粘贴时会触发paste事件事件对象上带一个clipboardData属性。clipboardData里有items和files两个核心数据源items是DataTransferItem列表每一项带type属性如text/html、text/plain、image/png要做截图粘贴核心就是从items里找出type以image/开头的项再调用getAsFile()拿到真正的File对象。这里有个关键细节截图工具截完图后剪贴板里放的是位图粘贴时type通常是image/png而从文件管理器复制一个JPG文件再粘贴type会出现image/jpeg有时候items里还会多一个Files项。插件要兼容三种情况直接粘贴图片数据、粘贴图片文件、以及两者同时存在时避免重复处理。还有一种特殊情况用户从Excel或网页里复制表格粘贴到编辑器的内容是text/html如果插件只关心图片应该放行让编辑器正常处理如果插件不加判断直接preventDefault会把用户复制表格的功能也弄坏。所以正确做法是进入paste事件后先遍历items没有图片就return把控制权还给编辑器。3.2 图片预处理三步曲格式、尺寸、体积剪贴板里的截图和图片文件并不是拿来就能上传的。手机翻拍的照片一张可能5MB、宽度6000像素微信截图虽然是PNG但好几MB也不少见。直接把原始文件传上去存储增长快、页面加载慢、接口带宽也扛不住。所以上传前必须做三步预处理。第一步统一格式。图片最终建议全部转成JPEG后缀统一为.jpg除非原图是带透明通道的PNG比如某些系统导出的图标这种情况单独保留PNG防止黑底。第二步限制尺寸。设置一个最大宽度一般取1920px超出的按比例缩放用canvas重绘。第三步控制质量。JPEG质量参数取0.7到0.85目标是把单张图控制在300KB以内。我通常先压到0.8质量如果压缩后仍超过500KB再把质量降到0.6或把最大宽度降到1280px两轮压缩基本覆盖所有情况。处理透明PNG时有个容易踩的坑canvas输出JPEG默认把透明部分填成黑色病历里拍的白底文档、检查报告如果变成大片黑底非常难看。解决办法是在canvas绘制前先填充白色背景再画原图几行代码低成本解决。3.3 上传与回显和编辑器打通图片处理完接下来是上传。医院系统一般都有现成的文件或图片上传接口但很多接口原本是给表单选文件用的直接收File对象。做法是把压缩后的Blob包装成FormData以文件形式提交如果后端要求base64就用FileReader把Blob转成DataURL再提交。上传成功后会返回URL拿到URL后要把图片插入编辑器当前光标位置。不同编辑器的插入API不一样UEditor可以用execCommand(insertimage, {src: url})WangEditor可以调用插入节点的方法。关键是等图片上传成功后再插入防止用户粘贴后立刻保存导致图片丢失。还有两个工程细节容易被忽略。一是并发控制用户可能连贴五六张图一下子发出多个上传请求内网服务器和数据库容易卡顿建议做一个简单的上传队列一次最多并行两个。二是失败回滚上传失败要给明确提示不要往编辑器里插入脏数据。4. 完整代码实战手写一个可落地的截图粘贴插件4.1 基础版监听paste事件拿到图片先写一个最小的可用版本。这个版本只做一件事监听页面paste事件发现剪贴板里有图片就取出来交给后续处理函数。document.addEventListener(paste, function (e) { const clipboardData e.clipboardData || window.clipboardData; if (!clipboardData || !clipboardData.items) return; for (let i 0; i clipboardData.items.length; i) { const item clipboardData.items[i]; if (item.type item.type.indexOf(image) ! -1) { const file item.getAsFile(); if (file) { e.preventDefault(); handlePastedImage(file); } break; } } }); function handlePastedImage(file) { console.log(粘贴到图片, file.name, file.type, file.size); }这里有个兼容细节老内核浏览器对剪贴板数据访问有限制如果items为空或getAsFile返回null要考虑降级方案。另外有些国产浏览器在iframe页面里粘贴时父页面监听不到子页面的paste事件监听器必须挂到iframe的document上这一点到第5章还会展开讲。4.2 增强版压缩、上传、插入编辑器接下来把完整链路串起来包括图片压缩、上传队列和插入回显三部分。图片压缩函数我直接贴常用版本function compressImage(file, maxWidth, quality) { return new Promise(function (resolve, reject) { const reader new FileReader(); reader.onload function (e) { const img new Image(); img.onload function () { let w img.width; let h img.height; if (w maxWidth) { h Math.round(h * maxWidth / w); w maxWidth; } const canvas document.createElement(canvas); canvas.width w; canvas.height h; const ctx canvas.getContext(2d); ctx.fillStyle #ffffff; ctx.fillRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h); canvas.toBlob(function (blob) { if (blob blob.size 500 * 1024) { canvas.toBlob(function (blob2) { resolve(blob2 || blob); }, image/jpeg, Math.max(0.4, quality - 0.2)); } else { resolve(blob); } }, image/jpeg, quality); }; img.onerror function () { reject(new Error(图片解析失败)); }; img.src e.target.result; }; reader.onerror function () { reject(new Error(文件读取失败)); }; reader.readAsDataURL(file); }); }这段代码做了几件事FileReader把图片文件读成DataURL用Image对象拿到真实宽高超宽图片等比缩放canvas绘制前用白色填充背景解决透明PNG转JPEG黑底的问题用canvas.toBlob输出JPEG并加了一轮大文件兜底压缩。toBlob在部分老浏览器不支持可以降级用toDataURL(image/jpeg, quality)再转Blob。上传函数function uploadImage(blob, uploadUrl) { const formData new FormData(); const filename paste_ Date.now() _ Math.random().toString(36).slice(2) .jpg; const file new File([blob], filename, { type: image/jpeg }); formData.append(file, file); return fetch(uploadUrl, { method: POST, body: formData, credentials: include }).then(function (res) { if (!res.ok) throw new Error(上传失败 res.status); return res.json(); }).then(function (data) { if (!data || !data.url) throw new Error(上传接口未返回url); return data.url; }); }文件名用时间戳加随机串重新生成不保留原始文件名避免中文和特殊字符。fetch里带credentials保持内网会话如果项目用token鉴权就在请求头里加认证信息。4.3 插入编辑器以UEditor为例封装插件拿到图片URL后插入位置必须落在用户光标处。UEditor的做法是调用execCommand(insertimage传入对象包含src属性。封装成插件类function PasteImagePlugin(editor, options) { this.editor editor; this.uploadUrl options.uploadUrl || /api/upload/image; this.maxWidth options.maxWidth || 1920; this.quality options.quality || 0.8; this.queue []; this.activeCount 0; this.init(); } PasteImagePlugin.prototype.init function () { const self this; self.editor.ready(function () { self.editor.addListener(paste, function (t, e) { const clipboardData e.clipboardData || window.clipboardData; if (!clipboardData || !clipboardData.items) return; for (let i 0; i clipboardData.items.length; i) { const item clipboardData.items[i]; if (item.type item.type.indexOf(image) ! -1) { const file item.getAsFile(); if (file) { e.preventDefault(); self.enqueue(file); } break; } } }); }); }; PasteImagePlugin.prototype.enqueue function (file) { this.queue.push(file); if (this.activeCount 2) { this.next(); } }; PasteImagePlugin.prototype.next function () { const self this; if (self.queue.length 0) { self.activeCount 0; return; } self.activeCount 1; const file self.queue.shift(); compressImage(file, self.maxWidth, self.quality) .then(function (blob) { return uploadImage(blob, self.uploadUrl); }) .then(function (url) { self.editor.execCommand(insertimage, { src: url }); }) .catch(function (err) { console.error(err); alert(截图上传失败 err.message); }) .finally(function () { self.activeCount 0; if (self.queue.length 0) { self.next(); } }); };这样封装后换成WangEditor只改插入图片那一步即可。还有两个细节如果有多个编辑器实例监听器只挂到当前实例避免其他区域粘贴也被截获插入图片前如果用户光标移到了别处最好在弹层或提示里让用户重新点一下编辑区再操作。5. 能省半年维护的部署细节这些坑别等上线才踩5.1 浏览器兼容性不能只看文档医院现场的环境远没有开发机干净。我遇到的真实问题包括IE11里clipboardData.items存在但getAsFile返回null国产浏览器在iframe页面里粘贴不触发paste事件老版Chrome对canvas.toBlob支持不完整。建议插件编写时就做好兼容降级检测到getAsFile不可用时回退读取clipboardData.files。不支持toBlob时用canvas.toDataURL(image/jpeg, quality)替代。在页面加载时做一次能力检测把不支持的功能在控制台打印出来方便现场联调。上线前拿IE11、Chrome 80、360安全浏览器、奇安信浏览器各跑一遍粘贴测试把测试结果记录成表格存档。这个动作能省掉上线后一大半的运维工单。5.2 别忘了“粘贴后立即保存”的时序问题临床科室的使用习惯是粘贴图片、点保存一气呵成。如果插件是异步上传用户粘贴完立刻点保存此时图片可能还没上传完、还没插入编辑器保存出去的病历里就没有这张图。这是HIS截图粘贴功能最容易翻车的问题。解决思路有三个层面。第一上传期间在编辑器内插入占位图用户能直观看到“图片正在上传”保存前提示等待。第二表单保存时检查上传队列是否为空有未完成任务就阻止保存并给出提示。第三如果业务确实等不了异步可以考虑转base64插入虽然不推荐但某些小系统为了保底数据体积的代价可以接受。我在实际项目里通常选“占位图保存拦截”组合既直观又稳妥。5.3 图片存储策略与定期清理内网服务器磁盘往往不大。一个地市级医院病历图片一天新增几百到上千张很正常每张300KB一年下来就是上百GB。存储策略上建议按年/月建目录比如/upload/2025/03/便于文件系统读写和定期归档。还要定期清理“从未被引用的图片”这些图片是用户上传失败、放弃保存、测试产生的孤儿文件。清理任务放在凌晨低峰期跑遍历内容字段里的图片引用把数据库中不存在的文件找出来超过90天未引用就移到回收目录人工确认后再删除。注意涉及病历的正式图片不能随意删除这个策略主要针对临时目录和无主文件正式引用的图片必须归档保留。这个脚本虽然简单但能防止磁盘悄悄爆掉。6. 常见问题排查实录与速查表6.1 粘贴没反应先查这三处页面粘贴完全没反应十有八九不是插件坏了而是事件没触达。第一个排查点焦点是否在可编辑区域。paste事件只在用户聚焦到编辑器时可靠触发如果焦点在按钮或空白区域很多浏览器不向页面派发paste事件。第二个排查点iframe。HIS里的编辑器经常嵌在iframe中父页面监听不到子iframe的paste监听器必须挂在iframe文档上。第三个排查点其他全局监听器。有些项目为了限制粘贴内容全局注册了paste处理并调用preventDefault但没有做图片透传结果把所有粘贴都拦了。6.2 图片上传失败的五种典型表现上传看起来简单实际报错花样很多。接口返回403通常是会话过期或token没带需要先做会话检查。返回413是请求体太大多半是压缩没生效原始图片直接提交了要检查压缩流程是否被跳过。返回500先看后端日志常见原因是上传目录无写权限、磁盘满了、文件名含特殊字符。连接超时常见于图片过大或走了代理处理方式是压缩得更狠必要时分片上传。还有一种隐蔽情况接口返回200但url字段缺失前端报“未返回url”这通常是后端返回的数据结构和前端预期不一致联调时先对齐字段名。6.3 排查速查表现象可能原因处理建议粘贴完全没反应焦点丢失、iframe监听缺失、全局拦截确认焦点在编辑器内监听器挂到iframe文档检查其他paste监听图片变形没有等比例缩放或canvas尺寸被拉伸按原始宽高比计算缩放核实drawImage参数透明图变黑底JPEG输出未处理alpha通道canvas绘制前填充白色背景上传报403会话失效、token未带刷新session请求头带上认证信息上传报413图片体积过大检查压缩逻辑是否生效降低质量参数上传超时图片过大或走了代理加强压缩必要时给上传接口加重试保存后图片丢失异步上传未完成就保存保存前检查上传队列或用占位图方案数据库记录超大编辑器把图片存成base64配置关闭base64粘贴改为上传返回URL排查这类问题我的习惯是先把浏览器开发者工具的Network面板打开看粘贴瞬间有没有发出上传请求。有请求就看返回状态和数据没请求就回到事件监听链路找问题。沿着“事件是否触发→数据是否拿到→压缩是否完成→请求是否发出→响应是否正常→是否插入成功”逐段定位基本没有查不出来的。最后说点个人经验。医院信息系统做截图粘贴最大的敌人不是技术而是现场环境的复杂程度。同一个插件开发机一切正常到了客户现场可能在旧浏览器上没反应也可能被某个分院的网络策略拦掉。所以我现在坚持几条原则插件里所有功能点都能降级处理能不用Polyfill就不用依赖越少越好上线前带一份现场测试清单把主流浏览器和常见截图工具各测一遍编辑器选型没有绝对最好只有最匹配把环境约束、团队能力、维护成本放在功能对比前面选出来的方案才真正能落地。如果你正在做医院项目的编辑器改造建议先调研现有技术栈和客户现场的浏览器分布再对照这篇文章里的选型表和代码去实现能少走不少弯路。
返回列表