wangeditor富文本编辑和vue3
官网:
wangEditor https://www.wangeditor.com/v5/
为啥用这个富文本编辑器(我觉得官网写自己优势已经非常好了没有啥可补充的了)
文档特别的全和友好
安装
yarn add @wangeditor/editor # 或者 npm install @wangeditor/editor --save vue3 yarn add @wangeditor/editor-for-vue@next # 或者 npm install @wangeditor/editor-for-vue@next --save vue2 yarn add @wangeditor/editor-for-vue # 或者 npm install @wangeditor/editor-for-vue --save 安装 React 组件(可选) yarn add @wangeditor/editor-for-react # 或者 npm install @wangeditor/editor-for-react --save cdn <!-- 引入 css --> <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet"> <!-- 引入 js --> <script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script> <script> var E = window.wangEditor; // 全局变量 </script>
效果样式:
特别说明我的修改文章和发布文章用的是同一个路由地址和组件就只改改接口和一些文字
定义一个子组件专门放编辑器的
<template> <div style="margin-top:10px;box-shadow: rgb(0 0 0 / 10%) 0px 2px 12px 0px;">
// toobar 这个是就是头部那些编辑器 editor 是下面那个编辑器实例 defaultConfig是配置编辑器的 <Toolbar style="border-bottom: 1px solid #efefef" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
// v-model 获取到的值就是文本编辑器内的值 defaultConfig 是配置编辑器的全局配置 <Editor style="height: calc(100vh - 280px); overflow-y: hidden;" v-model="valueHtml" :defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" /> </div> </template> <script setup lang="ts"> import '@wangeditor/editor/dist/css/style.css' import { SlateElement } from '@wangeditor/editor'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { onBeforeUnmount, ref, shallowRef, inject, watch, nextTick } from 'vue'
// 这个是我自己定义的两个服务器接口函数 一个上传图片的 一个删除图片的 import { postArticleImage, deleteArticleImages } from '@/utils/articleHttp' import { ElMessage } from 'element-plus'; import { useRoute } from 'vue-router' const route = useRoute() // 定义插入成功后的图片类型 type ImageElement = SlateElement & { src: string alt: string url: string href: string } // 编辑器实例,必须用 shallowRef const editorRef = shallowRef() // 全局状态 这个是自己定义的组件内传递数据的 类似于vuex const store: any = inject('store') // 内容 HTML const valueHtml = ref<string>('') // 插入的图片 const InsertionImages: ImageElement[] = [] // 背景图片 const bg = ref<string>('') // 传入的文本 默认值没有 const props = withDefaults(defineProps<{ updateHtml: string }>(), { updateHtml: '' }) // 因为我要靠queryid 去发请求请求要修改的数据 所以有id 才会执行下面逻辑 route.query.id && watch(() => props.updateHtml, () => { // 监视传入的值的改变 valueHtml.value = props.updateHtml // 给编辑器赋值初始化 nextTick(() => { // 要在数据回来dom 上树之后才能获取编辑器上的图片 InsertionImages.push(...editorRef.value.getElemsByType('image')) //把编辑器上初始的图片放入一个数组里面方便提交的时候对比 }) }, { immediate: true // 上来就监视 }) // 上传图片 type InsertFnType = (url: string, alt: string, href: string) => void // 定义类型 const updatedImage = async (file: File, insertFn: InsertFnType) => {
// 因为我要上传到服务器的图片是multipart /form-data格式必须要放入到formdata 里面
const fd = new FormData() fd.append('file', file) // 服务器定义的文件名字 postArticleImage(fd).then(res => { // 这个是一个上传服务器方法 这个根据自己的实际情况定义 if (res.data.ok) { // 这个是我自己写发服务器所以就拿ok 来判断是否成功了 insertFn(store.state.BaseUrl + res.data.imageUrl, '服务错误', '') // 这个是调用的回调函数来插入图片到编辑当中 三个参数 地址 alt href } else ElMessage.error(res.data.info) // 否者返回错误 }) } // 下面是插入图片的回调 官网给的的钩子函数 const onInsertedImage = (imageNode: ImageElement | null) => { if (imageNode == null) return // 放入图片到数组 方便发送的时候对比 InsertionImages.push(imageNode) } const toolbarConfig = {
// 这个是工具栏上的小工具的全局配置 excludeKeys: [ // 把上传视频的剔除掉 因为我的服务器实在也是垃圾接收视频需要占用大量的资源 'group-video'] } const editorConfig = { // 这个是编辑器的全局配置 placeholder: '请输入内容...', // 情况的时候显示的提示 MENU_CONF: { // 配置上传图片 uploadImage: { // 小于该值就插入 base64 格式(而不上传),默认为 0 base64LimitSize: 10 * 1024, // 10kb customUpload: updatedImage // 自定义上传文件的方法 在上面 }, insertImage: { onInsertedImage: onInsertedImage // 插入图片后成功的回调 } } } // 组件销毁时,也及时销毁编辑器 onBeforeUnmount(() => { const editor = editorRef.value if (editor == null) return editor.destroy() // 销毁 }) const handleCreated = (editor: any) => { editorRef.value = editor // 记录 editor 实例,重要! } // 删除服务器 图片的方法 const deletImage = (): void => { // 最后的图片 const images = (editorRef.value.getElemsByType('image') as ImageElement[]).map(item => item.src) const deletImages = InsertionImages.filter(item => !images.includes(item.src)) //筛选出来要删除的图片 就是上传时候的图片和插入过的图片对比多出来的就是 // 调用删除的函数 deleteArticleImages(deletImages.map(item => item.src)) images[0] && (bg.value = images[0].split(':3999/')[1]) // 这个是我选中如果文本上传图片的话就用第一张当文字背景 } // 清空编辑器里的东西 const clearHtml = () => { InsertionImages.splice(0) //清空插入过的图片数组 } // 把外面用的东西暴露了出去方便外面使用 defineExpose({ deletImage, valueHtml, bg, clearHtml, InsertionImages }) </script> <style lang="scss"> .w-e-bar-item { padding: 1px; } </style>
定义父组件来引用富文本编辑器 (因为我的element 组件是cdn引入所以不用按需引入就能使用看你们个人情况)
<template> <el-card class="box-card"> <h2 class="titleHeader">{{ route.query.id ? '修改文章' : '发布文章' }} // 判断是否是修改文章的 <div class="headerRight">
// 这个是文章的类型 <el-select v-model="lableNameData" class="m-2" placeholder="默认草稿"> <el-option v-for="item in store.state.lableData" :key="item._id" :label="item.labelName" :value="item.labelName" /> </el-select> <SendBtn @click="handleSend">send</SendBtn> // 这个是自己自定义的一个小按钮 </div> </h2> <el-input v-model="articleTitle" placeholder="请输入文章标题" clearable /> <TinymceEditor ref="Editor" :updateHtml="updateHtml"></TinymceEditor> // 引入文本编辑器组件传入值 </el-card> </template> <script lang="ts" setup> import { ref, onBeforeUnmount, inject, defineAsyncComponent } from 'vue' import { useRoute, useRouter } from 'vue-router' import dayjs from 'dayjs' // 这个是专门处理事件的比momentjs要小一些 import { ElMessage } from 'element-plus'; import { postArticle, getArticleSingle, updateArticle } from '@/utils/articleHttp' // 自己定义的上传文章获取文章内容 更新文章的方法 const TinymceEditor = defineAsyncComponent(() => import('@/components/article/TinymceEditor.vue')) // 异步导入文本组件 为了打包的时候分包可以在加载到这个的时候在加载资源 const SendBtn = defineAsyncComponent(() => import('@/components/button/SendBtn.vue')) // 这个单词的为了和上面的统一哈哈哈 // 传入的要更新的html const updateHtml = ref('') // 全局状态 这个是自己写的类使用vuex const store: any = inject('store') // 分类标签 store.GetLabelData()// 获取分类标签 有就return了 为了减轻服务器压力少发请求 // 编辑框 const Editor = ref() // 文章标题 const articleTitle = ref<string>('') articleTitle.value = dayjs(new Date()).format('YYYY-MM-DD HH:mm') // 标题上默认给个时间作为标题单纯为了好看 // 引入路由 const route = useRoute() const router = useRouter() const labelNameData = ref<string>('') // 文章标签的数据 // 是否是更新的文章 const updateArticleFlag: Boolean = !!route.query.id // 原始的标签 let oldlabelName: string; // 更新的就有id 先存起来不能用路由跳转的时候就没了 let updateArticleId = route.query.id as string | undefined // 如果有query id就发请求 const getUpdateData = () => { getArticleSingle(updateArticleId).then(res => { // 发请求自己定义的获取 要修改文章的内容 articleTitle.value = res.data.info[0].title // 初始化修改文章标题 lableNameData.value = res.data.info[0].articleType //初始化修改文章的类型 oldlabelName = res.data.info[0].articleType // 传给oldLableName updateHtml.value = res.data.info[0].info // 初始化修改文章的内容 }) } // 判断是否是传入的是更新的文章 就执行后面函数 updateArticleFlag && getUpdateData() // 提交 const handleSend = async () => { if (!articleTitle.value.trim()) return ElMessage.error('标题不能为空') const labelName = labelNameData.value || '草稿'; // 如果不选类型就当草稿发送 (Editor.value as { deletImage: () => void }).deletImage();// 触发删除图片函数就是定义在子组件的 let res; if (updateArticleFlag) { // 判断是否是修改的文章 // 修改的逻辑 res = await updateArticle(updateArticleId, articleTitle.value, Editor.value.valueHtml, labelName, Editor.value.bg) // 上传修改的内容 // 删除旧的labelName的文章数量 为啥删除因为方便下面还要添加 如果改了这个值就不需要判断直接能加1 了 store.subtractLabelArticle(oldlabelName) // 因为为了少发请求 lablename值改变了就会改变全局的label数据 因为这个数据在多个地方都用到了 } else { // 提交的逻辑 res = await postArticle(articleTitle.value, Editor.value.valueHtml, Editor.value.bg, labelName) } // 对应label的文章数量加1 store.addLableArticle(labelName) res.data.ok ? ElMessage.success(res.data.info) : ElMessage.error(res.data.info) // 服务器返回数据 提示一下 // 清空那个数组以防多次触发保存 因为我要在页面刷新的时候和切换组件的时候去执行提交函数 主要为了防止上传完图片而切换走了服务器的图片就会堆积删除不掉了 Editor.value && Editor.value.clearHtml() router.replace('/home/article/article-list') // 跳转到文章列表页 } // 当组件销毁的时候的钩子函数 onBeforeUnmount(() => { window.onbeforeunload = null // 把事件销毁 Editor.value.InsertionImages.length && handleSend() // 当没按提交按钮的时候切换组件导致 上传图片没有显示照成图片在服务器上堆积 }) // 当页面刷新的时候钩子函数 window.onbeforeunload = () => { Editor.value.InsertionImages.length && handleSend() // 刷新页面的时候也是一样的 } </script> <style lang="scss" scoped> .titleHeader { display: flex; justify-content: space-between; margin-bottom: 10px; } .m-2 { width: 100px; } .headerRight { display: flex; } </style>
真的是写代码十分钟打注释半个小时 ^_^