Files
workspace/code/g3soft-libs/docs/content/ui/form/rich-text.md
T
2026-10-11 22:14:13 +08:00

11 KiB
Raw Blame History

title, description
title description
RichText 富文本编辑器 基于 Tiptap 的富文本编辑,支持字体、字号、对齐、文字与背景色、上下标、图片视频上传与表格

RichText 富文本编辑器

RichText 是基于 Tiptap 封装的富文本编辑器,v-model 输出干净的 HTML 字符串,可直接作为邮件正文、公告正文等场景使用。

何时使用

  • 录入需要富文本格式的正文(公告、邮件、通知、帮助文档);
  • 需要字体 / 字号 / 对齐 / 文字颜色 / 背景颜色 / 上标 / 下标等排版能力时;
  • 需要在正文中嵌入图片 / 视频、插入表格时。

纯文本或短文本请用 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 附件」模型)。

// 上传函数约定
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<RichTextUploadResult> — 图片上传函数,返回完整元数据
uploadVideo (file: File) => Promise<RichTextUploadResult> — 视频上传函数,返回完整元数据
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 清空内容

类型定义

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 记号(<span style="color: …">),不引入额外的 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,输出 <video controls> 标签;邮件场景由业务层决定是否降级为「封面图 + 附件」(编辑器不绑定邮件语义);
  • 内联资源上报:图片 / 视频上传后 emit('upload-inline', result, kind),外层把 result 收集进 inlines 数组,与邮件「inline 附件」模型对齐(参考 Email-Vue 的 @upload-image);
  • 依赖策略:Tiptap 核心走 peerDependencies,扩展走 dependencies,不把编辑器全家桶强塞给所有 @g3soft/ui 使用者。