Vue.js - 富文本编辑器Tiptap使用详解12(添加表格功能)
作者:hangge | 2026-02-28 08:31
表格是许多编辑器必备的功能。Tiptap 提供了 Table / TableKit 等扩展,能让你在编辑器里插入/编辑/操作表格(增删行列、合并拆分单元格、切换表头、单元格属性等)。下面我将通过样例进行演示。
(2)关于该扩展的基本说明如下:
(2)运行效果如下,我们可以通过顶部工具栏在编辑器中插入表格,并对表格进行添加删除行列、合并拆分单元格等操作。
(2)运行效果如下,当我们将鼠标移动到边框线上时,可以拖动调节列宽。
(2)运行效果如下,选中单元格后点击“设置背景色”按钮会弹出输入框,填写颜色值后会将其设置为背景色。点击“清除背景色”按钮则会消除当前选中单元格的背景色。
(2)运行效果如下,在表格上选中内容时会出现气泡菜单。
十二、添加表格功能
1,安装表格扩展
(1)要使用表格功能,首先我们需要安装表格扩展,该扩展包含 Table、TableRow、TableCell、TableHeader 等:
npm install @tiptap/extension-table
(2)关于该扩展的基本说明如下:
- 表格由多个 node 共同构成:table、tableRow、tableCell、tableHeader。要能解析 HTML 或运行表格命令,必须在 Editor 的 extensions 中注册这些扩展(或直接使用 TableKit)。
- 插入表格后可通过扩展提供的命令(如 insertTable、addRowAfter、addColumnBefore、mergeCells 等)来修改表格结构。官方文档列出了完整命令列表(点击访问)。
- 如果需要用户拖拽调整列宽,开启 Table 的 resizable 配置(默认 false)。
2,使用样例
(1)下面代码演示如何初始化编辑器、引入 TableKit 插入表格、并使用一组按钮来操作表格。
<template>
<div class="p-4">
<div class="mb-3 flex gap-2">
<button @click="insertTable" class="btn">插入表格</button>
<button @click="addRowAfter" class="btn">下方加行</button>
<button @click="addRowBefore" class="btn">上方加行</button>
<button @click="addColAfter" class="btn">右侧加列</button>
<button @click="deleteRow" class="btn">删除行</button>
<button @click="deleteCol" class="btn">删除列</button>
<button @click="deleteTable" class="btn">删除表格</button>
<button @click="toggleHeaderRow" class="btn">切换表头行</button>
<button @click="mergeCells" class="btn">合并单元格</button>
<button @click="splitCell" class="btn">拆分单元格</button>
</div>
<div class="editor-box border rounded">
<editor-content :editor="editor" />
</div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
export default {
name: 'EditorWithTable',
components: { EditorContent },
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
// 启用表格并允许列宽可拖拽
TableKit.configure({
table: {
// 开启列拖拽调整
resizable: true,
// 拖拽手柄宽度(像素),可按需调整
handleWidth: 8,
// 单元格最小宽度(像素),避免拖到太小
cellMinWidth: 40,
// renderWrapper 若为 true,可在只读/非 resizable 场景保持 wrapper 布局一致
renderWrapper: true,
}
}),
],
content: `
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>刘小龙</td><td>88</td></tr>
<tr><td>2</td><td>张大伟</td><td>66</td></tr>
</tbody>
</table>
`,
})
},
beforeDestroy() {
if (this.editor) this.editor.destroy()
},
methods: {
insertTable() {
// 默认:3x3,有表头
this.editor.chain().focus().insertTable().run()
// 自定义大小:
// this.editor.chain().focus().insertTable({ rows: 4, cols: 5, withHeaderRow: false }).run()
},
addRowAfter() { this.editor.chain().focus().addRowAfter().run() },
addRowBefore() { this.editor.chain().focus().addRowBefore().run() },
addColAfter() { this.editor.chain().focus().addColumnAfter().run() },
deleteRow() { this.editor.chain().focus().deleteRow().run() },
deleteCol() { this.editor.chain().focus().deleteColumn().run() },
deleteTable() { this.editor.chain().focus().deleteTable().run() },
mergeCells() { this.editor.chain().focus().mergeCells().run() },
splitCell() { this.editor.chain().focus().splitCell().run() },
toggleHeaderRow() { this.editor.chain().focus().toggleHeaderRow().run() },
},
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }
/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
margin: 6px 0;
}
/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
padding: 8px 10px;
vertical-align: middle;
}
/* 表头样式(可选) */
::v-deep .ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
word-break: break-word;
}
/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
/* 提高优先级避免被 reset 覆盖 */
border-color: #d1d5db;
}
/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
background-color: rgba(200, 200, 255, 0.4) !important;
/* 防止 reset 覆盖 */
}
/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
content: "";
position: absolute;
right: -3px; /* 命中区域 */
top: 0;
width: 6px;
height: 100%;
cursor: col-resize !important;
}
</style>
(2)运行效果如下,我们可以通过顶部工具栏在编辑器中插入表格,并对表格进行添加删除行列、合并拆分单元格等操作。

