vue3富文本编辑器的二次封装开发-Tinymce
欢迎点击领取 -《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结
*分享一个使用比较久的🪜
简介
1、安装:pnpm add tinymce / pnpm add @tinymce/tinymce-vue ===> Vue3 + tinymce + @tinymce/tinymce-vue
2、功能实现图片上传、基金卡片插入、收益卡片插入、源代码复用、最大长度限制、自定义表情包插入、文本内容输入、预览等功能
代码展示
在components文件下创建TinymceEditor.vue文件作为公共组件
<template><div><Editor ref="EditorRefs" v-model="content" :init="myTinyInit" /><div class="editor_footer"><span v-if="wordlimit"><span>{{ wordLenght }}</span><span> / </span><span>{{ wordlimit.max }}</span> 字符</span></div><el-dialog title="自定义表情包" v-model="dialogVisible" width="45%"><div class="emoji"><div class="emoji-item" v-for="item in 40" :key="item"><img :src="`/src/assets/emoji/${item}.webp`" alt="" @click="chooseEmoji(item)" /></div></div></el-dialog><button @click="handlePreview">预览</button></div>
</template><script lang="ts" setup>
import './wordlimit' // 限制字符文件
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/icons/default/icons'
import 'tinymce/themes/silver'
import 'tinymce/models/dom/model'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/code'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/fullscreen'
import '/public/tinymce/plugins/image/index.js'import { sumLetter } from '@/utils/utilTool'
import { computed, onMounted, reactive, ref, watch } from 'vue'const props = withDefaults(defineProps<{modelValue?: stringplugins?: stringtoolbar?: stringwordlimit?: any}>(),{plugins: 'image code wordcount wordlimit preview', // 默认开启工具库toolbar: 'image emoji fund—icon income-icon code' // 富文本编辑器工具}
)const emit = defineEmits(['input'])const wordLenght = ref<number | string>(0)const content = ref<string>('')const EditorRefs = ref<any>()const dialogVisible = ref<boolean>(false)const myTinyInit = reactive({width: '100%',height: 600, // 默认高度statusbar: false,language_url: '/tinymce/langs/zh_CN.js', // 配置汉化-> 需下载对应汉化包引入language: 'zh_CN', // 语言标识branding: false, // 不显示右下角logoauto_update: false, // 不进行自动更新resize: true, // 可以调整大小menubar: false, // 关闭顶部菜单skin_url: '/tinymce/skins/ui/oxide', // 手动引入CSScontent_css: '/tinymce/skins/content/default/content.css', // 手动引入CSStoolbar_mode: 'wrap',plugins: props?.plugins, // 插件toolbar: props?.toolbar, // 功能按钮wordlimit: props?.wordlimit, // 字数限制image_caption: false,paste_data_images: true,//粘贴图片后,自动上传urlconverter_callback: function (url, node, on_save, name) {return url},images_upload_handler: (blobInfo) =>new Promise((resolve, reject) => {console.log(blobInfo.blob())const formData = new FormData()formData.append('file', blobInfo.blob(), blobInfo.filename())resolve('https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/image-20230512090059968.png')// axios// .post(`/api/backend/upload`, formData, {// headers: {// 'Content-Type': 'multipart/form-data',// Authorization: 'Bearer ' + store.state.user.accessToken,// },// })// .then((res) => {// if (res.data.code === 1) {// resolve(`/image_manipulation${res.data.data.filePath}`)// } else {// ElNotification.warning(res.data.msg)// }// })// .catch((error) => {// reject(error)// })}),setup: (editor) => { // 自定义图标内容及触发点击事件等功能editor.ui.registry.addIcon('fund—icon','<svg t="1696250970925" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24834" width="21" height="21"><path d="M512 133.12c208.91648 0 378.88 169.96352 378.88 378.88s-169.96352 378.88-378.88 378.88-378.88-169.96352-378.88-378.88 169.96352-378.88 378.88-378.88m0-71.68c-248.83712 0-450.56 201.72288-450.56 450.56s201.72288 450.56 450.56 450.56 450.56-201.72288 450.56-450.56-201.72288-450.56-450.56-450.56z" fill="#2c2c2c" p-id="24835"></path><path d="M624.74752 263.6288a35.72224 35.72224 0 0 0-25.344 10.496L512 361.52832 424.59648 274.1248a35.73248 35.73248 0 0 0-25.344-10.496 35.84 35.84 0 0 0-25.344 61.17888L451.07712 401.9712H348.16a35.84 35.84 0 1 0 0 71.68h128v66.56H348.16a35.84 35.84 0 1 0 0 71.68h128v133.12a35.84 35.84 0 1 0 71.68 0v-133.12h128a35.84 35.84 0 1 0 0-71.68h-128v-66.56h128a35.84 35.84 0 1 0 0-71.68h-102.91712l77.16352-77.16352a35.84 35.84 0 0 0-25.33888-61.17888z" fill="#2c2c2c" p-id="24836"></path></svg>')editor.ui.registry.addIcon('income-icon','<svg t="1696250530786" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15004" width="21" height="21"><path d="M920 152v720H104V152h816z m-67.2 67.2H171.2v585.6h681.6V219.2z" fill="#2c2c2c" p-id="15005"></path><path d="M32 152m7 0l946 0q7 0 7 7l0 53.2q0 7-7 7l-946 0q-7 0-7-7l0-53.2q0-7 7-7Z" fill="#2c2c2c" p-id="15006"></path><path d="M450.906 417.788l122.187 122.19 115.4-115.401 47.518 47.517-115.4 115.4-47.517 47.518-122.189-122.19-115.399 115.401-47.517-47.517 162.917-162.918z" fill="#2c2c2c" p-id="15007"></path><path d="M300.8 718.4H368v86.4h-67.2v-86.4z m120-86.4H488v172.8h-67.2V632z m120 48H608v124.8h-67.2V680z m120-67.2H728v192h-67.2v-192z" fill="#2c2c2c" p-id="15008"></path></svg>')editor.ui.registry.addButton('emoji', {icon: 'emoji',tooltip: '自定义表情包',onAction: () => {dialogVisible.value = true}})editor.ui.registry.addButton('fund—icon', {icon: 'fund—icon',tooltip: '基金',onAction: () => {editor.insertContent('Hello')}})editor.ui.registry.addButton('income-icon', {icon: 'income-icon',tooltip: '晒收益',onAction: () => {editor.insertContent('Hello')}})},init_instance_callback: (editor: any) => {editor.on('input', () => getEditorWordLen())}
})const initContent = computed(() => {return props.modelValue
})// 选择自定义表情包
const chooseEmoji = (item) => {const editor = EditorRefs.value.getEditor()const range = editor.selection.getRng()const imgNode = editor.getDoc().createElement('img')imgNode.width = 32imgNode.height = 32imgNode.style = 'vertical-align: bottom;'imgNode.src = `/src/assets/emoji/${item}.webp` // 注意写你的项目相对路径range.insertNode(imgNode)dialogVisible.value = falseeditor.execCommand('seleceAll')editor.selection.getRng().collapse()editor.focus()
}const getEditorWordLen = () => {const content = tinymce.activeEditor.getContent({ format: 'text' })const wordObj = sumLetter(content)wordLenght.value = wordObj?.txt?.length || 0
}const handlePreview = () => {const editor = tinymce.activeEditoreditor.on('preview', (editor) => {console.log(editor)})
}onMounted(() => {tinymce.init({})setTimeout(() => getEditorWordLen(), 800)
})watch(initContent,(newVal) => {content.value = newVal},{ deep: true, immediate: true }
)watch(content,(newVal) => {emit('input', newVal)},{ deep: true }
)
</script><script lang="ts">
export default { name: 'TinymceEditor' }
</script><style scoped lang="scss">
.emoji {display: flex;flex-wrap: wrap;
}.emoji-item {display: flex;justify-content: center;align-items: center;margin-left: 10px;margin-bottom: 8px;cursor: pointer;img {width: 48px;height: 48px;}
}.editor_footer {margin-top: 20px;font-size: 13px;
}
</style>
创建wordlimit.ts文件,作为限制字符的触发条件
import tinymce from 'tinymce/tinymce'
import { ElMessage } from 'element-plus'
import { sumLetter } from '@/utils/utilTool'tinymce.PluginManager.add('wordlimit', function (editor): any {const pluginName = '字数限制'const app = tinymce.util.Tools.resolve('tinymce.util.Delay')const Tools = tinymce.util.Tools.resolve('tinymce.util.Tools')const wordlimit_event = editor.getParam('ax_wordlimit_event', 'SetContent Undo Redo Keyup input paste')const options = editor.getParam('wordlimit', {}, 'object')let close = nullconst toast = function (message) {close && close.close()close = ElMessage.error(message)return}// 默认配置const defaults = {spaces: false, // 是否含空格isInput: false, // 是否在超出后还可以输入maxMessage: '超出最大输入字符数量!',changeCallback: () => {}, // 自定义的回调方法changeMaxCallback: () => {},toast // 提示弹窗}class WordLimit {constructor(editor, options) {options = Tools.extend(defaults, options)let preCount = 0let _wordCount = 0let oldContent = editor.getContent()const WordCount = editor.plugins.wordcounteditor.on(wordlimit_event, function (e) {const content = editor.getContent() || e.content || ''if (!options.spaces) {_wordCount = WordCount.body.getCharacterCount()} else {_wordCount = WordCount.body.getCharacterCountWithoutSpaces()}options.changeCallback({...options,editor,num: _wordCount,content,...sumLetter(content)})if (_wordCount > options.max) {preCount = _wordCountif (options.isInput == !1) {editor.setContent(oldContent)if (!options.spaces) {_wordCount = WordCount.body.getCharacterCount()} else {_wordCount = WordCount.body.getCharacterCountWithoutSpaces()}}editor.getBody().blur()editor.fire('wordlimit', {maxCount: options.max,wordCount: _wordCount,preCount: preCount,isPaste: e.type === 'paste' || e.paste || false})toast('最多只能输入' + options.max + '个字')}oldContent = editor.getContent()})}}const setup = function () {if (!options && !options.max) return falseif (!editor.plugins.wordcount) return toast('请先在tinymce的plugins配置wordlimit之前加入wordcount插件')app.setEditorTimeout(editor,function () {const editDom = editor.getContainer()const wordNum: any = editDom.querySelector('button.tox-statusbar__wordcount')const statusbarpath: any = editDom.querySelector('.tox-statusbar__path')statusbarpath ? statusbarpath.remove() : void nullif (wordNum?.innerText?.indexOf('字符') == -1) wordNum.click()new WordLimit(editor, options)},300)}setup()return {getMetadata: function () {return {name: pluginName}}}
})
使用
<template><div class="post_contaniner"><div style="width: 100%"><TinymceEditor v-model="content" @input="inputContent" :wordlimit="{ max: 300 }" /></div></div>
</template><script lang="ts" setup>
import { ref } from 'vue'const content = ref('Hello World')const inputContent = (newVal) => {console.log(newVal)content.value = newVal
}
</script><style scoped lang="scss">
.post_contaniner {.right {flex: 1;box-shadow: 0 1px 10px 3px #dbdbdb;margin-right: 10px;padding: 10px;box-sizing: border-box;}
}
</style>
相关文章:

vue3富文本编辑器的二次封装开发-Tinymce
欢迎点击领取 -《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结 *分享一个使用比较久的🪜 简介 1、安装:pnpm add tinymce / pnpm add tinymce/tinymce-vue > Vue3 tinymce tinymce/tinymce-vue 2、功能实现图片上传…...

typescript 类型声明文件
typescript 类型声明文件概述 在今天几乎所有的JavaScript应用都会引入许多第三方库来完成任务需求。这些第三方库不管是否是用TS编写的,最终都要编译成JS代码,才能发布给开发者使用。6我们知道是TS提供了类型,才有了代码提示和类型保护等机…...

Hadoop伪分布式环境搭建
什么是Hadoop伪分布式集群? Hadoop 伪分布式集群是一种在单个节点上模拟分布式环境的配置,用于学习、开发和测试 Hadoop 的功能和特性。它提供了一个简化的方式来体验和熟悉 Hadoop 的各个组件,而无需配置和管理一个真正的多节点集群。 在 Ha…...

javaee ssm框架项目添加分页控件
搭建ssm框架项目 参考上一篇博文 添加分页控件 引入依赖 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schema…...

2023年中国非晶纳米晶竞争格局、产业链及行业产量分析[图]
非晶合金又称“液态金属、金属玻璃”,是一种新型软磁合金材料,主要包含铁、硅、硼等元素。其主要制品非晶合金薄带的制造工艺是采用急速冷却技术将合金熔液以每秒106℃的速度急速冷却,形成厚度约0.03mm的非晶合金薄带,物理状态表现…...
在业务开发中遇到的树形结构(部门、区域、职位),递归处理。
文章目录 概要对象结构示例完整示例小结 概要 本文主要记录在树形结构中会遇到的问题, 使用部门结构讲解,main方法进行演示。 1、获取部门树结构 2、根据部门id获取所有下级 3、根据部门id获取上级部门 4、根据部门id获取类似面包屑(总公司…...

张量-算术操作函数
tf.add(x,y,name None)求和函数 示例代码如下: import tensorflow.compat.v1 as tf tf.disable_v2_behavior()x 1 y 2a tf.add(x,y)with tf.Session() as sess:print(sess.run(a)) tf.subtract(x,y,name None)减法函数 示例代码如下: import tensorflow.compat.v1 as …...

虚拟展厅有什么重要意义,了解虚拟展厅在宣传中的应用
引言: 随着科技的不断进步,虚拟展厅已经逐渐成为展览行业的重要一环。虚拟展厅是一种数字化平台,为观众提供了与传统展览完全不同的体验。 一.虚拟展厅的定义 虚拟展厅是一个通过互联网和虚拟现实技术创建的数字展示空间&#x…...
华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python)
华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python) 题目描述 近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1-N),排成一排。 一个月后,有M棵胡杨未能成活。现可补种胡杨K棵,请问如何补种(只能补种,不能新种),可以得到最多的连续胡杨树? 输入…...

