
本文手把手教你利用 vuetify 原生组件(如 v-textarea 和 v-btn-toggle)快速搭建一个功能完备、响应式且可扩展的 wysiwyg 富文本编辑器,兼顾开发效率与学习深度。
本文手把手教你利用 vuetify 原生组件(如 v-textarea 和 v-btn-toggle)快速搭建一个功能完备、响应式且可扩展的 wysiwyg 富文本编辑器,兼顾开发效率与学习深度。
Vuetify 作为成熟的 Vue UI 框架,提供了大量开箱即用、语义清晰且高度可定制的组件,使其成为构建轻量级 WYSIWYG 编辑器的理想基础——无需从零实现 DOM 操作或内容editable逻辑,而是聚焦于状态管理与交互编排。
核心思路:声明式控制 + 双向绑定
WYSIWYG 的本质是“所见即所得”,在 Vue 中可通过响应式数据驱动视图样式。我们使用
以下是一个最小可行示例(Vue 3 + Composition API + Vuetify 3):
<template>
<v-card class="pa-4">
<!-- 工具栏 -->
<v-btn-toggle v-model="activeFormat" multiple class="mb-4">
<v-btn value="bold" @click="applyFormat('bold')">B</v-btn>
<v-btn value="italic" @click="applyFormat('italic')">I</v-btn>
<v-btn value="h2" @click="applyFormat('h2')">H2</v-btn>
<v-btn value="ul" @click="applyFormat('ul')">•</v-btn>
</v-btn-toggle>
<!-- 编辑区(使用 contenteditable 实现更精准控制) -->
<div
ref="editorRef"
contenteditable
class="pa-3 border rounded-md bg-grey-lighten-4 font-sans min-h-[120px]"
@input="onInput"
@keydown="handleKeydown"
>
{{ formattedContent }}
</div>
<!-- 预览区(可选) -->
<v-divider class="my-4"></v-divider>
<div class="text-caption font-weight-medium">实时预览:</div>
<div class="mt-2 p-3 bg-grey-lighten-5 rounded" v-html="formattedContent"></div>
</v-card>
</template>
<script setup>
import { ref, watch } from 'vue'
const editorRef = ref(null)
const rawContent = ref('')
const activeFormat = ref([])
const formattedContent = ref('')
// 简单格式化逻辑(生产环境建议使用 markdown-it 或 turndown 等库)
const applyFormat = (type) => {
const el = editorRef.value
if (!el || !window.getSelection) return
const selection = window.getSelection()
if (selection.rangeCount === 0 || !selection.getRangeAt(0).toString().trim()) return
const range = selection.getRangeAt(0)
const text = range.toString()
let wrapped = text
switch (type) {
case 'bold': wrapped = `<strong>${text}</strong>`; break
case 'italic': wrapped = `<em>${text}</em>`; break
case 'h2': wrapped = `<h2>${text}</h2>`; break
case 'ul': wrapped = `<ul><li>${text}</li></ul>`; break
}
range.deleteContents()
range.insertNode(document.createTextNode(wrapped))
onInput() // 同步更新 rawContent
}
const onInput = () => {
rawContent.value = editorRef.value.innerHTML
// 这里可加入 HTML 清洗(如仅允许 <strong><em><h2><ul><li>)
formattedContent.value = rawContent.value
}
const handleKeydown = (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault()
document.execCommand('insertLineBreak') // 保留换行语义
}
}
// 初始化内容
rawContent.value = '<p>欢迎使用 Vuetify WYSIWYG 编辑器!</p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/ai/1176" title="AI Room Planner"><img
src="https://img.php.cn/upload/ai_manual/001/431/639/68b7b00a91a23666.png" alt="AI Room Planner" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/ai/1176" title="AI Room Planner">AI Room Planner</a>
<p>AI 室内设计工具,免费为您的房间提供上百种设计方案</p>
</div>
<a href="/ai/1176" title="AI Room Planner" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div>'
formattedContent.value = rawContent.value
</script>
<style scoped>
.border { border: 1px solid rgba(0,0,0,0.12); }
</style>✅ 优势说明:该方案完全基于 Vuetify 布局能力与 Vue 响应式系统,无第三方依赖;工具栏按钮状态由 v-model 自动同步;内容变更即时反映至预览区,符合 WYSIWYG 设计哲学。
注意事项与进阶建议
-
安全性第一:v-html 渲染用户输入内容存在 XSS 风险。生产环境务必引入 DOMPurify 对 innerHTML 进行白名单过滤,例如:
import DOMPurify from 'dompurify' formattedContent.value = DOMPurify.sanitize(rawContent.value, { ALLOWED_TAGS: ['strong', 'em', 'h2', 'ul', 'li', 'p'], ALLOWED_ATTR: [] }) -
体验优化点:
- 添加撤销/重做栈(监听 input + history.pushState 或自维护操作栈);
- 支持图片上传拖拽(集成 v-file-input 与 v-img);
- 使用 v-menu 实现下拉色板、字体选择等高级控件。
挑战延伸(进阶学习):若想深入理解编辑器底层机制,建议尝试脱离 Vuetify,自行封装 contenteditable 组件,手动处理光标位置、跨浏览器兼容性、格式继承与撤销逻辑——这将极大提升对 Vue 响应式原理、原生 DOM API 及事件循环的理解。
总之,Vuetify 不是“降低挑战”,而是帮你把精力从 UI 实现转移到架构设计与交互逻辑上。一个好编辑器的核心不在按钮多寡,而在于状态可控、行为可测、扩展可期——而这,正是 Vue + Vuetify 赋予你的坚实起点。









