--- title: RichText 富文本编辑器 description: 基于 Tiptap 的富文本编辑,支持字体、字号、对齐、文字与背景色、上下标、列表样式、图片视频上传、表格、预览打印、整理格式与源代码 --- # RichText 富文本编辑器 `RichText` 是基于 [Tiptap](https://tiptap.dev) 封装的富文本编辑器,`v-model` 输出干净的 HTML 字符串,可直接作为邮件正文、公告正文等场景使用。 ## 何时使用 - 录入需要富文本格式的正文(公告、邮件、通知、帮助文档); - 需要字体 / 字号 / 对齐 / 文字颜色 / 背景颜色 / 上标 / 下标等排版能力时; - 需要在正文中嵌入图片 / 视频、插入表格时。 > 纯文本或短文本请用 [Input](/ui/form/input)(`type="textarea"`),不要用富文本承载简单字段。 ## 基础用法 `v-model` 双向绑定 HTML 字符串,工具条默认置顶,内置字体 / 字号 / 对齐 / 颜色 / 上下标 / 表格 / 撤销重做。 ## 工具条 工具条按「一天点几次」而不是「功能重不重要」排布,并分成 7 组(组间靠**间距**区分,没有分割线): - **常驻**(顺序:历史 → 段落格式 → 行内格式 → 对齐 → 颜色 → 列表与插入 → 更多):撤销、重做 | 段落格式(正文 / 标题 1-6)| 加粗、斜体、下划线 | 对齐(左 / 中 / 右)| 字体颜色、背景颜色 | 无序列表、有序列表、图片、链接、插入表格 | `⋯`; - **收进 `⋯ 更多`**:`字体` / `字号`(二级菜单)、`格式` 组(删除线 / 上标 / 下标 / 引用 / **整理格式**)、`插入` 组(视频)、`查看与导出` 组(**预览** / **打印** / **源代码**)。菜单项带分组标题与快捷键提示(对着 Tiptap 的真实键位写的,如删除线 `Ctrl+Shift+S`)。 > 无序列表 / 有序列表的**样式**不在「更多」里 —— 它们挂在两个列表按钮自己的下拉上(点按钮展开样式面板),见「列表」一节。 图标 **18px**、按钮 32px(等于 `--g3-control-height`)、内边距 8px / 10px。图标前景用 `text-1`(88% 黑)而不是 ghost 按钮默认的 `text-2`(65%),描边保持 lucide 原生粗细 —— 实测「发虚」的主因是浅灰前景而不是线宽,所以只加深颜色、不加重描边。 ### 分组与折行 分组用 `role="group"` + `aria-label` 表达语义,**视觉上不画分割线**(对齐 TinyMCE 的 `tox-toolbar__group`): ```html
…
``` 分组容器用了 `display: contents` —— 组的盒子**不参与布局**,组内按钮被提升为工具条的直接 flex 项,因此**折行以按钮为单位**,每行都会填满、不会出现「整组被推到下一行、右边空一大片」。代价是间距不能再用组的 padding / `gap` 表达,改由按钮的 `margin-inline-end` 承担:组内 2px、组间 12px(6 倍对比构成分组感)。 > 用 `flex-wrap: wrap` + `min-width: 0` 给组**做不到**按按钮折行:组对工具条来说始终是一个不可分割的 flex 项,`flex-wrap` 只影响组内部如何排列。 ### 键盘操作 工具条是 `role="toolbar"` 的**单个 Tab 停靠点**(ARIA 作者实践):`Tab` 进入工具条后,用 **`←` / `→`** 在按钮间移动(两端循环),**`Home` / `End`** 跳到首 / 尾,再 `Tab` 离开落到正文。RTL 下左右键按屏幕方向解释。这样避免「20 多个按钮各占一个 Tab 位、键盘用户要按二十几次才能穿过工具条」。 内部用 roving tabindex 实现:焦点所在按钮 `tabindex=0`,其余 `-1`;按钮的禁用态(撤销 / 重做、图片上的对齐)变化后会重新归一化,不会出现「全部 -1 导致 Tab 进不来」。 ## 对齐与颜色 工具条提供三组排版能力,均作用于选区所在的块(正文段落 / 标题,表格单元格里的内容同样是段落): - **对齐**(左 / 居中 / 右):写 `text-align` 到块元素上;点当前档位即取消对齐,不需要单独的「取消」按钮; - **字体颜色**(`A` 图标 + 当前色条):写 `color`; - **背景颜色**(荧光笔图标 + 当前色条):写 `background-color`。 两个取色按钮都是「图标 + 当前色条」的形式,色条显示选区当前的颜色(未设色时是空位)。点开后是预设色板面板:常用色一屏排满(24px 色块),`无颜色` 清除该属性,右侧彩虹色块打开系统取色器选任意颜色。选完色面板自动收起。 ### 换一套色板 预设色板走 CSS 变量(`--g3-rich-text-swatch-text-1..10` / `--g3-rich-text-swatch-bg-1..10`),宿主可在任意祖先上覆盖,**不需要改组件代码、不需要发版**: ```css /* 换成品牌色板 */ .g3-rich-text__toolbar { --g3-rich-text-swatch-text-5: #ff4d4f; } ``` 变量化只解决「可覆盖」,默认色值不变(仍是 Ant Design 的 6 号色 / 1 号色)。**暗色主题下不会自动换一套色值** —— 正文颜色是文档内容的一部分,同一份 HTML 在不同主题下显示成不同颜色会让「所见即所得」失效;需要暗色调色板的宿主自行覆盖这些变量。 色板显示走变量,但**写进文档的始终是解析后的具体颜色**(`rgb(...)`),不会把 `var(--g3-…)` 存进 HTML:邮件正文、复制出去的内容、别人家的页面都没有这个变量,存变量会渲染成透明或黑色。 ## 图片 / 视频上传 图片和视频通过 `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 }> ``` ## 链接 工具条「链接」按钮弹出一个输入面板(不是 `window.prompt`),两个字段:**链接地址** 与 **显示文字**。填好后回车或点「确定」应用。两个输入框都**不设 placeholder** —— 字段名已经说明要填什么,示例文案属于噪音。 **操作区**(一行,「取消 / 确定」始终靠右成组): | 位置 | 按钮 | 说明 | | --- | --- | --- | | 左 | 移除链接 | 仅光标已在链接上时出现;危险语义与主操作分开,不让它和「确定」挨着 | | 右 | 取消 | Ghost 低强调,关闭面板且**不写入文档** | | 右 | 确定 | Primary 主操作;**只要地址非空就可用**(见下) | 按钮用**默认尺寸**(32px,等于 `--g3-control-height`)而不是 `size="small"`(24px):这是面板里唯一的决定动作,24px 在弹层里偏小,且无法与 `G3Input` 的 32px 等高、同一行里不齐平。用 `justify-content: space-between` 而非各组均分,是为了让**「确定」的位置在编辑态 / 非编辑态下始终不变**(编辑态左侧多出「移除链接」,靠右的主操作不该被挤动)。 **「显示文字」不是必填**(只填地址就能提交)。链接文字按「填的显示文字 → 选中文字 → 地址本身」依次兜底,因此最后一种情况下链接文字就是 URL: | 场景 | 「显示文字」的行为 | | --- | --- | | 有选中文字 | 预填选中的文字;改成别的内容即**替换选中文字**;留空则**沿用原文字** | | 空选区(光标停在某处) | 作为**链接文字插入**(等价于「先打字再选中」);留空则用**地址本身**当文字 | | 光标已在链接上 | 预填该链接的文字;改它就替换链接文字,留空则只改地址 | 其他规则: - **地址不带协议时自动补 `https://`**:`example.com` → `https://example.com`;已是完整 URL、`#` 锚点、`/` 站内路径、`mailto:` 的一律不动; - **光标已在链接上**:面板预填当前地址与链接文字,并多出一个「移除链接」按钮; - 为防 XSS,只有 `http` / `https` / `mailto` / `tel` / `#` / `/` 开头的地址会被写入。 ### 右键菜单 在正文的**链接**上右键,会在鼠标位置弹出操作菜单(在非链接处右键仍走浏览器原生菜单): | 菜单项 | 行为 | | --- | --- | | 编辑链接 | 打开链接面板并预填该链接的地址与文字 | | 打开链接 | 新标签页打开(带 `noopener,noreferrer`) | | 删除链接 | **只去掉链接、保留文字**(想连文字一起删,用选中 + `Delete` 更直接) | 菜单作用于**右键落点**那条链接,而不是当前光标所在处 —— 右键不一定移动光标,直接按选区操作会删错链接。 ## 列表 工具条上的「无序列表 / 有序列表」是**分裂按钮**(对齐 TinyMCE 的 `bullist` / `numlist`):**点图标 = 一键建 / 撤列表**,**点右侧箭头 = 打开样式面板**: | 面板 | 档位 | | --- | --- | | 无序列表 | 默认、实心圆点、空心圆圈、实心方块 | | 有序列表 | 默认、小写字母、小写罗马数字、小写希腊字母、大写罗马数字、大写字母 | 样式面板里每一项画成**缩略图**(三条内容线 + 左侧真实的项目符号 / 编号,与 TinyMCE 图标同一画法),下方标注档位名,**一眼扫过就能分辨**,不需要记住 `disc` / `circle` / `square` 这些英文枚举。当前档位打勾并高亮。 点某一档即把光标所在的列表设成该样式;**光标不在列表里时会先建列表再设样式** ——「想插一个方块列表」一次选择即可,不必先点无序列表再挑样式(内部是两个事务:建列表 + 设样式,按两次 `Ctrl+Z` 依次退回)。 ### 样式存成属性,不存成类名 样式写进文档的是 **HTML 属性**(`