最近在做医疗结构化报告的功能,实现检查结果的编辑和部分项目的自定义,调研的了一下使用tiptap来实现。先看下官方简介:Tiptap 是一个无头富文本编辑器框架,它可以让你创建一个完全符合你和你的客户需求的自定义编辑器。它建立在 ProseMirror 的基础之上,ProseMirror 是一个久经考验的用于在网络上构建富文本编辑器的库。Tiptap 严重依赖于事件、命令和扩展,为构建编辑器提供了灵活而强大的 API。话不多说,show code。
安装依赖
由于项目使用vue,需要安装tiptap vue的依赖
bash
npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit
其中 @tiptap/pm是ProseMirror,@tiptap/starter-kit是官方提供的一个基础扩展组合包,它帮你一次性引入了一组最常用的 Node 和 Mark(加粗、斜体、标题、列表等),这样你不用一个个手动去安装和注册。我们是做个demo,就先全量引入了,后续可以自定义。
集成tiptap
xml
// Editor.vue
<template>
<editor-content :editor="editor" />
</template>
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: "<p>检查报告单</p>",
extensions: [StarterKit],
})
</script>

这里我们使用了useEditor这个hook,content是要显示的内容,extensions是tiptap的拓展,返回的editor我们绑定到editor-content组件上,就能显示出内容了,可以像其他富文本编辑器一样在页面上编辑内容。
添加自定义组件
在Tiptap里,"添加自定义组件"其实就是自定义 Node 或 Mark 扩展,然后通过 Vue/React 的渲染桥接把它变成可视化组件。
- 自定义Node
我们想要实现一个select组件,实现疾病风险等级的选择,首先需要定义node,们创建一个 CustomSelect
节点扩展
typescript
// SelectNode.ts
import { Node, mergeAttributes } from '@tiptap/core';
import { VueNodeViewRenderer } from '@tiptap/vue-3';
import SelectNodeView from './components/CustomSelect.vue';
export default Node.create({
name: 'customSelect',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
value: {
default: '',
parseHTML: (element) => element.getAttribute('data-value') || '',
renderHTML: (attributes) => {
return { 'data-value': attributes.value };
},
},
};
},
parseHTML() {
return [{ tag: 'custom-select' }];
},
renderHTML({ HTMLAttributes }) {
return ['custom-select', mergeAttributes(HTMLAttributes)];
},
addNodeView() {
return VueNodeViewRenderer(SelectNodeView);
},
});
关键点:
- 自定义拓展设置name,在通过editor.getJSON获取内容的json是,为node的type值
- 在组件的属性(
attrs
)里存储当前选中的值,parseHTML和renderTML解析和显示自定义节点到html上 - VueNodeViewRenderer是tiptap提供的vue组件渲染器,也有react组件的渲染器
- 创建组件
实现select组件
vue
<template>
<node-view-wrapper>
<select :value="node.attrs.value" @change="onChange">
<option value="">请选择</option>
<option v-for="op in options" :key="op.value" :value="op.value">
{{ op.label }}
</option>
</select>
</node-view-wrapper>
</template>
<script setup lang="ts">
import { NodeViewWrapper } from '@tiptap/vue-3';
import { useSelections } from '../useStore';
const props = defineProps();
const options = useSelections();
function onChange(event: Event) {
const target = event.target as HTMLSelectElement;
props.updateAttributes({ value: target.value });
}
</script>
实现useSelections,选项可自定义返回,这里我们模拟1s后返回选项
ts
import { ref, onMounted } from 'vue';
export function useSelections() {
const options = ref<{ label: string; value: string }[]>([]);
onMounted(() => {
setTimeout(() => {
options.value = [
{ label: '重度', value: 'A' },
{ label: '中度', value: 'B' },
{ label: '轻度', value: 'C' },
];
}, 1000);
});
return options;
}
关键点:
- 组件模板需要NodeViewWrapper包裹才能正常显示
- props里有node,和updateAttributes这两个属性,可通过node.attr取值,updateAttributes更新值
- 在编辑器中注册
我们把自定义节点注册到editr的拓展中,并修改content
js
editor = useEditor({
content: '<p>检查报告单</p><p>风险等级:<custom-select></custom-select></p>',
extensions: [StarterKit, CustomSelect],
});

这时候我们的自定义节点就会显示出来,可以自定义选项和选择结果。最终输出结果可以通过editor.getJSON或者editor.getHTML得到json类型的数据或者html结构,保存到数据库实现后续的渲染和编辑。
以上是使用tiptap实现简单的结构化报告模板的渲染和编辑,tiptap提供了丰富的拓展和插件,当我们有类似需求的时候可以考虑使用tiptap。