xdd-rich-editor-vue3
tiptap-editor-vue3
简体中文 | English
基于 Tiptap 3 + Vue 3 + TypeScript + Vite 开发的高质量富文本编辑器组件库。
- 开箱即用:工具栏、气泡菜单、表格右键菜单、目录侧栏、图片浮动选中等全部内置
- 内置中英文国际化(基于 vue-i18n)
- 支持浅色 / 深色 / 跟随系统三种主题,CSS 变量可自由定制
- 完整 TypeScript 类型,支持按需导入与 Hooks 二次组装
- 在线演示:https://zekofront.github.io/tiptap-editor-vue3/
功能清单
| 分类 | 已实现功能 |
|---|---|
| 文字标记 | 加粗、斜体、下划线、删除线、高亮、字体颜色、背景颜色、上标、下标、行内代码 |
| 块级节点 | 标题(H1–H6)、段落、无序列表、有序列表、任务列表(可勾选)、引用块、分隔线、代码块(lowlight 语法高亮)、图片、表格、超链接、Emoji(: 触发) |
| 布局与行为 | 文本对齐(左 / 中 / 右 / 两端)、行间距、拖拽手柄(拖动块)、RTL 阿拉伯文方向 |
| 编辑体验 | 顶部工具栏、选区气泡菜单、图片气泡菜单、表格右键菜单与“边框和底纹”、目录侧栏(文档导航)、占位符、字符数限制、撤销 / 重做、清空文档、Word 风格多级列表样式、外部 @ 人员弹窗 |
| 组件级能力 | 只读视图(XddRichEditorView)、useEditor / useEditorEvents Hooks 自行组装、浅色 / 深色 / 系统主题、zh-CN / en-US 国际化、可自定义气泡菜单按钮、动态上传地址 / Token、CSS 变量主题定制 |
安装
npm install tiptap-editor-vue3
# 或
pnpm add tiptap-editor-vue3
# 或
yarn add tiptap-editor-vue3
项目需要 Vue 3 环境(仅 Vue 为外部依赖,其它依赖如 Tiptap、naive-ui、vue-i18n 等已打入产物)。
快速开始
方式一:全局安装(推荐)
// main.ts
import { createApp } from "vue";
import XddRichEditorVue3 from "tiptap-editor-vue3";
import "tiptap-editor-vue3/dist/css/style.css";
import App from "./App.vue";
const app = createApp(App);
app.use(XddRichEditorVue3);
app.mount("#app");
<!-- App.vue -->
<template>
<XddRichEditorVue3
v-model="content"
:is-editable="true"
:character-count="20000"
locale="zh-CN"
theme="system"
placeholder="请输入内容..."
@ready="onReady"
@update="onUpdate"
/>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Editor, EditorUpdatePayload } from "tiptap-editor-vue3";
const content = ref("<p>初始内容</p>");
const onReady = (editor: Editor) => {
console.log("editor ready", editor);
};
const onUpdate = ({ html, json }: EditorUpdatePayload) => {
console.log(html, json);
};
</script>
使用
v-model双向绑定内容。传入 HTML 字符串(默认)或 Tiptap JSON 对象;通过outputFormatprop 切换update:modelValue发送的格式。
方式二:按需导入
<template>
<XddRichEditorVue3 v-model:is-editable="editable" @update="onUpdate" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { XddRichEditorVue3, type EditorUpdatePayload } from "tiptap-editor-vue3";
import "tiptap-editor-vue3/dist/css/style.css";
const editable = ref(true);
const onUpdate = (p: EditorUpdatePayload) => console.log(p.html);
</script>
只读视图
<template>
<XddRichEditorView :editor="editor" />
</template>
<script setup lang="ts">
import { XddRichEditorView, useEditor } from "tiptap-editor-vue3";
import StarterKit from "@tiptap/starter-kit";
const { editor } = useEditor({
extensions: () => [StarterKit],
content: () => "<p>Hello</p>",
editable: () => false
});
</script>
Props(参数列表)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue (v-model) |
string | JSONContent | null |
"" |
编辑器内容;支持 HTML 字符串或 Tiptap JSON |
outputFormat |
'html' | 'json' |
'html' |
update:modelValue 发送的格式 |
defaultConfig |
Record<string, any> | null |
null |
透传给底层 new Editor(...) 的额外配置 |
extensions |
AnyExtension[] |
[] |
自定义扩展数组;不传则使用内置完整扩展集 |
isEditable |
boolean |
true |
是否可编辑 |
isShowToolbar |
boolean |
true |
是否显示顶部工具栏 |
isEnabledContent |
boolean |
true |
渲染内容区(关闭时仅工具栏) |
characterCount |
number | string |
10000 |
字符数限制;超过禁止输入 |
uploadUrl |
string | (() => string) |
undefined |
图片、文件、视频上传接口地址;函数形式会在每次上传前重新取值 |
uploadToken |
string | (() => string) |
undefined |
上传令牌;函数形式会在每次上传前重新取值 |
uploadTokenHeader |
string |
Authorization |
令牌请求头名称 |
uploadTokenPrefix |
string |
Bearer |
令牌前缀;传空字符串时直接发送原始 Token |
uploadFieldName |
string |
file |
multipart 上传字段名 |
locale |
'zh-CN' | 'en-US' |
'zh-CN' |
界面语言(工具栏提示、表格菜单、占位符等) |
theme |
'system' | 'light' | 'dark' |
'system' |
主题色彩;system 跟随操作系统 prefers-color-scheme |
placeholder |
string |
i18n 默认值 | 空文档占位符;不传时使用当前语言默认值 |
customDubbleMenus |
IDubbleMenu[] |
[] |
自定义气泡菜单按钮列表 |
editorWrapperClass |
string | any[] | Record<string, any> |
undefined |
最外层容器自定义类 |
editorToolkitClass |
string | any[] | Record<string, any> |
undefined |
工具栏自定义类 |
editorContentClass |
string |
'custom-tiptap-content' |
内容区 ProseMirror 容器类 |
editorContentsNavClass |
string | any[] | Record<string, any> |
undefined |
目录侧栏自定义类 |
IDubbleMenu等类型从包内导出:import type { IDubbleMenu, EditorProps } from "tiptap-editor-vue3"。
Events(事件列表)
| 事件 | 负载(payload) | 说明 |
|---|---|---|
update:modelValue |
string | JSONContent |
内容变化(v-model 同步);默认 HTML 字符串,outputFormat="json" 时为 JSON |
ready |
editor: Editor |
编辑器实例创建完成,等价于 Tiptap create |
update |
{ editor, html, json } |
内容变化;html / json 已自动获取,无需再调用 editor.getHTML() |
selection-update |
{ editor } |
光标 / 选区变化 |
transaction |
{ editor, transaction } |
任意 ProseMirror 事务 |
focus |
{ editor, event: FocusEvent } |
编辑器获得焦点 |
blur |
{ editor, event: FocusEvent } |
编辑器失去焦点 |
before-create |
{ editor } |
实例创建前 |
content-error |
{ editor, error } |
解析初始内容失败 |
mention-request |
Tev3MentionRequestPayload |
输入 @ 或点击工具栏 @ 时触发,外部弹窗选择人员后调用 payload.insert() |
destroy |
— | 实例销毁 |
update 事件负载类型:
import type { EditorUpdatePayload } from "tiptap-editor-vue3";
interface EditorUpdatePayload {
editor: Editor;
html: string;
json: JSONContent;
}
外部 @ 人员弹窗
组件不再内置人员建议列表,也不会弹出“张三、李四”等默认数据。输入 @ 或点击工具栏 @ 后会触发外部弹窗请求:
<template>
<XddRichEditorVue3
v-model="content"
@mention-request="openMentionPicker"
/>
<n-modal v-model:show="mentionVisible" preset="card" title="选择人员">
<button
v-for="item in people"
:key="item.id"
@click="selectMention(item)"
>
{{ item.name }}
</button>
</n-modal>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type {
Tev3MentionItem,
Tev3MentionRequestPayload
} from "tiptap-editor-vue3";
const people: Tev3MentionItem[] = [];
const mentionVisible = ref(false);
const mentionRequest = ref<Tev3MentionRequestPayload | null>(null);
const openMentionPicker = (payload: Tev3MentionRequestPayload) => {
mentionRequest.value = payload;
mentionVisible.value = true;
};
const selectMention = (item: Tev3MentionItem) => {
mentionRequest.value?.insert(item);
mentionVisible.value = false;
};
</script>
payload 包含 query、from、to、source 和 insert()。insert({ id, name }) 会删除输入框中的 @ 并写入人员节点,最终 HTML 中保留 data-id 与 data-name。
也可以通过 defaultConfig.mention.onRequest 接管弹窗请求:
const defaultConfig = {
mention: {
onRequest: (payload) => {
openMentionPicker(payload);
}
}
};
表格边框和底纹
在表格单元格上点击右键,选择“边框和底纹”。面板支持:
- 无、方框、全部、网格、自定义
- 线型、颜色、粗细
- 上、下、左、右、内部边框
- 应用于整张表格或当前选区单元格
- 3×3 实时预览
表格插入默认按每列 150px 计算初始宽度,后续可通过右下角拖动柄自由缩放。
上传接口
上传地址和 Token 支持通过组件属性动态传入,函数会在每次上传前重新执行:
<XddRichEditorVue3
:upload-url="getUploadUrl"
:upload-token="getUploadToken"
upload-token-header="X-Token"
:upload-token-prefix="''"
upload-field-name="file"
/>
默认按以下响应结构读取上传结果中的 url:
{
"code": 200,
"msg": "操作成功",
"fileName": "demo.pdf",
"url": "https://example.com/static/demo.pdf",
"fileId": "dbff10a5-8bf5-46a8-8eda-9e77dfe75256"
}
图片、文件和视频内容只保存服务器返回的 URL,不把 base64 或本地 Blob 写入正文。
导入文档(TXT 和 Word)
编辑器支持通过工具栏中的导入按钮导入 TXT 和 Word(DOC、DOCX)文档的内容。点击导入按钮后,会出现一个下拉菜单,可以选择导入 TXT 文件或 Word 文件。选择文件后,内容会被解析并插入到编辑器中。成功和错误信息会使用本地化的消息进行提示。
当前版本默认隐藏 Word 导入 / 导出入口,底层实现仍保留;需要启用时,注册
Imports扩展并恢复工具栏导出按钮。
图片上传
图片上传统一通过 defaultConfig.uploadImage 配置,规则非常简单:
imageLink/customUpload是函数 → 走用户自定义逻辑,弹窗把 URL / 文件交给你,由你自己editor.commands.setImage(...)插入;- 否则 → 走组件内部默认实现(链接 Tab 直接
setImage({ src })、上传 Tab 转 base64 插入)。
accept / maxSize / maxCount 始终生效,组件先做校验再交给用户回调;用户没选图片就点"立即上传"时会弹本地化提示。
自定义图片上传,严格遵循下面配置规则,否则不生效:
<template>
<XddRichEditorVue3
v-model="content"
:default-config="defaultConfig"
@ready="onReady"
/>
</template>
<script setup lang="ts">
import { shallowRef } from "vue";
import type { Editor, Tev3DefaultConfig } from "tiptap-editor-vue3";
const editors = shallowRef<Editor | null>(null);
const onReady = (editor: Editor) => {
editors.value = editor;
};
const defaultConfig: Tev3DefaultConfig = {
uploadImage: {
accept: "image/png,image/jpeg,image/webp",
maxSize: 5 * 1024 * 1024, // 单张最大 5MB
maxCount: 9,
imageLink: (link: string) => {
console.log(link, editors.value, "imageLink");
editors.value?.commands.setImage({ src: link });
},
customUpload: async (files) => {
console.log(files, editors.value, "customUpload");
for (let i = 0; i < files.length; i++) {
if (files[i]) setImageOne(files[i] as File);
}
}
}
};
const setImageOne = (file: File) => {
const reader = new FileReader();
reader.onload = event => {
const base64 = event.target?.result as string;
editors.value?.commands.setImage({ src: base64 });
};
reader.readAsDataURL(file);
};
</script>
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
accept |
string |
image/png,image/jpeg,image/gif,image/webp,image/svg+xml |
HTML accept 语法,过滤选择器并校验类型 |
maxSize |
number |
10 * 1024 * 1024 |
单张图片大小上限(字节),超过弹错误提示并跳过 |
maxCount |
number |
Infinity |
单次最多选择图片数量 |
imageLink |
(url: string) => void |
— | "图片链接" 选项卡确认时的回调。是函数 → 走用户自定义;否则走默认 setImage({ src }) |
customUpload |
(files: File[] | FileList) => void | Promise<void> |
— | "上传图片" 选项卡确认时的回调。是函数 → 走用户自定义;否则文件转 base64 直接插入 |
导出类型:Tev3DefaultConfig、Tev3UploadImageConfig。
主题(明暗模式)
通过 theme prop 控制:
<XddRichEditorVue3 theme="system" />
<XddRichEditorVue3 theme="light" />
<XddRichEditorVue3 theme="dark" />
system:自动跟随系统prefers-color-scheme,系统切换会实时响应;light/dark:固定使用对应主题。
底层通过 data-tev3-theme="light|dark" 数据属性切换,所有颜色基于 CSS 变量。如需自定义品牌色,覆盖以下常用变量即可:
.vue3-tiptap-editor[data-tev3-theme="light"] {
--theme-color: #2080f0;
--tev3-toolbar-bg: #fafbfc;
}
.vue3-tiptap-editor[data-tev3-theme="dark"] {
--theme-color: #36ad6a;
--tev3-editor-bg: #15161a;
}
JS 端可使用 useTheme() 自行实现切换:
import { ref } from "vue";
import { useTheme, type Tev3Theme } from "tiptap-editor-vue3";
const theme = ref<Tev3Theme>("system");
const { resolvedTheme } = useTheme(() => theme.value); // -> 'light' | 'dark'
国际化
内置 zh-CN / en-US,通过 locale prop 切换;任意时刻也可调用 setLocale() 全局切换。
import { setLocale, t } from "tiptap-editor-vue3";
setLocale("en-US");
console.log(t("toolbar.bold")); // "Bold"
<XddRichEditorVue3 locale="en-US" />
切换
locale时工具栏、表格菜单、目录、占位符会自动刷新。
如需自定义文案,可在自己的项目中调用 tev3I18n.global.mergeLocaleMessage('zh-CN', { ... }) 覆盖。
进阶:使用 Hooks 自行组装
若内置工具栏不满足需求,可直接复用 useEditor / useEditorEvents 自己组合 UI:
<template>
<div>
<button @click="editor?.chain().focus().toggleBold().run()">Bold</button>
<XddRichEditorView :editor="editor" />
</div>
</template>
<script setup lang="ts">
import {
XddRichEditorView,
useEditor,
useEditorEvents,
type EditorUpdatePayload
} from "tiptap-editor-vue3";
import StarterKit from "@tiptap/starter-kit";
const emit = defineEmits<{ (e: "update", p: EditorUpdatePayload): void }>();
const { editor, editable, toggleEditable } = useEditor({
extensions: () => [StarterKit],
content: () => "<p>Hello</p>",
editable: () => true
});
useEditorEvents(editor, { emit });
</script>
useEditor 返回:
| 字段 | 类型 | 说明 |
|---|---|---|
editor |
ShallowRef<Editor | null> |
实例(onMounted 后就绪) |
editable |
Ref<boolean> |
可编辑状态 |
rtl |
Ref<boolean> |
RTL 排版 |
toggleEditable |
() => void |
切换只读 |
toggleRtl |
() => void |
切换 RTL |
create / destroy |
() => void |
手动控制生命周期 |
包导出
// 组件
export { XddRichEditorVue3, XddRichEditorView };
// Composables
export { useEditor, useEditorEvents, useTev3I18n };
// Props / 类型
export { editorProps };
export type {
EditorProps,
UseEditorOptions,
UseEditorReturn,
EditorUpdatePayload,
EditorEventName,
Tev3Locale
};
// 国际化
export { tev3I18n, t, setLocale, getLocale, installTev3I18n, SUPPORTED_LOCALES, zhCN, enUS };
// 主题
export { SUPPORTED_THEMES, DEFAULT_THEME, resolveTheme, useTheme };
export type { Tev3Theme, Tev3ResolvedTheme };
// Tiptap 常用 API(避免重复装包)
export { Editor, EditorContent, NodeViewWrapper, mergeAttributes };
export type { XddRichEditor, JSONContent, Content, AnyExtension };
开发命令
npm run dev # 启动本地 playground
npm run build # 构建组件库(dist/)
npm run build:demo # 构建 playground 演示站(dist-demo/)
npm run preview:demo # 本地预览演示站
npm run fmt # 格式化(oxfmt)
npm run lint # 代码检查(oxlint)
在线演示(GitHub Pages)
参考链接
License
Related Packages
Version History
| Version | Released | License |
|---|---|---|
| 2.1.21 | MIT | |
| 2.1.19 | MIT | |
| 2.1.17 | MIT | |
| 2.1.12 | MIT | |
| 2.1.11 | MIT | |
| 2.1.10 | MIT | |
| 2.1.9 | MIT | |
| 2.1.8 | MIT | |
| 2.1.7 | MIT | |
| 2.1.5 | MIT | |
| 2.1.3 | MIT | |
| 2.1.2 | MIT | |
| 2.0.9 | MIT | |
| 2.0.8 | MIT |