注意,前方高能!CSS惨不忍睹~
1. 引言
Tiptap 是一个基于 ProseMirror 的富文本编辑器,具有高度的可定制性和扩展性。本文将逐步指导您如何在 Nuxt.js 项目中集成和使用 Tiptap 富文本编辑器,并实现一些常见功能,如表格、图片和视频插入。
2. 安装依赖
首先,我们需要安装 nuxt-tiptap-editor 模块,它可以帮助我们在 Nuxt.js 项目中轻松集成 Tiptap 编辑器。使用以下命令添加依赖:
npx nuxi@latest module add tiptap
3. 配置 nuxt.config.ts
接下来,我们需要在 nuxt.config.ts 文件中添加 nuxt-tiptap-editor 模块,并进行一些基本配置:
export default defineNuxtConfig({
modules: ['nuxt-tiptap-editor'],
tiptap: {
prefix: 'Tiptap', // 为 Tiptap 导入设置前缀,composables 不包括在内
},
});
4. 创建基础编辑器组件
在 components 目录下创建 TiptapEditor.vue 文件,并复制以下代码:
<template>
<div>
<div v-if="editor" class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
italic
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
strike
</button>
<button
@click="editor.chain().focus().toggleCode().run()"
:disabled="!editor.can().chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
>
code
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks
</button>
<button @click="editor.chain().focus().clearNodes().run()">
clear nodes
</button>
<button
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
>
paragraph
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
h1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
h2
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
>
h3
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
>
h4
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
>
h5
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
>
h6
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
bullet list
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
>
ordered list
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
>
code block
</button>
<button
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
>
blockquote
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
horizontal rule
</button>
<button @click="editor.chain().focus().setHardBreak().run()">
hard break
</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
undo
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
redo
</button>
</div>
<TiptapEditorContent :editor="editor" />
</div>
</template>
<script setup>
const editor = useEditor({
content: "<p>I'm running Tiptap with Vue.js. 🎉</p>",
extensions: [TiptapStarterKit],
});
onBeforeUnmount(() => {
unref(editor).destroy();
});
</script>
<style scoped>
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #eee;
}
button.is-active {
background: #ddd;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>
以上内容来自该包的快速开始翻译
5. 使用编辑器组件
在页面中引入并使用 TiptapEditor 组件:
<template>
<div>
<h1>My Nuxt.js App with Tiptap Editor</h1>
<TiptapEditor />
</div>
</template>
<script setup lang="ts"></script>
6. 美化编辑器
为了增强编辑器的视觉效果,我们可以添加一些样式,并扩展工具栏按钮的功能:
<template>
<div class="editor-container">
<div v-if="editor" class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
加粗
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
斜体
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
删除线
</button>
<button
@click="editor.chain().focus().toggleCode().run()"
:disabled="!editor.can().chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
>
代码
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
清除格式
</button>
<button @click="editor.chain().focus().clearNodes().run()">
清除节点
</button>
<button
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
>
段落
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
标题1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
标题2
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
>
标题3
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
>
标题4
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
>
标题5
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
>
标题6
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
无序列表
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
>
有序列表
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
>
代码块
</button>
<button
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
>
引用
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
水平线
</button>
<button @click="editor.chain().focus().setHardBreak().run()">换行</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
撤销
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
重做
</button>
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
插入表格
</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
在前面添加列
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
在后面添加列
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
删除列
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
在上面添加行
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
在下面添加行
</button>
<button @click="editor.chain().focus().deleteRow().run()">
删除行
</button>
<button @click="editor.chain().focus().deleteTable().run()">
删除表格
</button>
</div>
<TiptapEditorContent :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, watch } from "vue";
import { useEditor, EditorContent as TiptapEditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header';
const props = defineProps({
content: {
type: String,
default: "",
},
onUpdate: Function,
});
const editorContent = ref(props.content);
const editor = useEditor({
content: editorContent.value,
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
TableCell,
TableHeader,
],
onUpdate: ({ editor }) => {
editorContent.value = editor.getHTML();
if (props.onUpdate) {
props.onUpdate(editorContent.value);
}
},
});
watch(
() => props.content,
(newContent) => {
if (editor.value && newContent !== editorContent.value) {
editor.value.commands.setContent(newContent);
}
}
);
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
</script>
<style>
.editor-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
background: #fff;
border-radius: 8px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
background: #ffffff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tiptap {
box-sizing: border-box;
line-height: 2rem !important;
padding: 5px;
}
.editor-content {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
height: calc(100vh - 152px);
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #eee;
}
button.is-active {
background: #ddd;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.editor-content table {
width: 100%;
border-collapse: collapse;
}
.editor-content th,
.editor-content td {
border: 1px solid #ddd;
padding: 8px;
}
.editor-content th {
background-color: #f2f2f2;
text-align: left;
}
</style>
7. 加入表格功能
为了在编辑器中加入表格功能,我们需要安装相关依赖:
yarn add @tiptap/extension-table @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-table-row
然后在编辑器中集成表格功能:
<template>
<div class="editor-container">
<div v-if="editor" class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
粗
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
斜
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
U
</button>
<button
@click="editor.chain().focus().toggleCode().run()"
:disabled="!editor.can().chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
>
Code
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
清除格式
</button>
<button @click="editor.chain().focus().clearNodes().run()">
清除节点
</button>
<button
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
>
段落
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
标题1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
标题2
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
>
标题3
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
>
标题4
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
>
标题5
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
>
标题6
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
无序列表
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
>
有序列表
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
>
代码块
</button>
<button
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
>
引用
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
水平线
</button>
<button @click="editor.chain().focus().setHardBreak().run()">换行</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
撤销
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
重做
</button>
<button
@click="
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
"
>
插入表格
</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
在前面添加列
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
在后面添加列
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
删除列
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
在上面添加行
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
在下面添加行
</button>
<button @click="editor.chain().focus().deleteRow().run()">删除行</button>
<button @click="editor.chain().focus().deleteTable().run()">
删除表格
</button>
<!-- 其他按钮代码 -->
</div>
<TiptapEditorContent :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, watch } from "vue";
import { useEditor, EditorContent as TiptapEditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
const props = defineProps({
content: {
type: String,
default: "",
},
onUpdate: Function,
});
const editorContent = ref(props.content);
const editor = useEditor({
content: editorContent.value,
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
TableCell,
TableHeader,
],
onUpdate: ({ editor }) => {
editorContent.value = editor.getHTML();
if (props.onUpdate) {
props.onUpdate(editorContent.value);
}
},
});
watch(
() => props.content,
(newContent) => {
if (editor.value && newContent !== editorContent.value) {
editor.value.commands.setContent(newContent);
}
}
);
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
</script>
<style>
.editor-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
background: #fff;
border-radius: 8px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
background: #ffffff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tiptap {
box-sizing: border-box;
line-height: 2rem !important;
padding: 5px;
}
.editor-content {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
height: calc(100vh - 152px);
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #eee;
}
button.is-active {
background: #ddd;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.editor-content table {
width: 100%;
border-collapse: collapse;
}
.editor-content th,
.editor-content td {
border: 1px solid #ddd;
padding: 8px;
}
.editor-content th {
background-color: #f2f2f2;
text-align: left;
}
</style>
注意!一定要给一点基础的css,不然看不见你的表格长啥样
8. 实现图片插入功能
可以参考一下官方的文档 @Image extension
为了在编辑器中插入图片和视频,我们需要安装相关扩展:
yarn add @tiptap/extension-image
然后在编辑器中集成图片插件:
<template>
<div class="editor-container">
<div v-if="editor" class="toolbar">
<!-- 其他按钮代码 -->
<button @click="insertImage">插入图片</button>
<!-- 其他按钮代码 -->
</div>
<TiptapEditorContent :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, watch } from "vue";
import { useEditor, EditorContent as TiptapEditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Image from "@tiptap/extension-image";
const props = defineProps({
content: {
type: String,
default: "",
},
onUpdate: Function,
});
const editorContent = ref(props.content);
const editor = useEditor({
content: editorContent.value,
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
TableCell,
TableHeader,
Image,
Video,
],
onUpdate: ({ editor }) => {
editorContent.value = editor.getHTML();
if (props.onUpdate) {
props.onUpdate(editorContent.value);
}
},
});
watch(
() => props.content,
(newContent) => {
if (editor.value && newContent !== editorContent.value) {
editor.value.commands.setContent(newContent);
}
}
);
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
const insertImage = () => {
if (!editor) return;
const url = prompt("请输入图片URL");
if (url) {
editor.value.commands.setImage({ src: url });
}
};
</script>
<style>
.editor-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
background: #fff;
border-radius: 8px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
background: #ffffff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tiptap {
box-sizing: border-box;
line-height: 2rem !important;
padding: 5px;
}
.editor-content {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
height: calc(100vh - 152px);
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #eee;
}
button.is-active {
background: #ddd;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.editor-content table {
width: 100%;
border-collapse: collapse;
}
.editor-content th,
.editor-content td {
border: 1px solid #ddd;
padding: 8px;
}
.editor-content th {
background-color: #f2f2f2;
text-align: left;
}
</style>
9. 实现视频插入功能
在社区中没有找到video相关的插件,npm上也没搜到这里就自己写了。各位老哥如果有推荐可以评论区见~
有请Nuxt3插件系统
// plugins/extensions/video.ts
import { Node, mergeAttributes, CommandProps, RawCommands } from "@tiptap/core";
interface VideoOptions {
src: string;
controls?: boolean;
}
export const Video = Node.create({
name: "video",
group: "block",
selectable: true,
draggable: true,
addAttributes() {
return {
src: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute("src"),
renderHTML: (attributes: { [key: string]: any }) => {
if (!attributes.src) {
return {};
}
return { src: attributes.src };
},
},
controls: {
default: true,
parseHTML: (element: HTMLElement) => element.hasAttribute("controls"),
renderHTML: (attributes: { [key: string]: any }) => {
if (!attributes.controls) {
return {};
}
return { controls: attributes.controls };
},
},
};
},
parseHTML() {
return [
{
tag: "video",
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"video",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
];
},
addCommands() {
return {
setVideo:
(options: VideoOptions) =>
({ commands }: CommandProps) => {
return commands.insertContent({
type: this.name,
attrs: options,
});
},
} as Partial<RawCommands>;
},
});
在组件中初始化视频插件
<template>
<div class="editor-container">
<div v-if="editor" class="toolbar">
<!-- 其他按钮代码 -->
<button @click="insertImage">插入图片</button>
<button @click="insertVideo">插入视频</button>
<!-- 其他按钮代码 -->
</div>
<TiptapEditorContent :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, watch } from "vue";
import { useEditor, EditorContent as TiptapEditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Image from "@tiptap/extension-image";
import { Video } from "~/plugins/extensions/video";
const props = defineProps({
content: {
type: String,
default: "",
},
onUpdate: Function,
});
const editorContent = ref(props.content);
const editor = useEditor({
content: editorContent.value,
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
TableCell,
TableHeader,
Image.configure({
inline: false, // 配置选项
allowBase64: false, // 配置选项
HTMLAttributes: {
class: "my-custom-class", // 自定义 HTML 属性
},
}),
Video,
],
onUpdate: ({ editor }) => {
editorContent.value = editor.getHTML();
if (props.onUpdate) {
props.onUpdate(editorContent.value);
}
},
});
watch(
() => props.content,
(newContent) => {
if (editor.value && newContent !== editorContent.value) {
editor.value.commands.setContent(newContent);
}
}
);
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
const insertImage = () => {
if (!editor) return;
const url = prompt("请输入图片URL");
if (url) {
editor.value.commands.setImage({ src: url });
}
};
const insertVideo = () => {
if (!editor.value) return; // 确保 editor 已初始化
const url = prompt("请输入视频URL");
if (url) {
editor.value.commands.setVideo({ src: url });
}
};
</script>
<style>
.my-custom-class {
width: 90%;
margin: 0 5%;
}
.editor-container video {
width: 75%;
margin: 0 12.5%;
}
</style>
10. 总结
通过以上步骤,我们在 Nuxt.js 项目中成功集成了 Tiptap 富文本编辑器,并实现了包括基础文本格式化、表格操作、图片和视频插入等功能。Tiptap 的高度可扩展性使其成为一个非常强大的富文本编辑器解决方案,适用于各种复杂的编辑需求。希望本文能对您有所帮助,祝您开发愉快!
附件——完整代码
<template>
<div class="editor-container">
<div v-if="editor" class="toolbar">
<!-- 其他按钮代码 -->
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
加粗
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
斜体
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
删除线
</button>
<button
@click="editor.chain().focus().toggleCode().run()"
:disabled="!editor.can().chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
>
代码
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
清除格式
</button>
<button @click="editor.chain().focus().clearNodes().run()">
清除节点
</button>
<button
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
>
段落
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
标题1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
标题2
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
>
标题3
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
>
标题4
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
>
标题5
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
>
标题6
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
无序列表
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
>
有序列表
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
>
代码块
</button>
<button
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
>
引用
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
水平线
</button>
<button @click="editor.chain().focus().setHardBreak().run()">换行</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
撤销
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
重做
</button>
<button
@click="
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
"
>
插入表格
</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
在前面添加列
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
在后面添加列
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
删除列
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
在上面添加行
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
在下面添加行
</button>
<button @click="editor.chain().focus().deleteRow().run()">删除行</button>
<button @click="editor.chain().focus().deleteTable().run()">
删除表格
</button>
<button @click="insertImage">插入图片</button>
<button @click="insertVideo">插入视频</button>
<!-- 其他按钮代码 -->
</div>
<TiptapEditorContent :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, watch } from "vue";
import { useEditor, EditorContent as TiptapEditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Image from "@tiptap/extension-image";
import { Video } from "~/plugins/extensions/video";
const props = defineProps({
content: {
type: String,
default: "",
},
onUpdate: Function,
});
const editorContent = ref(props.content);
const editor = useEditor({
content: editorContent.value,
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
TableCell,
TableHeader,
Image.configure({
inline: false, // 配置选项
allowBase64: false, // 配置选项
HTMLAttributes: {
class: "my-custom-class", // 自定义 HTML 属性
},
}),
Video,
],
onUpdate: ({ editor }) => {
editorContent.value = editor.getHTML();
if (props.onUpdate) {
props.onUpdate(editorContent.value);
}
},
});
watch(
() => props.content,
(newContent) => {
if (editor.value && newContent !== editorContent.value) {
editor.value.commands.setContent(newContent);
}
}
);
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
const insertImage = () => {
if (!editor) return;
const url = prompt("请输入图片URL");
if (url) {
editor.value.commands.setImage({ src: url });
}
};
const insertVideo = () => {
if (!editor.value) return; // 确保 editor 已初始化
const url = prompt("请输入视频URL");
if (url) {
editor.value.commands.setVideo({ src: url });
}
};
</script>
<style>
.editor-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
background: #fff;
border-radius: 8px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
background: #ffffff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tiptap {
box-sizing: border-box;
line-height: 2rem !important;
padding: 5px;
}
.editor-content {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
height: calc(100vh - 152px);
overflow-y: auto;
}
button {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #eee;
}
button.is-active {
background: #ddd;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.editor-content table {
width: 100%;
border-collapse: collapse;
}
.editor-content th,
.editor-content td {
border: 1px solid #ddd;
padding: 8px;
}
.editor-content th {
background-color: #f2f2f2;
text-align: left;
}
.my-custom-class {
width: 90%;
margin: 0 5%;
}
.editor-container video {
width: 75%;
margin: 0 12.5%;
}
</style>