@orilight/editor

基于 Tiptap 3 的 Vue 3 富文本编辑器组件:Word 粘贴还原、图片 / 附件上传、表格与段落排版
Install
npm install @orilight/editor
Latest Version:0.1.1
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: npm
Maintainer: orilight

@orilight/editor

基于 Tiptap 3 封装的 Vue 3 富文本编辑器组件。开箱即用:自带工具栏与中文排版能力, 重点解决「从 Word 粘贴」与「图片 / 附件上传」这两个真实业务里最容易踩坑的场景。

  • 字符格式:加粗、斜体、下划线、字符边框、删除线、上标、下标、清除格式、格式刷
  • 段落排版:H1 ~ H6、字号、行间距、段前距 / 段后距、首行缩进、有序 / 无序列表、四种对齐
  • 插入内容:字体颜色、背景色、超链接、分割线、表格(增删行列 / 合并拆分 / 表头切换)
  • 图片:单图 / 多图上传、粘贴与拖拽上传、八向手柄自由调整宽高(按住 Shift 锁定宽高比)、 行内 ↔ 独立一行互转、独立行图片支持左 / 中 / 右对齐
  • 附件:以卡片形式插入(行内节点,可与文字同行),展示文件图标、名称与大小
  • Word 粘贴:保留字体、字号、颜色、对齐与表格列宽;还原 ☑ / ☐ 等符号字体勾选框; 图文混排里内嵌的 base64 图片会在后台自动上传并就地替换地址
  • 上传:编辑器不含任何请求代码,只依赖宿主实现的 EditorUploader 接口; 多图 / 附件弹窗支持分批添加、边传边看进度、失败重试与移除
  • 字数统计与上限:底部实时计数(按字形簇,emoji 也算 1 个),可配置上限; 超限只拒绝新输入并标红提醒,不会静默截断已有内容
  • 零样式依赖:组件样式自带(custom-editor__* 前缀 + 内置最小化重置), 不依赖宿主引入 Tailwind / normalize 等全局样式

安装

pnpm add @orilight/editor vue
# npm i @orilight/editor vue
# yarn add @orilight/editor vue
  • vue 是 peer 依赖,需要由宿主项目提供(^3.5.0)。
  • Tiptap 与图标库由本包自带,无需额外安装。
  • 只提供 ESM 产物,构建工具请使用 Vite / Rollup / webpack 5 等支持 ESM 的方案。

快速开始

<script setup lang="ts">
import { CustomEditor } from '@orilight/editor'
import '@orilight/editor/style.css' // 必须引入,否则样式缺失
import { ref } from 'vue'

const html = ref('<p>你好,世界</p>')
</script>

<template>
  <CustomEditor v-model="html" placeholder="请输入内容" min-height="360px" />
</template>

样式文件是独立的 CSS,不会被自动注入,必须显式引入一次(通常在 main.ts 里全局引入也可以)。

若项目没有 vite/client 类型(例如 tsconfig 里写了 "types": [] 且没有 vite-env.d.ts), import '@orilight/editor/style.css' 会报 TS2882。按 Vite 官方模板补一行 /// <reference types="vite/client" /> 即可。

Props

名称 类型 默认值 说明
modelValue string '' 双向绑定的 HTML 字符串
placeholder string '请输入内容' 内容为空时的占位提示
editable boolean true 是否可编辑(false 时工具栏整体禁用)
minHeight string '320px' 内容区最小高度
maxHeight string '' 内容区最大高度,超出后内容区内部滚动
autofocus boolean false 是否自动聚焦(聚焦到文档末尾)
showToolbar boolean true 是否展示工具栏
maxLength number 0 最大字数,0 表示不限制
showCount boolean true 是否在底部展示字数统计
uploader EditorUploader — 文件上传器,见下文;不传时所有上传入口置灰

事件

名称 参数 说明
update:modelValue (html: string) v-model 同步
change (html: string, editor: Editor) 内容变化
ready (editor: Editor) 编辑器创建完成
focus / blur (editor: Editor) 获得 / 失去焦点
countChange (count: number, limit: number | null) 字数变化,limit 为 null 表示未限制
uploadSuccess (file: File, result: UploadResult) 单个文件上传成功
uploadError (file: File, reason: UploadErrorReason, message: string) 未接入上传服务 / 文件校验不通过 / 上传失败

reason 取值:'not-ready' | 'invalid-file' | 'upload-failed'。

通过 ref 调用

