Vue.js - 富文本编辑器Tiptap使用详解8(实现HTML模式、设计模式切换功能)
作者:hangge | 2026-02-22 09:33
八、实现 HTML 模式、设计模式切换功能
1,准备工作
(1)我们需要在前文的基础上,给自定义的工具栏添加“显示 HTML 代码 ↔ 正常设计编辑”这两种模式切换功能,实现原理如下:
- 切换到“HTML 模式”时:把 editor.getHTML() 赋值到一个 textarea(供用户查看/编辑原始 HTML),并让工具栏按钮失效(避免在 HTML 模式下误操作 editor)。
- 切换回“设计模式”时:把 textarea 内容用 editor.commands.setContent(html) 恢复回编辑器(若 HTML 不合法会捕获错误并提示)。
(2)这里首先依照前文准备好相关代码。
2,实现步骤
(1)首先修改自定义工具栏 Toolbar.vue,新增一个需要新增一个 disabled prop,并在 isDisabled、handleClick、onDropdownCommand、onMoreCommand 等方法中进行判断,确保 HTML 模式下相关功能被禁用。
<template>
<div :class="['toolbar-root', toolbarClass]" role="toolbar" :aria-label="ariaLabel">
<div class="toolbar-left flex items-center gap-1">
<template v-for="(btn, index) in visibleButtons">
<!-- 下拉按钮(使用 Element UI 的 el-dropdown) -->
<el-dropdown v-if="btn.type === 'dropdown'" trigger="click" :key="index"
@command="onDropdownCommand(btn, $event)">
<el-button :size="size" :disabled="isDisabled(btn)" :class="btnClass(btn)">
<i v-if="btn.icon" :class="btn.icon" aria-hidden="true"></i>
<span v-if="showLabels" class="ml-1">{{ btn.label }}</span>
<i class="fas fa-caret-down ml-2"></i>
</el-button>
<el-dropdown-menu slot="dropdown" class="toolbar-dropdown-menu">
<template v-for="(item, i) in btn.items">
<!-- 颜色选择器菜单项 -->
<div v-if="item.type === 'color-picker'" :key="i" class="toolbar-color-item"
@click.stop>
<el-color-picker ref="colorPicker" v-model="item.value" color-format="hex"
size="mini" :show-alpha="false" class="toolbar-color-picker"
@change="onColorChange(item)" :predefine="[
'#ff0000',
'#00ff00',
'#0000ff',
'#ff9800',
'#9c27b0'
]" />
<span class="toolbar-color-label" @click.stop="openColorPicker()">
{{ item.label }}
</span>
</div>
<!-- 字体菜单项:左侧小预览,右侧文字(点击文字或行都触发 onDropdownCommand) -->
<div v-if="item.type === 'font'" :key="i" class="toolbar-font-item"
@click.stop="onDropdownCommand(btn, item)">
<span class="font-sample" :style="{ fontFamily: item.font }"
aria-hidden="true">Aa</span>
<span class="toolbar-font-label">
{{ item.label }}
</span>
<!-- 可选:显示 active 状态 -->
<i v-if="item._active" class="fas fa-check toolbar-font-active"></i>
</div>
<!-- 普通菜单项 -->
<el-dropdown-item v-else :key="i" :command="item" :disabled="isDisabled(item)">
<i v-if="item.icon" :class="item.icon" class="mr-2"></i>
{{ item.label }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
<!-- 普通按钮 -->
<button v-else :class="btnClass(btn)" :title="btn.title || btn.label"
:aria-pressed="isActive(btn)":disabled="isDisabled(btn)" :key="index"
@click="handleClick(btn)" :type="'button'">
<i v-if="btn.icon" :class="btn.icon" aria-hidden="true"></i>
<span v-if="showLabels" class="ml-1">{{ btn.label }}</span>
</button>
</template>
<!-- overflow / 更多按钮 -->
<el-dropdown v-if="overflowButtons.length" trigger="click" @command="onMoreCommand">
<el-button :size="size" class="toolbar-more-btn">
<i class="fas fa-ellipsis-h"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(btn, idx) in overflowButtons" :key="idx" :command="btn"
:disabled="isDisabled(btn)" @click.native.stop>
<i v-if="btn.icon" :class="btn.icon" class="mr-2"></i>
{{ btn.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="toolbar-right ml-auto">
<slot name="right"></slot>
</div>
</div>
</template>
<script>
/**
* Toolbar.vue(工具栏)
*
* props:
* - editor: Tiptap Editor 实例(必填)
* - buttons: 按钮配置数组
*
* 每个按钮格式如下:
* {
* label: 按钮文字,
* icon: 图标类名(如 "fas fa-bold"),
* name: 节点/Mark 名称,用于 isActive 判断,
* attrs: 对应 isActive 的属性,
*
* command: string | function | undefined
* - 如果是字符串,会执行 editor.chain().focus()[command]()
* - 如果是函数,直接执行 command(editor)
*
* onClick: 自定义回调函数(editor)
*
* can: string
* - 用于 editor.can().chain().focus()[can]() 判断是否可执行
*
* type: 'dropdown'(可选)
* - 需要提供 items: [{ label, icon, command, onClick, name, attrs }]
* }
*
* - maxVisible: 最多显示多少个按钮,超出的放到 Overflow 菜单
* - size: Element UI 的尺寸 ('small'/'mini')
* - showLabels: 是否显示文字(只显示图标或图标+文字)
* - dark: 是否启用暗色模式
* - ariaLabel: 工具栏的 aria-label
*/
export default {
name: 'Toolbar',
props: {
editor: { type: Object, required: true },
buttons: { type: Array, required: true },
maxVisible: { type: Number, default: 7 },
size: { type: String, default: 'small' },
showLabels: { type: Boolean, default: false },
dark: { type: Boolean, default: false },
ariaLabel: { type: String, default: 'Editor toolbar' },
disabled: { type: Boolean, default: false }, // 新增:全局禁用(如 HTML 模式)
},
data() {
return { showMore: false }
},
computed: {
// 前 maxVisible 个按钮
visibleButtons() {
return this.buttons.slice(0, this.maxVisible)
},
// 超出 maxVisible 的按钮进入 More 菜单
overflowButtons() {
return this.buttons.slice(this.maxVisible)
},
// 根据 dark 切换样式
toolbarClass() {
return this.dark ? 'toolbar--dark' : 'toolbar--light'
}
},
methods: {
// 通用按钮 class:Tailwind 辅助类 + 激活态 + 禁用态
btnClass(btn) {
const base = 'toolbar-btn inline-flex items-center justify-center px-2 py-1 rounded'
const active = this.isActive(btn) ? ' toolbar-btn--active' : ''
const disabled = this.isDisabled(btn) ? ' opacity-50 cursor-not-allowed'
: ' hover:bg-gray-100'
return base + active + disabled
},
// 判断按钮是否处于 active 状态
isActive(btn) {
if (!this.editor) return false
try {
// 优先根据 name + attrs 判断
if (btn && btn.name) return this.editor.isActive(btn.name, btn.attrs || {})
// 如果只有 command 字符串,无法可靠判断激活态 → 默认 false
if (btn && typeof btn.command === 'string') {
return false
}
} catch (e) {
return false
}
return false
},
// 判断按钮是否禁用
isDisabled(btn) {
if (this.disabled) return true // 当外部标记为禁用时,所有按钮均不可用
if (!this.editor) return true
try {
if (btn && btn.disabled) return true
// 优先使用 btn.can 来测试可执行性
if (btn && btn.can) {
return !this.editor.can().chain().focus()[btn.can]().run()
}
// 如果 command 是字符串,尝试模拟执行判断是否可用
if (btn && typeof btn.command === 'string') {
try {
return !this.editor.can().chain().focus()[btn.command]().run()
} catch (e) {
// 如果无法检测,默认认为是可用的
return false
}
}
} catch (e) {
return false
}
return false
},
// 普通按钮点击事件
handleClick(btn) {
if (this.disabled) return
if (!this.editor) return
// 如果按钮提供 onClick,优先执行
if (btn.onClick && typeof btn.onClick === 'function') {
return btn.onClick(this.editor)
}
// command 是函数
if (typeof btn.command === 'function') {
return btn.command(this.editor)
}
// command 是字符串 → 调用 editor.chain().focus()[command]()
if (typeof btn.command === 'string') {
try {
this.editor.chain().focus()[btn.command]().run()
} catch (e) {
// 如果命令需要传参(如 setColor),并提供了 btn.args → 使用 args 调用
if (btn.args) {
try {
this.editor.chain().focus()[btn.command](...btn.args).run()
} catch (err) {
console.warn(err)
}
} else {
console.warn('执行命令失败:', btn.command, e)
}
}
}
},
// 下拉菜单点击命令
onDropdownCommand(btn, item) {
if (this.disabled) return
// item 是按钮项
if (!item) return
if (item.onClick && typeof item.onClick === 'function') {
return item.onClick(this.editor)
}
if (typeof item.command === 'string') {
try {
this.editor.chain().focus()[item.command](...(item.args || [])).run()
} catch (e) {
console.warn(e)
}
} else if (typeof item.command === 'function') {
item.command(this.editor)
}
},
// “更多 (More)” 菜单按钮点击
onMoreCommand(payload) {
if (this.disabled) return
const btn = payload
this.handleClick(btn)
},
// 自定义颜色
onColorChange(item) {
if (!this.editor || !item.value) return
this.editor
.chain()
.focus()
.setColor(item.value)
.run()
},
// 点击文字时,模拟点击 color-picker
openColorPicker(index) {
const picker = this.$refs.colorPicker?.[0]
if (!picker) return
// Element UI 内部 trigger
const trigger = picker.$el.querySelector(
'.el-color-picker__trigger'
)
trigger && trigger.click()
},
}
}
</script>
<style scoped>
/* Tailwind 可用时会增强样式,这里提供最基础的 fallback CSS */
.toolbar-root {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 6px;
}
.toolbar--light {
background: var(--toolbar-bg, #fafafa);
border: 1px solid var(--toolbar-border, #eee);
}
.toolbar--dark {
background: #111827;
border: 1px solid #333;
color: #e5e7eb;
}
.toolbar-btn {
background: transparent;
border: none;
cursor: pointer;
}
.toolbar-btn--active {
background: var(--btn-active-bg, #e6f7ff);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
}
.toolbar-more-btn {
background: transparent;
border: none;
padding: 6px 8px;
border-radius: 6px;
}
/* 颜色选择器菜单项 */
.toolbar-color-item {
display: flex;
align-items: center;
gap: 8px;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 20px;
cursor: default;
white-space: nowrap;
}
.toolbar-color-item:hover {
background-color: #f5f7fa;
}
/* 缩小取色器 color-picker 体积 */
.toolbar-color-picker ::v-deep(.el-color-picker__trigger) {
width: 18px;
height: 18px;
padding: 0;
margin-top: 5px;
}
/* 取色器右侧文字 */
.toolbar-color-label {
font-size: 13px;
color: #606266;
cursor: pointer;
user-select: none;
}
/* 字体菜单项(与 dropdown item 高度一致)*/
.toolbar-font-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
cursor: pointer;
white-space: nowrap;
}
.toolbar-font-item:hover {
background-color: #f5f7fa;
}
/* 左侧小样例 */
.font-sample {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 20px;
border-radius: 4px;
font-size: 12px;
line-height: 1;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.06);
background: white;
}
/* 右侧 label */
.toolbar-font-label {
font-size: 13px;
color: #606266;
}
/* active 标识(可用勾或高亮) */
.toolbar-font-active {
margin-left: auto;
color: #409eff;
}
</style>
(2)接着在父组件中添加 HTML 模式显示与切换功能代码(黄色高亮部分)
<template>
<div class="p-4">
<Toolbar :editor="editor" :buttons="buttons" :maxVisible="16"
:showLabels="false" :dark="false" size="small"
:disabled="showHtml">
<template #right>
<div class="flex items-center">
<!-- 切换按钮:显示 HTML / 设计 -->
<el-button size="small" @click="toggleHtml">
{{ showHtml ? '返回设计' : '显示 HTML' }}
</el-button>
<el-button size="small" @click="saveContent">保存</el-button>
</div>
</template>
</Toolbar>
<div class="mt-3 border rounded">
<!-- 设计编辑区域 -->
<editor-content v-show="!showHtml" :editor="editor" class="prose p-4 min-h-[200px]" />
<!-- HTML 编辑区域(简单 textarea,可换成 CodeMirror/Monaco) -->
<div v-show="showHtml" class="p-4">
<textarea v-model="htmlContent" class="w-full min-h-[260px] font-mono p-3 border rounded"
spellcheck="false"></textarea>
<div class="mt-2 flex gap-2">
<el-button size="small" @click="applyHtml">应用到编辑器</el-button>
<el-button size="small" type="danger" @click="revertHtml">放弃并返回</el-button>
<span class="text-sm text-gray-500 ml-auto">提示:编辑 HTML 后点击“应用到编辑器”。</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import Toolbar from './Toolbar.vue'
// 可选扩展(链接、图片、下划线、删除线等)
import Link from '@tiptap/extension-link'
import Underline from '@tiptap/extension-underline'
import Strike from '@tiptap/extension-strike'
import Image from '@tiptap/extension-image'
import { TextStyleKit } from '@tiptap/extension-text-style'
import TextAlign from '@tiptap/extension-text-align'
import { Color } from '@tiptap/extension-color'
export default {
components: { Toolbar, EditorContent },
data() {
return {
editor: null,
buttons: [],
showHtml: false,
htmlContent: '',
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
Link,
Underline,
Strike,
Image,
TextStyleKit,
TextAlign.configure({
types: ['heading', 'paragraph'], // 对齐支持哪些块级节点
}),
Color.configure({
types: ['textStyle'],
}),
],
content: `<p>示例文本:试试选择文字并点击工具栏按钮(或使用快捷键)</p>`
})
// 监听选区变化 & 内容变化
const syncColor = () => {
// 颜色
const color =
this.editor.getAttributes('textStyle').color || '#000000'
this.updateToolbarColor(color)
// 字体
const font = this.editor.getAttributes('textStyle').fontFamily || null
this.updateToolbarFont(font)
}
this.editor.on('selectionUpdate', syncColor)
this.editor.on('transaction', syncColor)
// 初始化同步一次
syncColor()
/**
* 丰富的工具栏按钮示例
* 包含:
* - 普通命令(toggleBold 等)
* - 下拉菜单(标题)
* - 自定义 onClick 行为(插入链接/图片)
* - 复杂 command 函数写法
*/
this.buttons = [
{ label: '加粗', icon: 'fas fa-bold', name: 'bold', command: 'toggleBold' },
{ label: '斜体', icon: 'fas fa-italic', name: 'italic', command: 'toggleItalic' },
{ label: '下划线', icon: 'fas fa-underline', name: 'underline', command: 'toggleUnderline' },
{ label: '删除线', icon: 'fas fa-strikethrough', name: 'strike', command: 'toggleStrike' },
{ label: '行内代码', icon: 'fas fa-code', name: 'code', command: 'toggleCode' },
{
label: '引用', icon: 'fas fa-quote-right', command: 'toggleBlockquote',
name: 'blockquote'
},
{
label: '左对齐',
icon: 'fas fa-align-left',
name: 'paragraph',
attrs: { textAlign: 'left' },
command: (editor) =>
editor.chain().focus().setTextAlign('left').run(),
},
{
label: '居中对齐',
icon: 'fas fa-align-center',
name: 'paragraph',
attrs: { textAlign: 'center' },
command: (editor) =>
editor.chain().focus().setTextAlign('center').run(),
},
{
label: '右对齐',
icon: 'fas fa-align-right',
name: 'paragraph',
attrs: { textAlign: 'right' },
command: (editor) =>
editor.chain().focus().setTextAlign('right').run(),
},
{
label: '取消对齐',
icon: 'fas fa-align-justify',
onClick: (editor) =>
editor.chain().focus().unsetTextAlign().run(),
},
// 文字颜色设置
{
type: 'dropdown',
label: '文字颜色',
icon: 'fas fa-palette',
items: [
{
label: '黑色',
icon: 'fas fa-circle text-black',
onClick: (editor) => editor.chain().focus().setColor('#000000').run(),
},
{
label: '蓝色',
icon: 'fas fa-circle text-blue-500',
onClick: (editor) => editor.chain().focus().setColor('#0066ff').run(),
},
{
label: '绿色',
icon: 'fas fa-circle text-green-500',
onClick: (editor) => editor.chain().focus().setColor('#22bb33').run(),
},
{
label: '红色',
icon: 'fas fa-circle text-red-500',
onClick: (editor) =>
editor.chain().focus().setColor('#ff0000').run(),
},
{
label: '清除颜色',
icon: 'fas fa-ban',
onClick: (editor) => editor.chain().focus().unsetColor().run(),
},
{
type: 'color-picker',
label: '自定义颜色',
value: '#ff0000',
},
],
},
// 字体大小设置
{
type: 'dropdown',
icon: 'fas fa-text-height',
label: '字体大小',
items: [
{
label: '14px',
command: 'setFontSize',
args: ['14px'],
name: 'textStyle',
attrs: { fontSize: '14px' }
},
{
label: '16px(默认)',
command: 'setFontSize',
args: ['16px'],
name: 'textStyle',
attrs: { fontSize: '16px' }
},
{
label: '18px',
command: 'setFontSize',
args: ['18px'],
name: 'textStyle',
attrs: { fontSize: '18px' }
},
// 自定义输入
{
label: '自定义大小…',
icon: 'fas fa-keyboard',
onClick: (editor) => {
const val = prompt('请输入字体大小,例如 22px 或 1.5rem')
if (!val) return
const valid = /^(\d+(px|em|rem|pt)|\d+(\.\d+)?(em|rem))$/.test(val)
if (!valid) {
alert('请输入合法的大小,例如:18px、1.2rem')
return
}
editor.chain().focus().setFontSize(val).run()
}
},
// 清除字体大小
{
label: '清除大小',
icon: 'fas fa-ban',
onClick: (editor) => editor.chain().focus().unsetFontSize().run()
}
]
},
// 字体设置
{
type: 'dropdown',
label: '字体',
icon: 'fas fa-font',
items: [
{
type: 'font',
label: '仿宋',
font: "'FangSong', '仿宋_GB2312', '仿宋'",
onClick: (editor) => {
editor
.chain()
.focus()
.setMark('textStyle', {
fontFamily: "'FangSong', '仿宋_GB2312', '仿宋'",
})
.run()
},
},
{
type: 'font',
label: 'PingFang SC',
font: "'PingFang SC'",
onClick: (editor) => {
editor.chain().focus().setMark('textStyle', { fontFamily: "'PingFang SC'" }).run()
},
},
{
type: 'font',
label: 'Microsoft YaHei',
font: "'Microsoft YaHei'",
onClick: (editor) => {
editor.chain().focus().setMark('textStyle', { fontFamily: "'Microsoft YaHei'" }).run()
},
},
{
type: 'font',
label: 'Arial',
font: "Arial",
onClick: (editor) => {
editor.chain().focus().setMark('textStyle', { fontFamily: "Arial" }).run()
},
},
{
type: 'font',
label: 'Times New Roman',
font: "'Times New Roman'",
onClick: (editor) => {
editor.chain().focus().setMark('textStyle', { fontFamily: "'Times New Roman'" }).run()
},
},
{
type: 'font',
label: 'Courier New',
font: "'Courier New', Courier, monospace",
onClick: (editor) => {
editor.chain().focus()
.setMark('textStyle', { fontFamily: "'Courier New', Courier, monospace" }).run()
},
},
{
type: 'font',
label: '自定义字体…',
icon: 'fas fa-keyboard',
onClick: (editor) => {
const val = prompt('请输入字体族,例如"Georgia,serif"或"MyFont,Arial"(可带引号)')
if (!val) return
editor.chain().focus().setMark('textStyle', { fontFamily: val }).run()
}
}
]
},
// 列表
{
label: '项目符号', icon: 'fas fa-list-ul', command: 'toggleBulletList',
name: 'bulletList'
},
{
label: '编号列表', icon: 'fas fa-list-ol', command: 'toggleOrderedList',
name: 'orderedList'
},
// 下拉:标题级别选择
{
type: 'dropdown',
label: '标题',
icon: 'fas fa-heading',
items: [
{ label: '正文 (p)', command: (editor) => editor.chain().focus().setParagraph().run() },
{
label: 'H1', command: 'toggleHeading', args: [1],
onClick: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()
},
{
label: 'H2', command: 'toggleHeading', args: [2],
onClick: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
},
{
label: 'H3', command: 'toggleHeading', args: [3],
onClick: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
},
],
},
// 自定义链接插入(手动 prompt)
{
label: '插入链接', icon: 'fas fa-link',
onClick: (editor) => {
const url = prompt('请输入 URL (以 http(s):// 开头):')
if (url) editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
},
// 图片(示例:插入图片)
{
label: '插入图片', icon: 'far fa-image',
onClick: (editor) => {
const url = prompt('图片 URL:')
if (url) editor.chain().focus().setImage({ src: url }).run()
}
},
// 自定义清除格式
{
label: '清除格式', icon: 'fas fa-eraser',
onClick: (editor) => editor.chain().focus().clearNodes().unsetAllMarks().run()
},
// 代码块、撤销、重做
{ label: '代码块', icon: 'fas fa-file-code', command: 'toggleCodeBlock' },
{
label: '撤销', icon: 'fas fa-undo',
command: (editor) => editor.chain().focus().undo().run()
},
{
label: '重做', icon: 'fas fa-redo',
command: (editor) => editor.chain().focus().redo().run()
},
]
},
beforeDestroy() {
if (this.editor) this.editor.destroy()
},
methods: {
saveContent() {
const html = this.editor.getHTML()
console.log('保存的 HTML', html)
alert('保存的 HTML 长度:' + html.length)
},
// 切换显示 HTML / 设计
toggleHtml() {
if (!this.showHtml) {
// 进入 HTML 模式:读取当前内容到 textarea
this.htmlContent = this.editor ? this.formatHtml(this.editor.getHTML()) : ''
this.showHtml = true
} else {
// 退出 HTML 模式:行为等同点击“应用到编辑器”
this.applyHtml()
}
},
// 将 textarea 的 HTML 应用回编辑器
applyHtml() {
if (!this.editor) {
alert('编辑器未就绪')
return
}
// 尝试将 HTML 设置回 editor,捕获异常(若 HTML 无效)
try {
// 使用 commands.setContent(Tiptap 支持)
this.editor.commands.setContent(this.htmlContent)
// 切回设计视图
this.showHtml = false
} catch (e) {
console.error(e)
alert('应用 HTML 到编辑器失败,请检查 HTML 是否正确:' + e.message)
// 保持在 HTML 模式,用户可修正
}
},
// 放弃改动并返回设计视图(不改变 editor 内容)
revertHtml() {
this.showHtml = false
},
// HTML格式化工具
formatHtml(html) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
function formatNode(node, indent = 0) {
const pad = ' '.repeat(indent)
let result = ''
node.childNodes.forEach(child => {
// 文本节点
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.trim()
if (text) {
result += pad + text + '\n'
}
return
}
// 元素节点
if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName.toLowerCase()
const attrs = Array.from(child.attributes)
.map(a => ` ${a.name}="${a.value}"`)
.join('')
const isSelfClosing = ['img', 'br', 'hr', 'input', 'meta', 'link'].includes(tag)
if (isSelfClosing) {
result += `${pad}<${tag}${attrs} />\n`
} else {
result += `${pad}<${tag}${attrs}>\n`
result += formatNode(child, indent + 1)
result += `${pad}</${tag}>\n`
}
}
})
return result
}
return formatNode(doc.body).trim()
},
// 更新工具栏色块
updateToolbarColor(color) {
const colorDropdown = this.buttons.find(
(b) => b.type === 'dropdown' && b.label === '文字颜色'
)
if (!colorDropdown) return
const pickerItem = colorDropdown.items.find(
(i) => i.type === 'color-picker'
)
if (!pickerItem) return
// 避免不必要的重复赋值(防抖)
if (pickerItem.value !== color) {
pickerItem.value = color
}
},
// 新增:更新 toolbar 中 font 下拉样例(将会被父组件渲染)
updateToolbarFont(font) {
const fontDropdown = this.buttons.find(b => b.type === 'dropdown' && b.label === '字体')
if (!fontDropdown) return
// 选择第一个 font 类型 item(通常是自定义色块所在位置)
// 这里我们循环更新每个 font item 的 state.active(可选)或 value
fontDropdown.items.forEach(item => {
if (item.type === 'font') {
// 标记 active:当 item.font 与当前 font 相同或包含时置 active
item._active = !!(font && item.font && item.font.split(',')
.map(s => s.trim().replace(/^"|'|`|`'$/g, '')).some(f => font.includes(f)))
}
})
},
}
}
</script>
<style scoped>
/* 示例样式(如项目中已有 Tailwind,可省略) */
.prose {
/* 基本文本样式 */
}
.toolbar-root {
padding: 6px;
}
</style>
3,运行测试
(1)点击工具栏上面的“显示 HTML”按钮即可切换到 HTML 代码模式。


(2)进入 HTML 模式后,内容区域会显示 HTML 代码,并且顶部工具栏会被禁用。我们可以直接对 HTML 代码进行编辑,然后应用并返回,或者放弃并返回。

全部评论(0)