阅读量:0
安装
wangeditor5
官网:https://www.wangeditor.com/v5/
yarn add @wangeditor/editor # 或者 npm install @wangeditor/editor --save yarn add @wangeditor/editor-for-vue # 或者 npm install @wangeditor/editor-for-vue --save
mammoth.js
官网:https://github.com/mwilliamson/mammoth.js
npm install mammoth
若出现依赖包下载失败的情况,可能是镜像问题,可选择使用国内镜像,参考文档:https://blog.csdn.net/hyk521/article/details/140706064
使用
editor.vue:
<template> <div style="border: 1px solid #ccc;"> <input type="file" id="weWordBtn" style="display:none;" accept="application/vnd.openxmlformats-officedocument.wordprocessingml.document"/> <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" /> <Editor :style="editorStyle" v-model="html" :defaultConfig="editorConfig" :mode="mode" @onCreated="onCreated" @onChange="onChange" @customPaste="customPaste" /> </div> </template> <script> import Vue from 'vue'; import {Boot, DomEditor} from '@wangeditor/editor'; import {Editor, Toolbar} from '@wangeditor/editor-for-vue'; import '@wangeditor/editor/dist/css/style.css'; import {uploadPic} from "@/api/fileUpload/upload"; import mammoth from "mammoth"; import {Loading} from "element-ui"; //自定义新菜单 class wordImportMenu { constructor() { this.title = 'word导入'; this.iconSvg = '<svg t="1721893685983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12124" width="16" height="16"><path d="M563.2 1006.933333s-3.413333 0 0 0l-549.546667-102.4c-6.826667-3.413333-13.653333-10.24-13.653333-17.066666V170.666667c0-6.826667 6.826667-13.653333 13.653333-17.066667l546.133334-136.533333c3.413333 0 10.24 0 13.653333 3.413333s6.826667 6.826667 6.826667 13.653333v955.733334c0 3.413333-3.413333 10.24-6.826667 13.653333-3.413333 3.413333-6.826667 3.413333-10.24 3.413333zM34.133333 873.813333l512 95.573334V54.613333L34.133333 184.32v689.493333z" fill="" p-id="12125"></path><path d="M1006.933333 938.666667h-443.733333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667H989.866667v-785.066666H563.2c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667h443.733333c10.24 0 17.066667 6.826667 17.066667 17.066667v819.2c0 10.24-6.826667 17.066667-17.066667 17.066667zM358.4 699.733333c-6.826667 0-13.653333-6.826667-17.066667-13.653333l-68.266666-249.173333-68.266667 249.173333c-3.413333 6.826667-6.826667 13.653333-17.066667 13.653333-6.826667 0-13.653333-3.413333-17.066666-10.24l-102.4-307.2c-3.413333-10.24 3.413333-17.066667 10.24-20.48 10.24-3.413333 17.066667 3.413333 20.48 10.24l85.333333 252.586667 71.68-252.586667c3.413333-13.653333 27.306667-13.653333 34.133333 0l71.68 252.586667 85.333334-252.586667c3.413333-10.24 13.653333-13.653333 20.48-10.24 10.24 3.413333 13.653333 13.653333 10.24 20.48l-102.4 307.2c-3.413333 6.826667-10.24 10.24-17.066667 10.24z" fill="" p-id="12126"></path><path d="M904.533333 256h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066666h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066666s-6.826667 17.066667-17.066667 17.066667zM904.533333 392.533333h-334.506666c-10.24 0-17.066667-6.826667-17.066667-17.066666s6.826667-17.066667 17.066667-17.066667h334.506666c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066666zM904.533333 529.066667h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066667h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066667zM904.533333 665.6h-341.333333c-10.24 0-17.066667-6.826667-17.066667-17.066667s6.826667-17.066667 17.066667-17.066666h341.333333c10.24 0 17.066667 6.826667 17.066667 17.066666s-6.826667 17.066667-17.066667 17.066667zM904.533333 802.133333H580.266667c-10.24 0-17.066667-6.826667-17.066667-17.066666s6.826667-17.066667 17.066667-17.066667h324.266666c10.24 0 17.066667 6.826667 17.066667 17.066667s-6.826667 17.066667-17.066667 17.066666z" fill="" p-id="12127"></path></svg>'; this.tag = 'button'; } //菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false isActive(editor) { return false; } //获取菜单执行时的 value,用不到则返回空字符串或 false getValue(editor) { return ''; } //菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false isDisabled(editor) { return false; // or true } //点击菜单时触发的函数 exec(editor, value) { document.getElementById('weWordBtn').click(); } } const wordImportConf = { key: 'wordImport', factory() { return new wordImportMenu(); } }; Boot.registerMenu(wordImportConf); export default Vue.extend({ components: {Editor, Toolbar}, props: { /* 编辑器的内容 */ value: { type: String, default: "", }, /* 高度 */ height: { type: Number, default: 500, }, /* 是否只读 */ readOnly: { type: Boolean, default: false }, /* 编辑器内提示语 */ placeholder: { type: String, default: '请输入内容...' } }, data() { return { editor: null, html: '', toolbarConfig: { modalAppendToBody: false, toolbarKeys: ['headerSelect', 'blockquote', '|', 'bold', 'underline', 'italic', 'through', 'code', 'sup', 'sub', 'clearStyle', '|', 'color', 'bgColor', 'fontSize', 'lineHeight', '|', 'bulletedList', 'numberedList', 'todo', { 'key': 'group-justify', 'title': '对齐', 'iconSvg': '<svg viewBox=\"0 0 1024 1024\"><path d=\"M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z\"></path></svg>', 'menuKeys': ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'] }, { 'key': 'group-indent', 'title': '缩进', 'iconSvg': '<svg viewBox=\"0 0 1024 1024\"><path d=\"M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z\"></path></svg>', 'menuKeys': ['indent', 'delIndent'] }, '|', 'insertLink', 'uploadImage', 'insertTable', 'codeBlock', 'divider', '|', 'undo', 'redo', '|', '|', 'fullScreen' ], // excludeKeys: ['fontFamily', 'emotion', 'group-video'] insertKeys: { index: 32, keys: ['wordImport'] } }, editorConfig: { placeholder: this.placeholder, readOnly: this.readOnly, autoFocus: true, MENU_CONF: { 'uploadImage': { timeout: 300000, fieldName: 'files', maxNumberOfFiles: 10, allowedFileTypes: ['image/jpeg', 'image/png'], // allowedFileTypes: ['image/*'], maxFileSize: 1024 * 1024 * 5, server: process.env.VUE_APP_BASE_API + '/system/fileStorage/uploadPic', onError: (e, t, n) => { this.$message.error('图片上传失败:' + t); }, onFailed: (e, t) => { this.$message.error('图片上传失败:未知错误'); }, onSuccess: (e, t) => { this.$message.success('图片上传成功'); }, customInsert(resp, insertFn) { insertFn(process.env.VUE_APP_BASE_API + resp.data.url, '', ''); } } } }, mode: 'default' } }, computed: { editorStyle() { return 'overflow-y: hidden;height: ' + this.height + 'px;'; } }, watch: { value: { handler(val) { if (val !== this.html) { this.html = val === null ? "" : val; } }, immediate: true, }, readOnly: { handler(flag) { if (this.editor !== null) { if (flag) { this.editor.disable(); } else { this.editor.enable(); } } } } }, methods: { onCreated(editor) { this.editor = Object.seal(editor); console.log('editor.getConfig()', editor.getConfig()) console.log('editor.getAllMenuKeys()', editor.getAllMenuKeys()) console.log('editor.getConfig().hoverbarKeys', editor.getConfig().hoverbarKeys) console.log('editor.getMenuConfig(uploadImage)', editor.getMenuConfig('uploadImage')) }, onChange(editor) { console.log('toolbar.getConfig().toolbarKeys', DomEditor.getToolbar(editor).getConfig().toolbarKeys) console.log('editor.children ', editor.children) this.$emit('onChange', {editor: editor, html: editor.getHtml(), text: editor.getText()}); }, customPaste(editor, event, callback) { console.log('ClipboardEvent 粘贴事件对象', event) // const html = event.clipboardData.getData('text/html') // 获取粘贴的 html // const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本 // const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴) // 自定义插入内容 // editor.insertText('xxx') // 返回 false ,阻止默认粘贴行为 // event.preventDefault() // callback(false) // 返回值(注意,vue 事件的返回值,不能用 return) // 返回 true ,继续默认的粘贴行为 // callback(true) }, base64ToBlob(imageType, imageBuffer) { let byteCharacters = atob(imageBuffer); let byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } let byteArray = new Uint8Array(byteNumbers); let blob = new Blob([byteArray], {type: imageType}); let imageName = 'e' + new Date().getTime(); return new File([blob], imageName, {type: imageType}); } }, mounted() { document.getElementById("weWordBtn").addEventListener("change", (event) => { let requestLoading = Loading.service({ fullscreen: true, text: 'word解析中......', spinner: 'el-icon-loading', background: 'rgba(217,217,217,0.2)' }); let editorObj = this.editor; let _this = this; if (event.target.files && event.target.files.length > 0) { let file = event.target.files[0]; mammoth.convertToHtml({arrayBuffer: file.arrayBuffer()}, { ignoreEmptyParagraphs: true, transformDocument: mammoth.transforms.paragraph((element) => { console.log('element', element) if (element.styleName === null) { if (element.children && element.children.length > 0) { for (let i = 0; i < element.children.length; i++) { let secondChild = element.children[i]; if (secondChild.type === 'hyperlink') { secondChild.targetFrame = '_blank'; } else if (secondChild.type === 'run') { if (secondChild.children && secondChild.children.length > 0) { if (i === 0 && secondChild.children[0].type === 'text') { let originVal = secondChild.children[0].value; secondChild.children[0].value = ' ' + originVal; } if (secondChild.highlight !== null) { secondChild.style = 'background-color: ' + secondChild.highlight + ';'; for (let j = 0; j < secondChild.children.length; j++) { let thirdChild = secondChild.children[j]; thirdChild.style = 'background-color: ' + secondChild.highlight + ';'; } } } } else { } } } } return element; }), styleMap: ["u => u"], convertImage: mammoth.images.imgElement(function (image) { return image.read('base64').then(async (imageBuffer) => { //本地图片上传至服务器 let result = ''; let imgFile = _this.base64ToBlob(image.contentType, imageBuffer); let formData = new FormData(); formData.append('files', imgFile); await uploadPic(formData).then(resp => { if (resp.code === '200') { result = process.env.VUE_APP_BASE_API + resp.data.url; } }).catch(e => { console.error('uploadPic-error : ', e) }); return {src: result} }); }) }).then(function (result) { console.log('result', result) if (result.messages.length > 0) { _this.$message.warning('发生错误:' + result.messages[0].message); } else { if (editorObj !== null) { editorObj.clear(); editorObj.dangerouslyInsertHtml(result.value); } } requestLoading.close(); }).catch(function (error) { console.error(error); requestLoading.close() }); } }); }, beforeDestroy() { if (this.editor !== null) { this.editor.destroy(); } } }); </script> <style scoped> </style>
Test.vue:
<template> <div> <h1 style="text-align: center">editor测试</h1> <div style="width: 80%;margin: 0 auto;"> <editor :value="editorHtml" :height="450" :readOnly="readOnly" @onChange="onChange"/> <div class="test_count"> <span>{{editorCount}} 字</span> </div> </div> <div style="text-align: center;margin-top: 25px;"> <el-button type="primary" @click="control">{{controlText}}</el-button> <el-button type="primary" @click="submit">提交</el-button> </div> </div> </template> <script> import Editor from './editor'; export default { name: "Test", components: {Editor}, data() { return { readOnly: false, controlText: '禁用', editorHtml: '', editorText: '' } }, computed: { editorCount() { return this.editorText.replace(/\s*/g, "").replace(/\n/g, "").length; } }, mounted() { }, methods: { onChange(data) { if (data.html !== this.editorHtml) { this.editorHtml = data.html; this.editorText = data.text; } }, control() { this.readOnly = !this.readOnly; if (this.readOnly) { this.controlText = '启用'; } else { this.controlText = '禁用'; } }, submit() { console.log('editorHtml', this.editorHtml) console.log('editorText', this.editorText) } }, }; </script> <style scoped> .test_count { height: 40px; line-height: 40px; text-align: right; padding-right: 20px; border: 1px solid #ccc; border-top: none; } </style>
页面效果:
word导入问题与解决方案
问题:
mammoth 仅支持简单的样式,对于背景色、颜色字体等高级样式无法支持。
解决方案:
1、修改 mammoth.js 的源码,参考文档:https://blog.csdn.net/Jioho_chen/article/details/124699665
2、前端加一个按钮或触发器,后端 Java 使用 poi 解析 word 内容,具体参考:https://www.cnblogs.com/ismallboy/p/12584761.html
若有其他方法,欢迎留言探讨。