<script setup lang="ts">
import type { CustomEditor } from '@orilight/editor'
import { ref } from 'vue'

const editorRef = ref<InstanceType<typeof CustomEditor> | null>(null)

function save() {
  // 保存时用 getHTML():它会返回解析清洗之后的 HTML(见「注意事项」)
  fetch('/api/save', { method: 'POST', body: editorRef.value?.getHTML() })
}
</script>
成员 类型 说明
editor ShallowRef<Editor | undefined> 底层 tiptap 编辑器实例
getHTML() / getJSON() / getText() string / object | null / string 读取内容
getCount() / count / limit / isOverLimit number / ComputedRef<number> / ComputedRef<number | null> / ComputedRef<boolean> 字数相关
isEmpty() boolean 文档是否为空
setContent(content) / clearContent() / insertContent(content) — 写入内容
focus(position?) / blur() — 焦点控制
undo() / redo() — 撤销 / 重做
isActive(name, attrs?) boolean 判断当前选区是否处于某个节点 / mark
uploadReady / isUploading / uploadingCount ComputedRef<boolean> / ComputedRef<boolean> / ComputedRef<number> 上传状态(可用于保存前校验)

文件上传

编辑器不关心上传细节(请求方式、鉴权、分片、重试策略……),只需宿主实现一个接口并通过 uploader prop 传入:

import type { EditorUploader } from '@orilight/editor'
import { DEFAULT_UPLOAD_RULES } from '@orilight/editor'

const uploader: EditorUploader = {
  // 可以传函数:上传器异步初始化完成(例如拿到 token)后会自动恢复可用
  isReady: () => !!token.value,

  rules: {
    ...DEFAULT_UPLOAD_RULES,
    image: { accept: 'image/*', maxSize: 8 * 1024 * 1024 },
  },

  async upload(file, { onProgress, signal }) {
    const form = new FormData()
    form.append('file', file)
    const { data } = await axios.post('/api/upload', form, {
      signal,
      onUploadProgress: e => onProgress?.(Math.round((e.loaded / (e.total ?? 1)) * 100)),
    })
    // 返回的 url 会被写入文档,请确保它是长期有效的地址
    return { url: data.url, name: data.name, size: data.size, mimeType: file.type }
  },

  // 可选:删除占位、取消弹窗时中止请求
  abort: fileId => pending.get(fileId)?.abort(),
}
<CustomEditor v-model="html" :uploader="uploader" @upload-error="onUploadError" />

EditorUploader 字段:

字段 类型 说明
upload (file: File, context: UploadContext) => Promise<UploadResult> 必填;失败时 reject,Error.message 会展示给用户
isReady boolean | (() => boolean) 未就绪时所有上传入口置灰;传函数时按需轮询,就绪后自动恢复
abort (fileId: string) => void 可选;取消 / 删除占位时中止对应请求
rules Partial<Record<'image' | 'attachment', UploadRule>> 可选;accept 语法同 input[accept],maxSize 单位为字节

UploadContext 提供 fileId、signal(AbortSignal)与 onProgress(0 - 100)。 UploadResult 至少需要 url,可选 name / size / mimeType。

行为说明:

  • 未传 uploader 或未就绪时,工具栏的上传按钮、粘贴、拖拽三条入口全部失效并给出提示, 不会静默失败。
  • 上传过程不写文档:先插入「上传中」占位节点,成功后就地替换,因此一次上传只占 2 步撤销 (插入占位、替换),进度刷新不会污染撤销栈,也不会触发 v-model 更新。
  • 从 Word 粘贴的 base64 内嵌图片会自动上传:图片先正常显示,随后在后台上传并就地替换 src; 失败时保留原图并显示重试 / 忽略按钮。程序性 setContent 不会触发这个行为。
  • 上传中取 HTML(例如用户提前保存)会包含占位节点,但占位节点不参与解析, 重新加载时被丢弃,不会残留死占位。

导出的类型与常量

// 组件与类型
import type {
  CustomEditorProps, CustomEditorEmits, SelectOption,
  BlockTypeValue, TextAlignValue, ImageAlign, ImageMode, BlockStyleAttr,
  EditorUploader, UploadContext, UploadResult, UploadRule, UploadKind,
  UploadStatus, UploadTask, UploadNotice, UploadDialogState, UploadController,
  UploadErrorReason, CreateUploadControllerOptions,
  Content, Editor, FocusPosition, // 转出的 tiptap 类型
} from '@orilight/editor'
import { CustomEditor, createUploadController, DEFAULT_UPLOAD_RULES, UPLOAD_KIND_LABELS } from '@orilight/editor'

