VUE后台管理中使用富文本编辑器导入word 文档进行编辑
前言
最近因业务需求在项目中嵌入了tinymce这个编辑器,用于满足平台给用户编辑各类新闻内容什么的业务需求,前后也花了不少时间体验和对比了市面上各类开源编辑器,直接将新闻部门的工作从半天工作量提升只要一个小时就可以搞到,一两分钟就可以搞定一个任务
优点
文档好,功能强,bug少,无外部依赖。
word文档粘贴进来要带格式
兼容移动端
word文档粘贴进来要正常显示并且还要兼容移动端
电脑网页里粘贴进来内容要正常显示并且排版还不能乱
电脑网页拷过来的内容还要兼容到移动端
安装
npm install tinymce @tinymce/tinymce-vue@3.2.8 -S
下载语言包
开始操作文件
将依赖包 node_modules 里找到
tinymce
文件夹,复制到public 里,【左图是tinymce,右图是public复制后的目录】
一下是注意点:
很多的博客的写法是将 node_modules 里面的
skins
文件夹复制到public/tinymce
目录下,经过尝试是不完善的,需要将整个目录倒入进public里
不完全导入的错误显示,我还以为是语言包的问题,一个个语言包版本去试,这种展示一半英文一半中文的
导入tinymcejs
public/index.html 添加 tinymce.js
<div id="app"></div>
<script src="/tinymce/tinymce.min.js"></script>
引入基本文件
// 引入组件
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
// 引入富文本编辑器主题的js和css
import 'tinymce/themes/silver/theme.min.js'
import 'tinymce/skins/ui/oxide/skin.min.css'
// 扩展插件
import 'tinymce/plugins/image'
import 'tinymce/plugins/link'
import 'tinymce/plugins/code'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/wordcount'
注册组件
components: { Editor },
使用组件
<div class="activeConfig-container">
<Editor id="tinymce" v-model="tinymceHtml" :init="editorInit" />
</div>
tinymce 初始化配置
data() {
return {
// tinymce的绑定值
tinymceHtml: '',
// tinymce的初始化配置
editorInit: {
selector: '#tinymce',
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: 400,
plugins: 'link lists image code table wordcount importword',
toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | link unlink image code | removeformat | importword',
//点击富文本图片上传的时候将图片转成base64再通过success插入
images_upload_handler: (blobInfo, success) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
success(img)
},
importword_filter: function(result, insert, message) {
// console.log(result)
// console.log(insert)
// console.log(message)
// 自定义操作部分
insert(result) // 回插函数
},
// statusbar: false // 是否隐藏底部的状态栏
// menubar: false, // 是否隐藏最上方的菜单
branding: false // 是否禁用“Powered by TinyMCE”
}
}
},
整体代码
<template>
<div class="activeConfig">
<div class="activeConfig-container">
<Editor id="tinymce" v-model="tinymceHtml" :init="editorInit" />
</div>
</div>
</template>
<script>
// 引入组件
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
// 引入富文本编辑器主题的js和css
import 'tinymce/themes/silver/theme.min.js'
import 'tinymce/skins/ui/oxide/skin.min.css'
// 扩展插件
import 'tinymce/plugins/image'
import 'tinymce/plugins/link'
import 'tinymce/plugins/code'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/wordcount'
// import { uploadImgage } from '@/api/activeConfig'
export default {
name: 'ActiveConfig',
components: { Editor },
data() {
return {
// tinymce的绑定值
tinymceHtml: '',
// tinymce的初始化配置
editorInit: {
selector: '#tinymce',
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: 400,
plugins: 'link lists image code table wordcount importword',
toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | link unlink image code | removeformat | importword',
// 此处为图片上传处理函数
// images_upload_handler: (blobInfo, success, failure) => {
// this.handleImgUpload(blobInfo, success, failure)
// },
images_upload_handler: (blobInfo, success) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
success(img)
},
importword_filter: function(result, insert, message) {
// console.log(result)
// console.log(insert)
// console.log(message)
// 自定义操作部分
insert(result) // 回插函数
},
// statusbar: false // 是否隐藏底部的状态栏
// menubar: false, // 是否隐藏最上方的菜单
branding: false // 是否禁用“Powered by TinyMCE”
}
}
},
mounted() {
tinymce.init({})
},
methods: {
// 图片上传
handleImgUpload(blobInfo, success, failure) {
// this.baseUrl = process.env.VUE_APP_BASE_URL
// const imgBase64 = 'data:image/jpeg;base64,' + blobInfo.base64()
// const data = { img: [imgBase64] }
// uploadImgage(data).then(res => {
// // 传入success回调里的数据就是富文本编辑器里插入图片的src的值
// success(`${this.baseUrl}/${res.data[0]}`)
// }).catch(() => { failure('error') })
}
}
}
</script>
<style lang="scss" scoped>
.activeConfig {
&-container {
margin: 30px;
}
}
</style>
特殊需求
需要找个能实现word文档上传到富文本编辑器,减轻编辑机构的负担,解决机构需要每次打开文档复制再编辑的繁琐工作,实现直接导入编辑
一个下载插件,一个是注意点,图片在富文本是base64的方式
导入插件
下载解压后
将整个文件复制到public的插件目录
再往tinymce配置添加即可
editorInit.plugins 和 editorInit.toolbar 添加importword即可
editorInit: {
selector: '#tinymce',
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: 400,
plugins: 'link lists image code table wordcount importword',
toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | link unlink image code | removeformat | importword'
}
图片解决方案
如果碰到图片的问题,处理图片有两种方式:
1、一种在提交内容的时候统一处理图片,稿件文件里图片只要提交一次。
2、一种是监听图片上传,每次更新图片都上传图片。
图片上传函数有
images_upload_handler
第二种
const files = []
const base64ImgSum = []
for (let i = 0; i < imgs.length; i++) {
//去除不用的属性
imgs[i].removeAttribute('data-mce-src')
imgs[i].removeAttribute('alt')
//拿到所有的图片列表
imgs[i].src.includes('base') && base64ImgSum.push(imgs[i])
}
//将base64图片转换file文件
dataURLToFile(dataURL, filename) {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)[1] // mime类型 image/png
const bstr = atob(arr[1]) // base64 解码
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, { type: mime })
// return new Blob([a8arr], {type: mime});
},
//将处理file文件当参处理即可
fd.append('file', this.dataURLToFile(base64ImgSum[i].src, `${(new Date()).getTime()}.jpg`))
完整代码
<template>
<!-- 稿件文本编辑器 -->
<div class="activeConfig">
<div style="display:flex">
<div class="activeConfig-container">
<Editor id="tinymce" v-model="tinymceHtml" :init="editorInit" />
</div>
<div style="width:98px;margin: 0 10px;text-align:center">
<div><el-button :disabled="!tinymceHtml" @click="openPreview">预览页面</el-button></div>
<div><el-button :disabled="!tinymceHtml" style="margin:10px 0" @click="deleteBlankLine">删除空行</el-button></div>
<!-- <div style="margin:10px 0"><el-button @click="getImgList" :disabled="!tinymceHtml">上传图片</el-button></div> -->
</div>
</div>
<previewView ref="previewId" :content="tinymceHtml" />
</div>
</template>
<script>
// 引入组件
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
// 引入富文本编辑器主题的js和css
import 'tinymce/themes/silver/theme.min.js'
import 'tinymce/skins/ui/oxide/skin.min.css'
// 扩展插件
import 'tinymce/plugins/image'
import 'tinymce/plugins/link'
import 'tinymce/plugins/code'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/wordcount'
import {
uploadBatch
} from '@/api/myTask'
import previewView from './preview'
export default {
name: 'ActiveConfig',
components: { Editor, previewView },
props: {
taskId: {
type: Number,
required: true,
default: 1
},
authorize: {
type: Object,
required: true
}
},
data() {
return {
// tinymce的绑定值
tinymceHtml: '',
// tinymce的初始化配置
editorInit: {
selector: '#tinymce',
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: 400,
plugins: 'image importword wordcount fullscreen',
toolbar: 'image | importword | fullscreen',
menubar: false,
content_style: '.tox-tinymce-aux {z-index: 2600 !important;}',
// 此处为图片上传处理函数
// images_upload_handler: (blobInfo, success, failure) => {
// this.handleImgUpload(blobInfo, success, failure)
// },
images_upload_handler: (blobInfo, success) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
// // console.log(1111)
// setTimeout(() => {
// // success('https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF')
// }, 3000)
success(img)
},
importword_filter: (result, insert, message) => {
// console.log(result)
// console.log(insert)
// console.log(message)
// 自定义操作部分
setTimeout(() => {
const trTileList = document.getElementById('tinymce_ifr').contentWindow.document.body.getElementsByTagName('table').item(0).getElementsByTagName('tr')
console.log(trTileList, trTileList.length)
if (trTileList.length < 4) {
console.log('表格数据格式错误')
} else {
console.log(document.getElementById('tinymce_ifr').contentWindow.document.body.getElementsByTagName('table').item(0).getElementsByTagName('tr'))
const titleList = {}
for (const index in trTileList) {
console.log(index, typeof index)
if (index === '0') titleList.bigTitleA = trTileList[index].innerText
if (index === '1') titleList.bigTitleB = trTileList[index].innerText
if (index === '2') titleList.bigTitleC = trTileList[index].innerText
if (index === '3') titleList.shortTitle = trTileList[index].innerText
}
console.log(trTileList[0].innerText)
console.log(titleList)
this.$emit('updateTitle', titleList)
document.getElementById('tinymce_ifr').contentWindow.document.body.getElementsByTagName('table').item(0).remove()
}
}, 1000)
insert(result) // 回插函数
},
// statusbar: false // 是否隐藏底部的状态栏
// menubar: false, // 是否隐藏最上方的菜单
branding: false // 是否禁用“Powered by TinyMCE”
}
}
},
mounted() {
tinymce.init({})
},
methods: {
// 图片上传
handleImgUpload(blobInfo, success, failure) {
// this.baseUrl = process.env.VUE_APP_BASE_URL
// const imgBase64 = 'data:image/jpeg;base64,' + blobInfo.base64()
// const data = { img: [imgBase64] }
// uploadImgage(data).then(res => {
// // 传入success回调里的数据就是富文本编辑器里插入图片的src的值
// success(`${this.baseUrl}/${res.data[0]}`)
// }).catch(() => { failure('error') })
},
dataURLToFile(dataURL, filename) {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)[1] // mime类型 image/png
const bstr = atob(arr[1]) // base64 解码
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, { type: mime })
// return new Blob([a8arr], {type: mime});
},
getImgList() {
const imgs = document.getElementById('tinymce_ifr').contentWindow.document.getElementsByTagName('img')
// console.log(imgs)
const files = []
const base64ImgSum = []
for (let i = 0; i < imgs.length; i++) {
imgs[i].removeAttribute('data-mce-src')
imgs[i].removeAttribute('alt')
// console.log(imgs[i].src.includes('base'))
imgs[i].src.includes('base') && base64ImgSum.push(imgs[i])
}
if (base64ImgSum.length) {
for (let i = 0; i < base64ImgSum.length; i++) {
// const blob = this.dataURLToFile(base64ImgSum[i].src, 'Whatever.jpg')
// console.log(blob)
const fd = new FormData()
fd.append('file', this.dataURLToFile(base64ImgSum[i].src, `${(new Date()).getTime()}.jpg`))
fd.append('id', this.taskId)
uploadBatch(fd, this.authorize).then(res => {
// console.log(res)
files.push(res.msg[0].url)
base64ImgSum[i].src = res.msg[0].url
if (files.length === base64ImgSum.length) {
// console.log('图片上传成功')
this.$emit('updateEidt')
}
})
}
} else {
this.$emit('updateEidt')
}
},
// 删除空行
deleteBlankLine() {
const brLength = document.getElementById('tinymce_ifr').contentWindow.document.getElementsByTagName('br')
console.log(brLength.length)
while (brLength.length) {
console.log(brLength[brLength.length - 1].parentNode.children.length)
if (brLength[brLength.length - 1].parentNode.children.length === 1) {
brLength[brLength.length - 1].parentNode.remove()
} else if (brLength[brLength.length - 1].parentNode.children.length > 1) {
brLength[brLength.length - 1].remove()
}
}
this.tinymceHtml = document.getElementById('tinymce_ifr').contentWindow.document.body.innerHTML
},
openPreview() {
this.$refs.previewId.dialogVisible = true
}
}
}
</script>
<style lang="scss" scoped>
.activeConfig-container{
width: 100%;
}
.tox-silver-sink,.tox-tinymce-aux{
z-index: 2600 !important;
}
</style>