欢迎点击领取 -《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结
*分享一个使用比较久的
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?: string
plugins?: string
toolbar?: string
wordlimit?: 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, // 不显示右下角logo
auto_update: false, // 不进行自动更新
resize: true, // 可以调整大小
menubar: false, // 关闭顶部菜单
skin_url: '/tinymce/skins/ui/oxide', // 手动引入CSS
content_css: '/tinymce/skins/content/default/content.css', // 手动引入CSS
toolbar_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',
''
)
editor.ui.registry.addIcon(
'income-icon',
''
)
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 = 32
imgNode.height = 32
imgNode.style = 'vertical-align: bottom;'
imgNode.src = `/src/assets/emoji/${item}.webp` // 注意写你的项目相对路径
range.insertNode(imgNode)
dialogVisible.value = false
editor.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.activeEditor
editor.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 = null
const 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 = 0
let _wordCount = 0
let oldContent = editor.getContent()
const WordCount = editor.plugins.wordcount
editor.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 = _wordCount
if (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 false
if (!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 null
if (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>