createUploadController(options) 只在「自建 tiptap 编辑器、复用本包的上传控制器」这类高级场景才需要; 常规用法直接给 CustomEditor 传 uploader prop 即可。

样式与定制

  • 样式前缀统一为 custom-editor__*,富文本内容区为 .custom-editor__content, 节点视图(图片 / 附件 / 占位)为 .ce-*。

  • 高度通过内联 CSS 变量控制,也可以自己覆盖:

    .custom-editor {
      --ce-min-height: 480px;
      --ce-max-height: 70vh;
      border-radius: 10px;
    }
    
  • 组件自带一份最小化重置(box-sizing、表单元素字体继承等), 因此宿主项目不需要引入 Tailwind / normalize;若宿主已引入,也不会冲突。

注意事项

  • 仅客户端渲染:编辑器依赖 document / window。SSR 项目请把它包在客户端组件里 (Nuxt 用 <ClientOnly> / .client.vue,Next.js 用动态导入 + ssr: false)。
  • v-model 不回写清洗结果:外部写入的 HTML 由 ProseMirror 的 schema 白名单解析, <script>、事件属性等会被丢弃,但解析后的结果只在编辑器内部,v-model 变量仍是原始串。 保存 / 入库请一律使用 getHTML() 或 change 事件给出的 HTML。
  • 图片默认行内:无论上传、粘贴、拖拽还是导入 HTML,<img> 默认解析为行内图片(可与文字同行); 导出的独立行图片会带 data-align 标记与内联 margin,重新导入仍保持独立行。
  • 表格列宽:会同时兼容 tiptap 自身的 colwidth、Word 写在单元格上的 width / style.width 以及 <colgroup><col width>。导出格式带 <col style="width: ...">,往返不丢列宽。
  • 字数上限:达到上限后拒绝「变多」的编辑并标红,但不会截断已有内容; 带下划线撤销 / 重做可以自由越过上限(历史内容属于宿主的权威数据)。
  • link 粘贴不自动加链接:粘贴纯文本里的裸网址不会自动转成链接(输入时仍会自动识别), 避免把「网址 + 中文备注」整段吞进 href。

本地开发

仓库同时是组件库本体与它的演示应用:

pnpm install

pnpm dev          # 启动演示应用(Vite dev server)
pnpm build:demo   # 构建演示站点到 dist-demo/
pnpm preview      # 预览演示站点

pnpm build        # 构建组件库到 dist/(ESM + 类型声明 + style.css)
pnpm type-check   # 类型检查
pnpm lint / lintf # ESLint(lintf 自动修复)

发布:

pnpm build        # prepublishOnly 会自动执行
npm publish --access public

目录结构

src/
├── index.ts                    # 组件库公共入口(唯一对外出口)
├── App.vue / main.ts           # 演示应用(不参与组件库构建)
└── components/CustomEditor/
    ├── index.vue               # CustomEditor 组件本体
    ├── types.ts                # props / emits / 选项常量
    ├── useEditorToolbar.ts     # 工具栏状态与命令封装
    ├── components/             # 工具栏、下拉、弹层、上传弹窗、节点视图
    ├── extensions/             # tiptap 扩展(图片、附件、链接、字数上限、Word 粘贴……)
    ├── uploader/               # 上传契约与控制器(零 HTTP 代码)
    ├── styles/editor.scss      # 组件全部样式(原生 CSS,不依赖 Tailwind)
    └── utils/                  # 文本 / 文件 / 颜色 / 块样式工具

License

MIT

Related Packages

tiptap-vue-pro-core

Tiptap Vue Pro 的 UI 无关核心层:封装 Tiptap v3,提供 composable 与类型契约

3,174 8
tiptap-vue-pro-element-plus

Tiptap Vue Pro 的 Element Plus 适配:开箱即用的富文本编辑器组件

2,698 9
tiptap-vue-pro-naive

Tiptap Vue Pro 的 Naive UI 适配:开箱即用的富文本编辑器组件

2,657 9
tiptap-vue-pro-ant-design-vue

Tiptap Vue Pro 的 Ant Design Vue 适配:开箱即用的富文本编辑器组件

2,059 9
@abhinavakhil/penna-vue

Vue 3 bindings for @abhinavakhil/penna-core

307 0

Version History

Version Released Vue License
0.1.1 ^3.5.0 MIT
0.1.0 ^3.5.0 MIT