附一:实现手动调整列宽
(1)Table 扩展支持我们手动拖拽调整列宽,只需要将 resizable 配置设置为 true 即可,并且其还提供了一些其他的微调参数。
this.editor = new Editor({
extensions: [
StarterKit,
// 启用表格并允许列宽可拖拽
TableKit.configure({
table: {
// 开启列拖拽调整
resizable: true,
// 拖拽手柄宽度(像素),可按需调整
handleWidth: 8,
// 单元格最小宽度(像素),避免拖到太小
cellMinWidth: 40,
// renderWrapper 若为 true,可在只读/非 resizable 场景保持 wrapper 布局一致
renderWrapper: true,
}
}),
],
content: `<p>下面示例插入一个 3x3 表格:</p>`,
})
(2)运行效果如下,当我们将鼠标移动到边框线上时,可以拖动调节列宽。

附二、增加设置单元格颜色功能
(1)Tiptap 默认不包含“单元格颜色”功能,不过我们可以在 Tiptap 的 tableCell / tableHeader 扩展中注册了自定义 attribute,然后通过比如 setCellAttribute('backgroundColor', '#ff0') 来设置当前单元格的背景色,具体代码如下。
<template>
<div class="p-4">
<div class="mb-3 flex gap-2">
<button @click="setCellBg" class="btn">设置背景色</button>
<button @click="clearCellBg" class="btn">清除背景色</button>
</div>
<div class="editor-box border rounded">
<editor-content :editor="editor" />
</div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit, TableCell } from '@tiptap/extension-table'
export default {
name: 'EditorWithTable',
components: { EditorContent },
data() {
return {
editor: null,
}
},
mounted() {
/**
* 扩展 TableCell,添加 backgroundColor 属性
* - parseHTML: 从元素 style 读取 background-color
* - renderHTML: 当有属性时把 background-color 写入 style
*/
const CustomTableCell = TableCell.extend({
name: 'tableCell', // 保持同名以替换原有 tableCell 节点
addAttributes() {
// 取父类已有的 attributes(如果有),然后合并自定义属性
const parent = TableCell.options && TableCell.options.addAttributes
? TableCell.options.addAttributes()
: {}
return {
// 保留原有 attrs(例如 colspan/rowspan 等),再添加 backgroundColor
...parent,
backgroundColor: {
default: null,
parseHTML: element => {
// 读取 inline style 的 background-color(若存在)
return element.style &&
element.style.backgroundColor ? element.style.backgroundColor : null
},
renderHTML: attrs => {
if (!attrs || !attrs.backgroundColor) return {}
return {
style: `background-color: ${attrs.backgroundColor}`,
}
},
},
}
},
})
this.editor = new Editor({
extensions: [
StarterKit,
TableKit,
// 使用自定义 TableCell 替代默认 TableCell
CustomTableCell
],
content: `
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>刘小龙</td><td>88</td></tr>
<tr><td>2</td><td>张大伟</td><td>66</td></tr>
</tbody>
</table>
`,
})
},
beforeDestroy() {
if (this.editor) this.editor.destroy()
},
methods: {
// 设置背景色
setCellBg() {
// 简单 prompt 演示:输入 HEX 或 color 名称
const val = prompt('输入背景色(例如 #ffeb3b 或 lightblue):', '#fffae6')
if (!val) return
// 使用 setCellAttribute(table 扩展提供的命令)
this.editor.chain().focus().setCellAttribute('backgroundColor', val).run()
},
// 清除背景色
clearCellBg() {
this.editor.chain().focus().setCellAttribute('backgroundColor', null).run()
},
},
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }
/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
margin: 6px 0;
}
/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
padding: 8px 10px;
vertical-align: middle;
}
/* 表头样式(可选) */
::v-deep .ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
word-break: break-word;
}
/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
/* 提高优先级避免被 reset 覆盖 */
border-color: #d1d5db;
}
/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
background-color: rgba(200, 200, 255, 0.4) !important;
/* 防止 reset 覆盖 */
}
/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
content: "";
position: absolute;
right: -3px; /* 命中区域 */
top: 0;
width: 6px;
height: 100%;
cursor: col-resize !important;
}
</style>
(2)运行效果如下,选中单元格后点击“设置背景色”按钮会弹出输入框,填写颜色值后会将其设置为背景色。点击“清除背景色”按钮则会消除当前选中单元格的背景色。