Java卷上天,可以转行干什么?
小刚是某名企里的一位有5年经验的高级Java开发工程师,每天沉重的的工作让他疲惫不堪,让他萌生出想换工作的心理,但是转行其他工作他又不清楚该找什么样的工作 因为JAVA 这几年的更新实在是太太太……快了,JAVA 8 都还没用多久&am…...

Pyside6 安装和简单界面开发
Pyside6 安装和简单界面开发 Pyside6介绍Pysied6开发环境搭建Python安装Pysied6安装 Pyside6界面开发简单界面设计界面设计界面编译 编写界面初始化代码软件打包 Pyside6介绍 对于Python的GUI开发来说,Python自带的可视化编程模块的功能较弱,PySide是跨…...

python读取vivo手机截图,将满屏图片文件移动别的路径
问题之初 python读取vivo手机截图, 将满屏图片文件移动别的路径好多这样的图片,占用手机大量的内存,食之无味弃之可惜!那么会复制粘贴👀代码的我们我们今天就把这些图片筛选清理掉。 这段代码 原有逻辑的基础上&…...

【一周安全资讯1007】多项信息安全国家标准10月1日起实施;GitLab发布紧急安全补丁修复高危漏洞
要闻速览 1.以下信息安全国家标准10月1日起实施 2.GitLab发布紧急安全补丁修复高危漏洞 3.主流显卡全中招!GPU.zip侧信道攻击可泄漏敏感数据 4.MOVEit漏洞导致美国900所院校学生信息发生大规模泄露 5.法国太空和国防供应商Exail遭黑客攻击,泄露大量敏感…...
2023年09月个人工作生活总结
本文为 2023 年 9 月工作生活总结。 研发编码 Alpine 容器 某工程部署于alpine镜像,当初看上是因为其体积小,其它微服务,在250MB左右,但那个工程只用50MB。最近发现时间戳转换不正确。对于同一时间字符串转时间戳函数࿰…...

现货白银图表分析的依据
现货白银的行情图表分析其实与股票的差不多,投资者可以结合均线、k线的变化,来分析实时的行情走势。当走势图的均线呈多头排列,即短期、中期、长期均线依次从上到下排列并向右上方运行,且白银价格沿各均线向右上方拉升,…...
python多线程与多进程
多线程与多进程 一, 什么是进程, 什么是线程? 进程: 运行中的程序. 每次我们执行一个程序, 咱们的操作系统对自动的为这个程序准备一些必要的资源(例如, 分配内存, 创建一个能够执行的线程. ) 线程: 程序内, 可以直接被CPU调度的执行过程. 是操作系统能够进行运算调度…...

62从零开始学Java之时间相关的类都有哪些?
作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 我们在开发时,除了数字、数学这样的常用API之外,还有日期时间类,更…...
【Leetcode】买卖股票系列
121. 买卖股票的最佳时机 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔…...

SLAM面试笔记(8) — 计算机视觉面试题
目录 问题1:目标检测的算法分类 问题2:卷积神经网络的组成 问题3:输入层的作用 问题4:卷积层作用 问题5:卷积核类型 问题6:11卷积核作用 问题7:卷积核是否越大越好 问题8:棋…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...