@vsoft_liu/vsoft-emr
v1.0.159
Published
Vue 3 电子病历编辑器组件,基于 ThinkEditor SDK
Maintainers
Readme
@vsoft_liu/vsoft-emr — ThinkEditor Vue3 组件
基于 ThinkEditor SDK 封装的 Vue3 电子病历编辑器组件,提供完整的电子病历编辑功能。
文件结构
VsoftEmr/
├── index.ts # 入口文件,导出组件、类型和枚举
├── VsoftEmr.vue # 主组件文件(SDK 封装层)
├── types.ts # TypeScript 类型定义
├── enums.ts # 枚举定义(统一 re-export @vsoft_liu/vsoft-editor 的 VS_ENUM_* 枚举)
├── config/ # 配置系统
│ ├── EmrEditorConfig.ts # 主配置接口(含字段类型接口和映射函数)
│ ├── EmrServiceProviders.ts # 服务提供者接口
│ ├── rolePresets.ts # 角色预设配置
│ ├── (mock 服务已移至 demo 层 src/mock/,组件库不包含 mock)
│ ├── utils.ts # 配置工具函数(deepMerge 等)
│ └── injectionKeys.ts # Vue provide/inject 键
├── full/ # 完整编辑器 UI
│ ├── FullEditor.vue # 主文件(工具栏 + 侧边面板 + 状态栏)
│ ├── EditorToolBar.vue # 主工具栏
│ ├── DocToolBar.vue # 文档工具栏(由 VsoftEditor 提供)
│ ├── StatusBar.vue # 底部状态栏
│ ├── DocTabsBar.vue # 多文档标签栏
│ ├── panels/ # 面板组件
│ │ ├── LeftPanel.vue # 左侧面板(独占手风琴)
│ │ ├── RightPanel.vue # 右侧面板(属性面板)
│ │ ├── FieldGroup.vue # 字段分类递归渲染组件
│ │ ├── OutlineTreeNode.vue # 大纲树节点组件
│ │ └── content/ # 右侧面板内容组件
│ ├── toolbars/ # 工具栏子组件
│ ├── composables/ # useTheme.ts 等
│ ├── hooks/ # 组合式函数
│ ├── dialogs/ # 对话框组件
│ ├── styles/ # CSS 样式
│ └── icons.ts # SVG 图标
└── README.md # 说明文档快速开始
1. 安装
pnpm add @vsoft_liu/vsoft-emr
# 或
npm install @vsoft_liu/vsoft-emr
@vsoft_liu/vsoft-emr内部通过@vsoft_liu/vsoft-editor依赖自动管理 SDK 运行时(含 .wasm 文件)。
2. 引入组件
import { VsoftEmr, VS_ENUM_VIEW_MODE, VS_ENUM_DOC_TYPE, VS_ENUM_DOC_FORMAT } from '@vsoft_liu/vsoft-emr'
import '@vsoft_liu/vsoft-emr/styles'3. 使用组件
<template>
<VsoftEmr
ref="emrRef"
:lib-path="libPath"
:font-path="fontPath"
width="100%"
height="100%"
:view-mode="VS_ENUM_VIEW_MODE.Edit"
@ready="onReady"
@doc-modified="onModified"
/>
</template>
<script setup>
import { ref } from 'vue'
import { VsoftEmr, VS_ENUM_VIEW_MODE, VS_ENUM_DOC_TYPE, VS_ENUM_DOC_FORMAT } from '@vsoft_liu/vsoft-emr'
import '@vsoft_liu/vsoft-emr/styles'
const emrRef = ref(null)
const libPath = '/think-editor-sdk/'
const fontPath = '/fonts/'
const onReady = (editor) => {
console.log('编辑器就绪', editor)
// 新建文档
emrRef.value.newDoc('myDoc', VS_ENUM_DOC_TYPE.Template)
}
const saveDoc = () => {
const xml = emrRef.value.getDoc(VS_ENUM_DOC_FORMAT.XML, VS_ENUM_DOC_TYPE.Entity)
console.log('文档数据:', xml)
}
</script>组件 Props
| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | libPath | string | '' | SDK库文件路径 | | fontPath | string | '' | 字体文件路径 | | width | number/string | '100%' | 容器宽度 | | height | number/string | '100%' | 容器高度 | | viewMode | VS_ENUM_VIEW_MODE | VS_ENUM_VIEW_MODE.Edit | 视图模式 | | displayScale | number | 1 | 显示缩放比例 | | pagesLayoutMode | number | 1 | 页面布局模式 | | docName | string | '' | 文档名称 | | docType | VS_ENUM_DOC_TYPE | VS_ENUM_DOC_TYPE.Template | 文档类型 | | editable | boolean | true | 是否可编辑 | | docOpenMode | 'single' | 'multi' | 'single' | 文档打开模式(单文档/多文档) | | showToolbar | boolean | false | 已废弃,请使用 FullEditor 组件 | | showStatusBar | boolean | false | 是否显示内置状态栏 | | enableContextMenu | boolean | false | 是否启用右键菜单 | | enableShortcutKeys | boolean | false | 是否启用快捷键 |
组件 Events
| 事件名 | 参数 | 说明 | |--------|------|------| | ready | vsEditor: VSEditor | null | 编辑器就绪(多文档模式为 null) | | load | data: any | 文档加载完成 | | unload | - | 文档卸载 | | focus | - | 获得焦点 | | blur | - | 失去焦点 | | error | error: Error | 错误事件 | | docChange | docName: string | 文档切换 | | docModified | modified: boolean | 文档修改状态改变 | | focusChange | properties: FocusProperties | 焦点属性改变 | | elementChange | data: any | 元素改变 | | undoCountChange | count: number | 撤销次数改变 | | redoCountChange | count: number | 重做次数改变 | | viewModeChange | mode: VS_ENUM_VIEW_MODE | 视图模式改变 | | scaleChange | scale: number | 缩放比例改变 | | fontFormatChange | data: any | 字体格式改变 | | paragraphStyleChange | data: any | 段落样式改变 | | printDoc | data: any | 打印文档 | | closeDoc | docName: string | 关闭文档 | | requestSources | data: any | 请求源 | | requestFont | data: any | 请求字体 | | shortcut | keyCombo: string, event: KeyboardEvent | 快捷键触发(由 FullEditor 处理) | | saveDoc | docName: string | 保存文档(Ctrl+S) |
内置 UI 功能
VsoftEmr 组件支持内置工具栏、状态栏、右键菜单和快捷键功能,可通过 props 开启。
FullEditor 内置 UI
通过 FullEditor 组件可获得完整编辑器体验:菜单栏 + 工具栏 + 左右侧边面板 + 状态栏。
工具栏/菜单栏禁用状态:按钮受 isReady(SDK 初始化)和 hasDoc(有文档打开)双重控制。无文档时,除新建/打开/面板切换/主题外的按钮全部 disabled。元素库内容在无文档时也禁用。
两侧面板:LeftPanel 为独占手风琴模式(同一时间只展开一个面板,展开的面板占据剩余空间,内部滚动)。RightPanel 为手风琴模式。宽度可通过拖拽分隔条调整(150~600px)。拖到最小宽度后持续向内拉 1.5 秒自动关闭面板。
菜单栏右侧:显示当前用户名称和科室铭牌(1px 焦点色边框 + 渐变背景),数据来自 config 的 user.userName 和 user.department。
左侧面板:包含任务、模板、患者文档、文档大纲、元素库、部件库、快速短语等手风琴面板(按角色配置可见性)。面板顺序和可见性由配置文件的 PanelTabConfig.order 字段驱动(动态渲染,非硬编码顺序)。"模板"和"患者文档"的数据通过不同渠道注入:
// services(后端服务,含模板/字段数据)
services: {
onLoadTemplateList: async () => [...], // 返回 TemplateTreeNode[]
onLoadTemplate: async (id) => `<xml>...</xml>`, // 返回模板 XML
onLoadFieldScenarios: async () => [...], // 返回 FieldScenario[]
onLoadFieldData: async (scenarioId?) => [...], // 返回 FieldDataCategory[]
}
// providers(仅 tasks——静态/响应式数据)
providers: {
tasks: TaskItem[], // 任务列表数据(响应式)
}
// panelProviders(面板特定数据——仅 patientDocs)
panelProviders: {
patientDocs: async (type, ...args) => {...}, // 患者文档树数据
}单击节点选中,双击触发 open-template / open-patient-doc 事件。右键树节点弹出各自独立的上下文菜单(新建/重命名/删除),通过 template-menu-action / patient-doc-menu-action 事件传出。Mock 数据在 src/mock/mockServices.ts。
患者管理(HIS Shell 层):患者列表侧边栏在 App.vue 管理(非编辑器组件内部),仅 Doctor/Nurse/Director 角色可见。点击患者自动切换到患者文档面板(switchLeftTab('patientDocs')),患者信息卡片显示在患者文档面板顶部(床号/姓名/诊断/护理级别/住院天数/出院标记)。
菜单栏:单行强制不换行,每个下拉菜单项显示 SVG 图标(圆角 + 填充形状风格),约 80 个菜单项通过 MENU_ICON_MAP 映射。
任务列表面板:标题带角标显示任务数量(0 显示"无"),数据通过 providers.tasks 注入,响应式更新。点击任务触发 task-click 事件。
快速短语面板:按分类显示常用短语(主诉、体格检查等),支持拖拽和点击插入。数据通过 services.onLoadQuickPhrases 懒加载,展开面板时自动加载。仅 Doctor/Nurse 角色可见。
多视图标签页:中部主体区域支持编辑器文档和自定义视图共存。通过 registerTab API 注册自定义标签(如影像查看器、检验面板),通过 #custom-view slot 渲染自定义内容。DocTabsBar 根据 tab.type 显示不同样式(自定义标签支持自定义图标和颜色)。
// 注册自定义视图
fullRef.registerTab({
key: 'exam:ct-001',
title: '胸部CT报告',
type: 'custom',
icon: '🔬',
color: '#e67e22',
})配置上下文层级:
{
user: { userId, userName, department, title, licenseNumber, signatureUrl, ... }, // 归属科室
organization: { orgId, orgName }, // 部署级
department: { department, ward }, // 会话级(工作科室)
consultation: { consultationId, type, role }, // 会诊时
patient: { patientId, name, department, bedNo, nursingLevel, status }, // 就诊级
}FullEditor 额外暴露的 API:
switchLeftTab(tabId: string)— 展开左侧面板的指定 tab(同时更新activeTab和expandedSections),用于患者切换时自动跳转registerTab/unregisterTab/switchToTab— 多视图标签页管理
开启内置 UI
<VsoftEmr
:show-toolbar="true"
:show-status-bar="true"
:enable-context-menu="true"
:enable-shortcut-keys="true"
@shortcut="onShortcut"
@save-doc="onSaveDoc"
/>内置工具栏按钮
| 分组 | 按钮 | |------|------| | 文件 | 新建、打开、保存、打印 | | 剪贴板 | 撤销、重做、复制、剪切、粘贴 | | 格式 | 加粗(B)、倾斜(I)、下划线(U)、删除线(S) | | 段落 | 左对齐、居中、右对齐 | | 插入 | 表格、图片、公式 | | 视图 | 放大、缩小、缩放显示 |
快捷键支持
| 快捷键 | 功能 | |--------|------| | Ctrl+Z | 撤销 | | Ctrl+Y / Ctrl+Shift+Z | 重做 | | Ctrl+C | 复制 | | Ctrl+X | 剪切 | | Ctrl+V | 粘贴 | | Ctrl+A | 全选 | | Ctrl+B | 加粗 | | Ctrl+I | 倾斜 | | Ctrl+U | 下划线 | | Ctrl+S | 保存(触发 saveDoc 事件) | | Ctrl+P | 打印 | | Ctrl+= | 放大 | | Ctrl+- | 缩小 | | Delete/Backspace | 删除选中内容 |
右键菜单
- 撤销、重做
- 分隔线
- 复制、剪切、粘贴、删除、全选
- 分隔线
- 格式子菜单(加粗、倾斜、下划线)
- 插入子菜单(表格、图片)
状态栏显示
- 文档名 / 文档数量(多文档模式)
- 修改状态(已修改/未修改)
- 撤销/重做计数
- 视图模式
- 缩放比例
暴露的 API 方法
组件通过 defineExpose 暴露以下方法,可通过 ref 调用:
文档操作
// 新建空文档
newDoc(docName: string, docType?: E_DOC_TYPE): boolean
// 创建文档
createDoc(docName: string): any
// 解析文档
parseDoc(docName: string, xml: string | ArrayBuffer, jsonCfg?: object): boolean
// 获取文档数据
getDoc(docFormat?: E_DOC_FORMAT, docType?: E_DOC_TYPE, jsonCfg?: object): string
// 获取PDF
getPDF(jsonCfg?: object): Promise<any>
// 获取页影像
getPagesImage(imageType?: E_IMAGE_TYPE, startIndex?: number, pagesCount?: number, scale?: number): any
// 关闭文档
closeDoc(docName: string): void
// 选择文档
selectDoc(docName: string, jsonCfg?: object): any
// 显示文档
visibleDoc(docName: string): any编辑操作
// 撤销/重做
undo(count?: number): void
redo(count?: number): void
// 复制/剪切/粘贴
copy(): void
cut(): void
paste(pasteType?: E_PASTE_TYPE): Promise<void>
deleteSelection(): void
selectAll(): any
// 格式刷
setFormatBrush(brushState: E_BRUSH_STATE): void
clearFormat(): void字体格式
setFontType(fontName: string): void
setFontSize(fontSize: number): void
setFontSizeItem(fontItem: string): any
setFontColor(color: string | number): void
setFontBackColor(color: string | number): void
setFontBold(bold: boolean): void
setFontItalic(italic: boolean): void
setFontUnderline(underline: boolean): void
setFontStrikethrough(strikethrough: boolean): void
setFontScript(scriptMode: E_SCRIPT_MODE): void
setFontFerrule(mode: E_FERRULE_MODE): void
displayFontSetWindow(displayMode: E_DISPLAY_MODE): void段落操作
insertParagraph(jsonCfg?: object): boolean
deleteParagraph(jsonCfg: object): boolean
setParagraphConfig(identity: object, config: object): boolean
setParagraphContent(id: string, content: string, jsonCfg?: object): any
setParagraphAlignContent(alignmentHorizontalMode: E_ALIGN_HORIZONTAL_MODE): void
setParagraphListMode(listMode: E_LIST_MODE): any
setParagraphListLevel(listLevel: E_LIST_LEVEL): void
setParagraphLeftIndent(indent: number): void
setParagraphRightIndent(indent: number): void
setParagraphSpaceBefore(space: number): void
setParagraphSpaceAfter(space: number): void
setParagraphLineSpacingRule(rule: number): void
setParagraphLineSpacingValue(value: number): void表格操作
insertTable(jsonCfg: TableConfig): boolean
deleteTable(identity: object): boolean
getTableProperties(identity: object, jsonCfg?: object): any
setTableConfig(id: string, jsonCfg: object): boolean
setTableData(identity: object, tableXml: string | ArrayBuffer, config?: object): boolean
setCellContent(param: object): boolean
tableInsertOpt(insertMode: E_TABLE_INSERT_MODE): void
tableDeleteOpt(deleteMode: E_TABLE_DELETE_MODE): void
cellOpt(mode: E_CELL_OPT_MODE): void元素操作
insertElement(jsonCfg: ElementConfig): boolean
deleteElement(id: string, jsonCfg?: object): boolean
setElementConfig(identity: object, config?: object): boolean
setElementContent(id: string, content: string, jsonCfg?: object): boolean
setElementsContent(paramArr: object[], jsonCfg?: object): boolean
getElementProperties(identity?: object, config?: object): any[]
getElementsContent(identity: object, jsonCfg?: object): any[]
setElementIdentity(id: string, newId: string, newName: string): boolean
setElementBase(id: string, hint: string, tip: string, startBorder: string, endBorder: string, beforeTag: string, afterTag: string): boolean
setElementInputText(id: string): boolean
setElementInputNumber(id: string): boolean
setElementSelectOption(id: string, optionClass: string, optionId: string, itemLinkStr: string, elementCfg: any): any
setElementSelectDateTime(id: string, dateType: E_TIME_TYPE): any图片操作
insertImage(id: string, width: number, height: number, filename: string, arrayBuffer: ArrayBuffer, dataLen: number, jsonCfg?: object): string
insertImageBase64(id: string, width: number, height: number, filename: string, base64Str: string, base64StrLen: number, jsonCfg?: object): boolean
setImageConfig(id: string, jsonCfg: object): boolean
setsetImageResource(imageName: string, arrayBuffer: ArrayBuffer, dataLen: number, jsonCfg?: object): string
setsetImageResourceBase64(fileName: string, base64Str: string, base64StrLen: number, jsonCfg?: object): string条形码/二维码
insertBarCode(jsonCfg: BarcodeConfig): boolean
getBarCodeList(): object
getBarCodeClass(barcodeType: E_BARCODE_TYPE): number
calcBarCodeProperties(jsonCfg: object): object
setBarCodeConfig(id: string, jsonCfg: object): boolean其他插入
// 公式
insertFormula(jsonCfg: FormulaConfig): boolean
getFormulaProperties(jsonCfg?: object): object
setFormulaConfig(id: string, jsonCfg: object): boolean
// 选框
insertCheckBox(jsonCfg: CheckBoxConfig): boolean
insertCheckBoxGroup(jsonCfg: object): boolean
getCheckBoxProperties(identity?: object): object
setCheckBoxConfig(identity: object, config: object): boolean
// 页码
insertPageNum(jsonCfg: object): boolean
setPageNumConfig(id: string, jsonCfg: object): boolean
// 特殊对象
insertPageFeed(count?: number): any
insertLineFeed(count?: number): any
insertSeparator(width: number, height: number, jsonCfg: object): boolean
insertButton(jsonCfg: object): any
insertAnnotate(id: string, fragment: string, jsonCfg?: object): any
// 批注
insertComment(id: string, fragment: string, jsonCfg?: object): any
deleteComment(jsonCfg: object): boolean
insertReplyComment(fragment: string, jsonCfg?: object): any
deleteReplyComment(jsonCfg: object): boolean
setCommentResolve(jsonCfg: object): boolean
selectComments(jsonCfg?: object): any
getCommentReport(): any视图操作
setViewMode(viewMode: E_VIEW_MODE, cfg?: object): void
setDisplayScale(pagesLayoutMode: number, value: number): void
resizeViewPanel(width: number, height: number): boolean水印设置
setWaterMarkConfig(id: string, jsonCfg?: object): any
getWaterMarkReport(): any
setBackgroundColor(backColor: string): void页面设置
setPageConfig(jsonCfg: PageConfig): any
getPageProperties(): any
setHeaderConfig(jsonCfg: object): any
setFooterConfig(jsonCfg: object): any
displayPageSetWindow(displayMode: E_DISPLAY_MODE): void查找替换
search(searchCfg: SearchConfig): any
searchReplace(searchCfg: SearchConfig): any
displaySearchReplaceWindow(displayMode: E_DISPLAY_MODE): void打印
printDoc(jsonCfg?: PrintConfig): Promise<void>
displayPrintSetWindow(displayMode: E_DISPLAY_MODE, jsonCfg?: object): void知识库
setOptions(options: object): any
delOptions(options: object): any
setAlgorithms(algorithms: object): any
delAlgorithms(algorithms: object): any
getSourceList(docName: string): object
getAlgorithmList(): object
getOptionList(): object数据源
setSource(sourceClass: string, sourceId: string, fragment: string): boolean
setSystemSource(sourceClass: string, sourceId: string, fragment: string): any
setSources(sources: SourceData[], jsonCfg?: object): boolean
setDataSources(obj: object | undefined, param: object): boolean
getDataSources(identity?: object): object
getDocSourceReport(docName: string): any质控
getQualityControlReport(): any
selectQualityControl(jsonCfg: object): any审阅痕迹
getReviseReport(jsonCfg?: object): any
selectRevise(jsonCfg: object): any
setReviseConfig(jsonCfg: object): any其他
getFocusProperties(jsonCfg?: object): FocusProperties
setInputFocus(state: boolean): void
setEditorFocus(): void
getSelectRangeContent(docFormat: E_DOC_FORMAT, jsonCfg?: object): string
getObjectFragment(identityObjectType: E_IDENTITY_OBJECT_TYPE, id: string, docFormat: E_DOC_FORMAT, jsonCfg?: object): any
selectObject(identity: object, config: object): boolean
updateServerTime(serverTime: string): void
loadFont(fontName: string, arrayBuffer: ArrayBuffer): boolean
setUserProfile(settings: object): boolean
setEditorConfig(obj: object): any
getMenu(): any
displayMessageBoxWindow(displayMode: E_DISPLAY_MODE, jsonCfg: object): void
displaySpecialSymbolSetWindow(displayMode: E_DISPLAY_MODE): void
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void
destroyEditor(): void枚举值
组件导出了完整的枚举定义(来自 @vsoft_liu/vsoft-editor 的 VS_ENUM_* 枚举),包括:
VS_ENUM_VIEW_MODE- 视图模式VS_ENUM_DOC_FORMAT- 文档格式VS_ENUM_DOC_TYPE- 文档类型VS_ENUM_IMAGE_TYPE- 图像类型VS_ENUM_PASTE_TYPE- 粘贴类型VS_ENUM_BRUSH_STATE- 格式刷状态VS_ENUM_DISPLAY_MODE- 显示模式VS_ENUM_SET_MODE- 设置模式VS_ENUM_SCRIPT_MODE- 上下标模式VS_ENUM_FERRULE_MODE- 套圈模式VS_ENUM_LIST_MODE- 列表模式VS_ENUM_LIST_LEVEL- 列表级别VS_ENUM_ALIGN_HORIZONTAL_MODE- 水平对齐VS_ENUM_ALIGN_VERTICAL_MODE- 垂直对齐VS_ENUM_TABLE_INSERT_MODE- 表格插入模式VS_ENUM_TABLE_DELETE_MODE- 表格删除模式VS_ENUM_CELL_OPT_MODE- 单元格操作模式VS_ENUM_TIME_TYPE- 日期时间类型VS_ENUM_MESSAGE_LEVEL- 消息级别VS_ENUM_IDENTITY_OBJECT_TYPE- 身份对象类型VS_ENUM_BARCODE_TYPE- 条形码类型VS_ENUM_FORMULA_STYLE- 公式样式- 更多枚举请参考
enums.ts文件
运行演示
- 将
VsoftEmrDoc或VsoftEmrDoc作为应用入口组件 - 确保 ThinkEditor SDK 文件在正确的路径下
- 启动开发服务器
注意事项
- 组件依赖 ThinkEditor SDK,请确保 SDK 文件正确放置
- 字体文件需要提前准备好
- 部分功能需要在特定环境下才能正常工作(如打印、文件选择等)
License
MIT
