@huyooo/vue-tiptap
v1.0.5
Published
面向 Vue 3 的 TipTap 富文本编辑器组件库,封装了常用正文编辑、目录、标题、表格、任务列表、图片上传、图片缩放、图片对齐、图片拖拽、图片工具栏等交互。
Readme
@huyooo/vue-tiptap
面向 Vue 3 的 TipTap 富文本编辑器组件库,封装了常用正文编辑、目录、标题、表格、任务列表、图片上传、图片缩放、图片对齐、图片拖拽、图片工具栏等交互。
特性
- 基于 Vue 3 + TipTap 3。
- 输出 TipTap JSON,适合直接存储到数据库。
- 内置基础格式、标题、列表、任务列表、代码块、表格、目录、链接、颜色、高亮、上下标等能力。
- 图片支持粘贴/拖拽上传、加载态、上传态、撤销、拖拽移动、宽度预设、滑块缩放、四角 + 四边中点缩放、左/中/右对齐。
- 图片缩放始终保持宽高比,并限制最大宽度不超过编辑器内容区域。
- 图片工具栏状态会与当前选中图片的对齐和宽度同步。
- 发布产物 external 掉 Vue、TipTap 等依赖,避免重复打包和多实例问题。
安装
npm install @huyooo/vue-tiptapvue 是 peer dependency,宿主项目必须安装 Vue 3:
npm install vueTipTap 相关包是本包的 dependencies,安装 @huyooo/vue-tiptap 时会由 npm 自动安装,业务项目不需要手动逐个安装 @tiptap/*。
基础用法
<template>
<TiptapEditor
v-model="content"
:editable="true"
:upload-image="uploadImage"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TiptapEditor } from '@huyooo/vue-tiptap'
import '@huyooo/vue-tiptap/style.css'
const content = ref({
type: 'doc',
content: [{ type: 'paragraph' }],
})
async function uploadImage(file: File) {
// 上传到你自己的文件服务或对象存储,返回编辑器可访问的图片 URL。
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData,
})
const data = await response.json()
return data.url
}
</script>必须引入样式:
import '@huyooo/vue-tiptap/style.css'组件 API
TiptapEditor
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| v-model | JSONContent \| null | null | 编辑器内容,TipTap JSON 格式。 |
| editable | boolean | true | 是否可编辑。只读模式下不显示工具栏、图片缩放点和图片菜单。 |
| autofocus | boolean | true | 是否自动聚焦。弹窗中建议传 false。 |
| limit | number \| null | null | 字数限制。 |
| tiptapEditorContentClass | string | '' | 追加到编辑器根容器上的 class。 |
| uploadImage | (file: File) => Promise<string> | undefined | 图片上传回调,返回可访问图片 URL。可编辑模式下建议必传。 |
事件
| 事件 | 参数 | 说明 |
| --- | --- | --- |
| tableOfContentsUpdate | unknown | 目录数据更新。 |
暴露方法
TiptapEditor 通过 defineExpose 暴露:
| 方法 | 说明 |
| --- | --- |
| getEditor() | 获取 TipTap Editor 实例。 |
| scrollView(id: string) | 根据 data-toc-id 滚动到对应标题。 |
内容格式
编辑器读写 TipTap JSON,不是 HTML 或 Markdown。推荐数据库字段直接存储 JSON:
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Hello"
}
]
}
]
}空文档建议使用:
const emptyDoc = {
type: 'doc',
content: [{ type: 'paragraph' }],
}图片上传
上传流程
- 用户粘贴或拖拽图片到编辑器。
- 编辑器先插入图片占位节点,显示“图片上传中”。
- 调用
uploadImage(file)。 - 上传成功后,将占位节点替换成真实图片 URL,并显示“图片加载中”直到图片资源加载完成。
- 上传失败时移除占位图。
uploadImage 只负责上传并返回 URL:
type UploadImageFn = (file: File) => Promise<string>返回值必须是浏览器可以访问的图片地址,例如 CDN URL、签名 URL 或公开 URL。
撤销行为
上传完成替换 URL 不进入 undo 历史。用户粘贴图片后按撤销,会直接删除整张图片,不会退回“图片上传中”的占位状态。
临时文件清理
编辑器组件无法知道业务侧“是否最终保存文章”,因此临时文件清理由宿主业务负责。
推荐做法:
- 打开编辑弹窗时记录已有图片 key,作为
baselineKeys。 - 本次弹窗中上传成功的图片记录到
sessionUploadedKeys。 - 用户取消或关闭弹窗时,删除
sessionUploadedKeys - baselineKeys。 - 保存成功后,只保留当前内容仍引用的本次上传图片,其余删除。
- 如果弹窗关闭后上传才完成,上传回调应立即删除该文件,避免永久残留。
- 保存前如果还有图片上传中,应阻止保存,避免把 loading 占位写入数据库。
业务侧可以从 TipTap JSON 中递归收集图片节点:
function collectImageSrcs(node: any, result = new Set<string>()) {
if (node?.type === 'image' && typeof node.attrs?.src === 'string') {
result.add(node.attrs.src)
}
node?.content?.forEach((child: any) => collectImageSrcs(child, result))
return result
}如果你的系统保存的是对象存储 key,建议在保存前把临时签名 URL 归一化为稳定 key,避免过期 URL 写入数据库。
图片交互
缩放
图片选中后显示四角 + 四边中点共 8 个缩放点。缩放规则:
- 永远保持宽高比。
- 四角和四边中点都按图片中心等比缩放。
- 最大宽度不超过编辑器内容区域。
- loading 状态下不显示缩放点,避免上传/加载过程中误操作。
对齐
图片是块级节点。对齐通过图片外层行容器实现:
- 左对齐:图片靠左。
- 居中:图片居中。
- 右对齐:图片靠右。
选中图片时:
- 外层虚线表示图片所在行和对齐范围。
- 图片本体实线表示真实图片边界。
- 图片工具栏左/中/右按钮会与当前图片
align属性实时同步。
拖拽
图片支持拖拽移动位置。拖拽图片本体时,编辑器会显示紫色 drop cursor,表示图片将插入的位置。
注意:
- loading 状态图片不可拖拽。
- 缩放点拖拽只处理缩放,不触发移动。
- 拖拽移动由 ProseMirror 节点拖拽机制处理。
只读模式
只读模式:
<TiptapEditor v-model="content" :editable="false" />只读模式不会显示:
- 顶部编辑工具栏
- 图片工具栏
- 图片缩放点
- 表格行列菜单
样式说明
本包发布一个独立样式文件:
import '@huyooo/vue-tiptap/style.css'如果宿主项目使用局部滚动容器或弹窗,请确保编辑器外层有明确高度或最小高度:
<TiptapEditor style="min-height: 400px" />导出内容
import {
TiptapEditor,
Aside,
Toc,
RepositorieTitle,
TiptapTitle,
} from '@huyooo/vue-tiptap'
import type {
UploadImageFn,
ModelType,
Option,
ShouldShowProps,
} from '@huyooo/vue-tiptap'构建与发布
本地构建
npm run build构建产物:
dist/index.jsdist/style.cssdist/**/*.d.ts
预览 npm 包内容
npm pack --dry-run发布前建议确认:
- 包中只包含
dist、README.md、package.json等必要文件。 - 不包含
public/vite.svg、源码临时文件、测试产物。 dist/index.js没有把 Vue 和 TipTap 全量打包进去。
发布
npm run releaserelease 会执行版本选择并发布:
bumpp && npm publish如果需要直接 patch 并发布:
npm run pub发布前必须执行:
npm run build
npm pack --dry-run依赖发布原则
- 不要在 npm 尚未发布新版本前,提前把下游项目依赖改到不存在的版本。
- 发布完成并确认 npm 上存在新版本后,下游项目再显式升级版本号。
- 下游依赖版本要写明确的新版本,例如从
^1.0.0升到^1.0.1,不要只依赖旧 semver 范围自动漂移。
依赖策略
本包使用库模式构建,并 external 掉主要依赖。
这表示构建 dist/index.js 时不会把 Vue、TipTap 等依赖源码复制进 bundle,但 npm 安装时仍会自动安装 dependencies 中的依赖。
当前策略:
vue:peerDependencies,宿主项目必须提供,避免出现多个 Vue 实例。@tiptap/*、lowlight、vue-color-kit、@iconify/vue:dependencies,安装本包时自动安装。- 构建时 external 所有上述依赖,减少产物体积并避免重复打包。
常见问题
为什么安装后还要引入 style.css?
库模式会把 CSS 输出为独立文件。宿主项目需要显式引入:
import '@huyooo/vue-tiptap/style.css'为什么不把 TipTap 全部打进 index.js?
不打进 bundle 可以显著减少产物体积,并让 npm 依赖树正常去重。TipTap 仍在 dependencies 里,使用方安装本包时会自动安装。
为什么 Vue 是 peer dependency?
Vue 必须由宿主项目提供,确保应用中只有一套 Vue 实例,避免响应式、插件上下文和组件渲染异常。
图片上传后关闭弹窗会自动删除吗?
组件本身不知道业务弹窗是否保存成功,不会自动删除远端文件。宿主业务必须按“临时上传会话”清理未保存文件。
数据库存 HTML 还是 JSON?
推荐存 TipTap JSON。HTML 可以用于渲染导出,但不建议作为主存储格式。
