--- title: RichText 富文本编辑器 description: 基于 Tiptap 的富文本编辑,支持字体、字号、对齐、文字与背景色、上下标、图片视频上传与表格 --- # RichText 富文本编辑器 `RichText` 是基于 [Tiptap](https://tiptap.dev) 封装的富文本编辑器,`v-model` 输出干净的 HTML 字符串,可直接作为邮件正文、公告正文等场景使用。 ## 何时使用 - 录入需要富文本格式的正文(公告、邮件、通知、帮助文档); - 需要字体 / 字号 / 对齐 / 文字颜色 / 背景颜色 / 上标 / 下标等排版能力时; - 需要在正文中嵌入图片 / 视频、插入表格时。 > 纯文本或短文本请用 [Input](/ui/form/input)(`type="textarea"`),不要用富文本承载简单字段。 ## 基础用法 `v-model` 双向绑定 HTML 字符串,工具条默认置顶,内置字体 / 字号 / 对齐 / 颜色 / 上下标 / 表格 / 撤销重做。 ## 工具条 工具条按「一天点几次」而不是「功能重不重要」排布,**始终一行、高度恒定 48px**: - **常驻**(顺序:历史 → 正文格式 → 行内格式 → 对齐 → 颜色 → 列表与插入):撤销、重做 | 段落格式(正文 / 标题 1-3)| 加粗、斜体、下划线 | 对齐(左 / 中 / 右)| 字体颜色、背景颜色 | 无序列表、有序列表、链接、图片、插入表格; - **收进 `⋯ 更多`**:`字体` / `字号`(二级菜单,选中态跟随光标)、`格式` 组(删除线 / 上标 / 下标 / 引用)、`插入` 组(视频)。菜单项带分组标题与快捷键提示(对着 Tiptap 的真实键位写的,如删除线 `Ctrl+Shift+S`)。 图标 16px、按钮 32px(等于 `--g3-control-height`)、组内间隙 4px、内边距 8px / 10px(左右 10px 让首列图标与正文的 12px 内边距对齐);图标前景用 `text-1`(88% 黑)而不是 ghost 按钮默认的 `text-2`(65%),描边保持 lucide 原生粗细 —— 实测「发虚」的主因是浅灰前景而不是线宽,所以只加深颜色、不加重描边。整条工具条自然宽度约 **744px**:760px 以上的容器都是一行,再窄才折行,且折行以「组」为单位,不会把分隔线甩到行首/行尾。表格插入按钮留在工具条上而不是进 `⋯`:它要先选行列,收起后承载不了那个面板。 ## 对齐与颜色 工具条提供三组排版能力,均作用于选区所在的块(正文段落 / 标题,表格单元格里的内容同样是段落): - **对齐**(左 / 居中 / 右):写 `text-align` 到块元素上;点当前档位即取消对齐,不需要单独的「取消」按钮; - **字体颜色**(`A` 图标 + 当前色条):写 `color`; - **背景颜色**(荧光笔图标 + 当前色条):写 `background-color`。 两个取色按钮都是「图标 + 当前色条」的形式,色条显示选区当前的颜色(未设色时是空位)。点开后是预设色板面板:常用色一屏排满,`无颜色` 清除该属性,右侧彩虹色块打开系统取色器选任意颜色。选完色面板自动收起。 ## 图片 / 视频上传 图片和视频通过 `upload-image` / `upload-video` 两个 prop 注入上传函数。上传函数接收 `File`,返回 `{ fileUrl, ... }` 元数据;上传完成后组件会触发 `upload-inline` 事件,把内联资源元数据上报给外层(对齐邮件「inline 附件」模型)。 ```ts // 上传函数约定 type Upload = (file: File) => Promise<{ fileUrl: string // 正文 img / video 的 src fileId?: string // 后端文件 id cid?: string // 内联资源 cid(邮件场景) fileName?: string fileSize?: number }> ``` ## 只读 / 禁用 `readonly` 可聚焦可复制但不可编辑;`disabled` 整体灰化不可交互。 ## 表格 工具条「插入表格」按钮弹出行 / 列选择面板(支持关闭表头行);光标进入表格后,表格下方会浮出就近操作栏: - **行**:上方插入行、下方插入行、删除行; - **列**:左侧插入列、右侧插入列、删除列; - **单元格**:合并单元格、拆分单元格(两个独立按钮,不可用时各自置灰); - **表头**:切换表头列、切换表头单元格; - **删除**:整表删除(与增删行列用分隔线隔开,防误点)。 单元格内文字的对齐、颜色在顶部工具条里设置(单元格内容也是普通段落),操作栏不再重复提供。 列宽拖拽:悬停列分隔线出现 `col-resize` 光标,左右拖动调整列宽。拖拽采用 `table-layout: fixed`,**只动被拖列、其余列纹丝不动**(Excel 语义);列宽以 `colwidth` 属性持久化进 HTML,最小列宽 10px。拖宽后表格总宽超过容器时,在表格容器内横向滚动,不撑破编辑器内容区。`Tab` / `Shift+Tab` 在单元格间跳转,末格 `Tab` 自动追加行。 > 行列数限制为 1–20,超出会在插入时自动夹回,避免一次性生成超大表格卡死编辑器。 ## 获取纯文本 通过 `ref` 调用 `getText()` 拿到纯文本(邮件 `text/plain` 备选),`getHTML()` 拿 HTML,`clear()` 清空。 ## API ### Props | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `modelValue` | `string` | `''` | 双向绑定的 HTML 字符串 | | `placeholder` | `string` | — | 占位文案 | | `disabled` | `boolean` | `false` | 禁用态 | | `readonly` | `boolean` | `false` | 只读态:可聚焦、可复制,但不可编辑 | | `toolbar` | `'top' \| 'bottom' \| false` | `'top'` | 工具条位置;`false` 隐藏工具条 | | `minHeight` | `number \| string` | `200` | 编辑区最小高度 | | `maxHeight` | `number \| string` | — | 编辑区最大高度(超出滚动) | | `uploadImage` | `(file: File) => Promise` | — | 图片上传函数,返回完整元数据 | | `uploadVideo` | `(file: File) => Promise` | — | 视频上传函数,返回完整元数据 | | `fonts` | `RichTextFontOption[]` | 内置字体表 | 「更多」菜单里字体子菜单的选项 | | `fontSizes` | `RichTextFontSizeOption[]` | 内置字号表 | 「更多」菜单里字号子菜单的选项 | ### Events | 名称 | 参数 | 说明 | | --- | --- | --- | | `update:modelValue` | `(html: string)` | 内容变化时触发 | | `focus` | `(event: FocusEvent)` | 获得焦点 | | `blur` | `(event: FocusEvent)` | 失去焦点 | | `upload-inline` | `(result: RichTextUploadResult, kind: 'image' \| 'video')` | 内联资源上传完成,上报给外层收集 | | `upload-error` | `(error: Error, kind: 'image' \| 'video')` | 上传失败 | ### Exposed(通过 ref 调用) | 名称 | 签名 | 说明 | | --- | --- | --- | | `editor` | `Editor` | 底层 Tiptap 实例(进阶用法) | | `getHTML` | `() => string` | 当前 HTML | | `getText` | `() => string` | 纯文本(邮件 text/plain) | | `focus` | `() => void` | 聚焦 | | `clear` | `() => void` | 清空内容 | ### 类型定义 ```ts export interface RichTextUploadResult { fileUrl: string fileId?: string cid?: string fileName?: string fileSize?: number [key: string]: unknown } export interface RichTextFontOption { label: string value: string } export interface RichTextFontSizeOption { label: string value: string } ``` ### 样式变量 | 变量 | 默认值 | 说明 | | --- | --- | --- | | `--g3-border-color` | `#d9d9d9` | 容器 / 表格边框 | | `--g3-radius` | `6px` | 容器圆角 | | `--g3-fill` | `rgba(0,0,0,.04)` | 工具条背景、表头背景 | | `--g3-color-primary` | `#18181b` | 激活态、链接、引用竖线颜色、聚焦时正文区的 2px 描边 | | `--g3-color-primary-soft` | 主色 12% | 按钮激活底、选中单元格底 | | `--g3-rich-text-toolbar-shadow-near` | `rgba(34,47,62,.1)` | 工具条投影近层(`0 2px 2px -2px`,`toolbar="bottom"` 时翻到上方) | | `--g3-rich-text-toolbar-shadow-far` | `rgba(34,47,62,.07)` | 工具条投影远层(`0 8px 8px -4px` 弥散) | ## 实现说明 - **选型 Tiptap**:MIT 协议、Vue 3 官方绑定(`@tiptap/vue-3`)、headless 架构便于贴合 G3 设计体系;相比 HugeRTE(TinyMCE 的 MIT fork)在 Vue 集成与 UI 定制上更契合组件库封装; - **不含代码块**:从 `starter-kit` 剔除了 Code / CodeBlock(ERP 场景不需要); - **字体 / 字号 / 颜色 / 背景色**:用官方 `TextStyleKit`(内置 `setFontFamily` / `setFontSize` / `setColor` / `setBackgroundColor` 命令),行高保持关闭;颜色只写 `textStyle` 记号(``),不引入额外的 mark; - **对齐**:官方 `@tiptap/extension-text-align`,只挂到 `heading` / `paragraph`,`alignments` 限左中右;面板里是 `toggleTextAlign`,点当前档位即取消; - **内容区即编辑区**:`minHeight` / `maxHeight` 作用在内容区上,可编辑区(`.tiptap`)用 `flex: 1 0 auto` 撑满 —— 内容短时整块都能点(点空白即聚焦、光标落到正文末尾),内容长时按内容高度撑开、由内容区滚动且不裁切。注意 EditorContent 会在内容区与可编辑区之间再插一层包裹 DOM,所以 `.tiptap` 上的 `min-height: inherit` 取的是那一层的值(恒为 0),撑不高可编辑区; - **聚焦高亮只给正文区**:编辑器获得焦点时(根节点 `:focus-within`),正文区四周亮起 2px 主色内描边,工具条不参与 —— 谁可编辑谁亮。用 `inset box-shadow` 而不是 `border`(不参与布局,聚焦不挤动正文),也不用 TinyMCE 那种 `::after` 覆盖层(正文区就是滚动容器,覆盖层会跟着正文滚走,而滚动容器的阴影不随内容滚动,天然钉在内容盒上); - **工具条密度与「更多」**:按钮 32px(等于 `--g3-control-height`)+ 组内 4px 间隙,字体 / 字号与低频功能收进 `⋯` 菜单,自然宽度从 928px 降到 744px;`⋯` 用 `G3Dropdown` 现成的 `type: 'label'`(分组标题)/ `children`(子菜单)/ `shortcut` / `selected` 能力拼出来,与工具条共用同一份 `ToolButton` 定义,激活态与禁用态一致。菜单内容约 295px,靠 Dropdown 默认的视口上限(`calc(100vh - 48px)`)保证面板内部不出滚动条,不需要额外传 `menuMaxHeight`; - **图标分量**:16px 图标 + lucide 原生描边(16px 下约 1.33px),只把前景从 ghost 默认的 `text-2`(65%)提到 `text-1`(88%)。试过描边 2.5 与放大到 24px,都偏重;实测「发虚」的主因是浅灰前景,动颜色这一项性价比最高; - **视频自写 node**:Tiptap 官方无视频扩展,`VideoNode.ts` 定义了轻量 `video` block node,输出 `