附三、给表格添加气泡菜单
(1)为方便表格操作,我们可以给表格添加一个气泡菜单集成常用的功能。并且在气泡菜单呈现时需要判断选区是否位于表格内,避免气泡无谓显示。
<template>
<div class="p-4">
<div class="mb-3 flex gap-2">
<button @click="insertTable" class="btn">插入表格</button>
<button @click="addRowAfter" class="btn">下方加行</button>
<button @click="addRowBefore" class="btn">上方加行</button>
<button @click="addColAfter" class="btn">右侧加列</button>
<button @click="deleteRow" class="btn">删除行</button>
<button @click="deleteCol" class="btn">删除列</button>
<button @click="deleteTable" class="btn">删除表格</button>
<button @click="toggleHeaderRow" class="btn">切换表头行</button>
<button @click="mergeCells" class="btn">合并单元格</button>
<button @click="splitCell" class="btn">拆分单元格</button>
</div>
<!-- 气泡菜单(表格内部右键/选中时显示) -->
<bubble-menu
v-if="editor"
:editor="editor"
:should-show="shouldShowTableBubbleMenu"
:tippy-options="{ placement: 'top' }"
class="table-bubble-menu">
<div class="tiptap-bubble-menu">
<button @click="addRowBefore">上方加行</button>
<button @click="addRowAfter">下方加行</button>
<button @click="addColAfter">右侧加列</button>
<button @click="deleteRow">删除行</button>
<button @click="deleteCol">删除列</button>
<button @click="mergeCells">合并</button>
<button @click="splitCell">拆分</button>
</div>
</bubble-menu>
<div class="editor-box border rounded">
<editor-content :editor="editor" />
</div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import { BubbleMenu } from '@tiptap/vue-2/menus'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
export default {
name: 'EditorWithTable',
components: { EditorContent, BubbleMenu},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
// 启用表格并允许列宽可拖拽
TableKit.configure({
table: {
// 开启列拖拽调整
resizable: true,
// 拖拽手柄宽度(像素),可按需调整
handleWidth: 8,
// 单元格最小宽度(像素),避免拖到太小
cellMinWidth: 40,
// renderWrapper 若为 true,可在只读/非 resizable 场景保持 wrapper 布局一致
renderWrapper: true,
}
}),
],
content: `
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>刘小龙</td><td>88</td></tr>
<tr><td>2</td><td>张大伟</td><td>66</td></tr>
</tbody>
</table>
`,
})
},
beforeDestroy() {
if (this.editor) this.editor.destroy()
},
methods: {
// 表格气泡菜单显示条件
shouldShowTableBubbleMenu({ editor, state }) {
const { from, to } = state.selection
// 是否选中区域包含表格节点
const isWithinTable = editor.isActive('table')
|| editor.isActive('tableRow')
|| editor.isActive('tableCell')
|| editor.isActive('tableHeader')
if (!isWithinTable) return false
// (可选)避免整段空选区显示
if (from === to) return false
return true
},
insertTable() {
// 默认:3x3,有表头
this.editor.chain().focus().insertTable().run()
// 自定义大小:
// this.editor.chain().focus().insertTable({ rows: 4, cols: 5, withHeaderRow: false }).run()
},
addRowAfter() { this.editor.chain().focus().addRowAfter().run() },
addRowBefore() { this.editor.chain().focus().addRowBefore().run() },
addColAfter() { this.editor.chain().focus().addColumnAfter().run() },
deleteRow() { this.editor.chain().focus().deleteRow().run() },
deleteCol() { this.editor.chain().focus().deleteColumn().run() },
deleteTable() { this.editor.chain().focus().deleteTable().run() },
mergeCells() { this.editor.chain().focus().mergeCells().run() },
splitCell() { this.editor.chain().focus().splitCell().run() },
toggleHeaderRow() { this.editor.chain().focus().toggleHeaderRow().run() },
},
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }
/** 气泡菜单样式 */
.table-bubble-menu .tiptap-bubble-menu {
display: flex;
gap: 4px;
padding: 6px 8px;
background: rgba(30, 30, 30, 0.92);
border-radius: 8px;
backdrop-filter: blur(6px);
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
border: 1px solid rgba(255,255,255,0.1);
}
/* 按钮 */
.table-bubble-menu .tiptap-bubble-menu button {
font-size: 13px;
color: #eee;
background: transparent;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
transition: all .15s ease;
}
/* hover 效果 */
.table-bubble-menu .tiptap-bubble-menu button:hover {
background: rgba(255,255,255,0.12);
border-color: rgba(255,255,255,0.25);
}
/* 激活状态(可选) */
.table-bubble-menu .tiptap-bubble-menu button:active {
background: rgba(255,255,255,0.18);
}
/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
margin: 6px 0;
}
/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
padding: 8px 10px;
vertical-align: middle;
}
/* 表头样式(可选) */
::v-deep .ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
word-break: break-word;
}
/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
/* 提高优先级避免被 reset 覆盖 */
border-color: #d1d5db;
}
/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
background-color: rgba(200, 200, 255, 0.4) !important;
/* 防止 reset 覆盖 */
}
/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
content: "";
position: absolute;
right: -3px; /* 命中区域 */
top: 0;
width: 6px;
height: 100%;
cursor: col-resize !important;
}
</style>
(2)运行效果如下,在表格上选中内容时会出现气泡菜单。
全部评论(0)