Vue 自定义富文本编辑器 tinymce 支持导入 word 模板

自定义富文本编辑器分为前端项目和后端项目两个部分,首先先说一下前端项目

前端

前端项目地址: https://github.com/haoxiaoyong1014/editor-ui

编辑器名称: tinymce

前端采用的 vue.js

至于Vue 中怎么集成 tinymce 编辑器参考: https://segmentfault.com/a/1190000012791569

其中关键代码在项目中的 index.vue

<template>
<div>
  <Row>
    <Col span="18" offset="3">
      <Card shadow>
        <Upload action="http://localhost:2020/upload/word/template"
                :on-success="handleSuccess">
          <Button icon="ios-cloud-upload-outline">上传模板Button>
        Upload>
        <Form ref="editorModel" :model="editorModel" :rules="editorRules">
          <FormItem prop="content">
            <textarea  class='tinymce-textarea' id="tinymceEditer" style="height: 800px">
            textarea>
          FormItem>
          <FormItem>
            <Button type="primary" @click="handleSubmit('editorModel')">SubmitButton>
            <Button type="ghost" @click="handleReset('editorModel')">ResetButton>
          FormItem>
        Form>
        <Spin fix v-if="spinShow">
          <Icon type="load-c" size=18 class="demo-spin-icon-load">Icon>
          <div>加载组件中...div>
        Spin>
      Card>
    Col>
  Row>
div>
template>
<script>
import tinymce from 'tinymce';
import util from '@/libs/util';
export default {
  name: 'index',
  data () {
    return {
      spinShow: true,
      editorModel: {
        content: 'dfsd'
      },
      content2: 'sdds',
      editorRules: {
        content: [
          {
            type: 'string',
            min: 5,
            message: 'the username size shall be no less than 5 chars ',
            trigger: 'blur'
          }
        ]
      },
      customEditor: null
    };
  },
  methods: {
    handleSuccess(res){
      console.log(res)
      this.customEditor=res.content;
      console.log('haoxy'+this.customEditor)
      tinymce.get('tinymceEditer').setContent(this.customEditor);
      /*this.$nextTick(() => {
        this.customEditor = tinymce.get("tinymceEditer");
      })*/
    },
    init () {
      this.$nextTick(() => {
        let vm = this;
        let height = document.body.offsetHeight - 300;
        tinymce.init({
          selector: '#tinymceEditer',
          branding: false,
          elementpath: false,
          height: height,
          language: 'zh_CN.GB2312',
          menubar: 'edit insert view format table tools',
          plugins: [
            'advlist autolink lists link image charmap print preview hr anchor pagebreak imagetools',
            'searchreplace visualblocks visualchars code fullpage',
            'insertdatetime media nonbreaking save table contextmenu directionality',
            'emoticons paste textcolor colorpicker textpattern imagetools codesample'
          ],
          toolbar1: ' newnote print preview | undo redo | insert | styleselect | forecolor backcolor bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image emoticons media codesample',
          autosave_interval: '20s',
          image_advtab: true,
          table_default_styles: {
            width: '100%',
            height: '100%',
            borderCollapse: 'collapse'
          },
          setup: function (editor) {
            editor.on('init', function (e) {
              vm.spinShow = false;
              if (localStorage.editorContent) {
                tinymce.get('tinymceEditer').setContent(localStorage.editorContent);
              }
            });
            editor.on('keydown', function (e) {
              localStorage.editorContent = tinymce.get('tinymceEditer').getContent();
              vm.editorModel.content = tinymce.get('tinymceEditer').getContent();
            });
          }
        });
        /*this.customEditor = tinymce.get("tinymceEditer");*/
      });
    },


    handleSubmit (name) {
      this.$refs[name].validate((valid) => {
        if (valid) {
          util.post('/html/pdf', this.editorModel).then(res => {
            this.$Message.success('Success!');
          });
        } else {
          this.$Message.error('Fail!');
        }
      });
    },
    handleReset (name) {
      this.$refs[name].resetFields();
    },
  },
  mounted () {
    this.init();
  },
  destroyed () {
    tinymce.get('tinymceEditer').destroy();
  }
}
script>

在原有的编辑器的基础上新增了上传模板功能, 在上传成功之后拿到服务端 返回的 html 数据,将其设置到


这个标签中,所有的编辑器都是这个道理.

上传成功之后:

handleSuccess(res){
      console.log(res)
      this.customEditor=res.content;
      console.log('haoxy'+this.customEditor)
      tinymce.get('tinymceEditer').setContent(this.customEditor);

看下效果图:

点击 submit 我是在后端将其转换成了 pdf 文件(按需求定义)

如果在集成中出现: Uncaught SyntaxError: Unexpected token < 这个错误

解决方法:

在 tinymce.init 中把language : zh_CN.GB2312 去掉

在你需要的地方引入: import '../../../zh_CN'(我是把 zh_CN.js放到了根目录下了,效果是一样的),

如果编辑器的样式还是没有出来,只出来一个编辑框的话 ,就在你的根目录下的 index.html 中引入:

后端

后端(服务端)项目地址: https://github.com/haoxiaoyong1014/editor-service

后端采用: springBoot , POI

这里就不对POI做过多的说明了,贴个官网 https://poi.apache.org/,随意看看。

整体思路:

1,在编辑器原来的基础上增加上传模板按钮

2, 前端上传 word 模板

3, 服务端接收将 word 转换为html 返回前端

4, 前端拿到服务端返回的值,将其放到富文本编辑器中

后端主要是第3步

首先搞清楚下要将doc/docx文档转成html/htm的话要怎么处理,根据POI的文档,我们可以知道,处理doc 格式文件对应的 POI API 为 HWPF、docx 格式为 XWPF。此处参考下这篇好文:http://www.open-open.com/lib/view/open1389594797523.html 在格式转换上说得很清楚。

所以整体就是:根据文档类型,doc我们用HWPF对象处理转换、docx用XWPF对象处理转换。

顺便贴一下这个在线文档 http://poi.apache.org/apidocs/index.html,不得不说看得相当麻烦,特别是XWPF的。

所需依赖

<dependency>
      <groupId>org.apache.poigroupId>
      <artifactId>poiartifactId>
      <version>3.12version>
    dependency>
    
    <dependency>
      <groupId>org.apache.poigroupId>
      <artifactId>poi-scratchpadartifactId>
      <version>3.12version>
    dependency>
    
    <dependency>
      <groupId>fr.opensagres.xdocreportgroupId>
      <artifactId>fr.opensagres.xdocreport.documentartifactId>
      <version>1.0.5version>
    dependency>
    
    <dependency>
      <groupId>fr.opensagres.xdocreportgroupId>
      <artifactId>org.apache.poi.xwpf.converter.xhtmlartifactId>
      <version>1.0.5version>
    dependency>
    
      
      <dependency>
        <groupId>org.apache.commons.iogroupId>
        <artifactId>commonsIOartifactId>
        <version>2.6version>
      dependency>
      
    <dependency>
      <groupId>com.aspose.wordsgroupId>
      <artifactId>aspose-wordsartifactId>
      <version>15.8.0version>
    dependency>

其中 commonsIO 这个依赖不知道为什么下载不下来,我将 jar 放到了我的私服上,在pom.xml 中有体现,这里不做详细说明

一、处理doc。

这个相对简单,网上一查一堆,我的代码也是根据网上的做下自己的优化和逻辑。

因为POI很早前就可以支持doc的处理,所以资料比较多。

思路就是:HWPFDocument对象实例化文件流 -> WordToHtmlConverter对象处理HWPFDocument对象及预处理页面的图片等(主要是图片)

文档说明是:

Converts Word files (95-2007) into HTML files.
This implementation doesn’t create images or links to them. This can be changed by overriding AbstractWordConverter.processImage(Element, boolean, Picture) method.

-> org.w3c.dom.Document对象处理WordToHtmlConverter,生成DOM对象 -> 输出文件。

这里有个好处就是使用到了Document对象,从而解决了编码、文件格式等问题。

这里因为过程简单,直接贴简单demo,看注释即可:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FileUtils;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.converter.PicturesManager;
import org.apache.poi.hwpf.converter.WordToHtmlConverter;
import org.apache.poi.hwpf.usermodel.Picture;
import org.apache.poi.hwpf.usermodel.PictureType;
import org.apache.poi.xwpf.converter.core.FileImageExtractor;
import org.apache.poi.xwpf.converter.core.FileURIResolver;
import org.apache.poi.xwpf.converter.xhtml.XHTMLConverter;
import org.apache.poi.xwpf.converter.xhtml.XHTMLOptions;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFPictureData;
import org.w3c.dom.Document;

public class POIForeViewUtil {

	public void parseDocx2Html() throws Throwable {
		final String path = "/tmp/";
		final String file = "xxxxxxx.doc";
		InputStream input = new FileInputStream(path + file);
		String suffix = file.substring(file.indexOf(".")+1);// //截取文件格式名

		//实例化WordToHtmlConverter,为图片等资源文件做准备
		WordToHtmlConverter wordToHtmlConverter = new WordToHtmlConverter(
				DocumentBuilderFactory.newInstance().newDocumentBuilder()
						.newDocument());
		wordToHtmlConverter.setPicturesManager(new PicturesManager() {
			public String savePicture(byte[] content, PictureType pictureType,
					String suggestedName, float widthInches, float heightInches) {
				return suggestedName;
			}
		});
		if ("doc".equals(suffix.toLowerCase())) {
			// doc
			HWPFDocument wordDocument = new HWPFDocument(input);
			wordToHtmlConverter.processDocument(wordDocument);
			//处理图片,会在同目录下生成并保存图片
			List pics = wordDocument.getPicturesTable().getAllPictures();
			if (pics != null) {
				for (int i = 0; i < pics.size(); i++) {
					Picture pic = (Picture) pics.get(i);
					try {
						pic.writeImageContent(new FileOutputStream(path
								+ pic.suggestFullFileName()));
					} catch (FileNotFoundException e) {
						e.printStackTrace();
					}
				}
			}
		} 

		// 转换
		Document htmlDocument = wordToHtmlConverter.getDocument();
		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		DOMSource domSource = new DOMSource(htmlDocument);
		StreamResult streamResult = new StreamResult(outStream);
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer serializer = tf.newTransformer();
		serializer.setOutputProperty(OutputKeys.ENCODING, "utf-8");//编码格式
		serializer.setOutputProperty(OutputKeys.INDENT, "yes");//是否用空白分割
		serializer.setOutputProperty(OutputKeys.METHOD, "html");//输出类型
		serializer.transform(domSource, streamResult);
		outStream.close();
		String content = new String(outStream.toByteArray());
		 //我此时不想让它生成文件,所以我注释掉了,按需求定
        /*FileUtils.writeStringToFile(new File(path, "interface.html"), content,
                "utf-8");*/
	}

	public static void main(String[] args) throws Throwable {
		new POIForeViewUtil().parseDocx2Html();
	}
}

其中 content 就是我们想要的 HTML 数据

接下来我看第二中 docx

二、处理docx。

docx是07的版本,处理起来困难的多,貌似POI对docx的处理方法没有doc那么便捷,处理样式等等都有问题,我遇到的两个最明显问题就是字体编码问题和表格的边框线显示。

思路:XWPFDocument加载文件流 -> XHTMLOptions处理页面资源(主要图片) -> OutputStream输出流直接输出文件。

过程代码相当简单,可是越简单结果约没有预期的好。输出的文件字体编码默认为GBK,例如我的“微软雅黑”字体就变成“寰蒋闆呴粦”,而且节点的显示也没有doc处理的好。

同样贴一下demo代码:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;

import org.apache.poi.xwpf.converter.core.FileImageExtractor;
import org.apache.poi.xwpf.converter.core.FileURIResolver;
import org.apache.poi.xwpf.converter.xhtml.XHTMLConverter;
import org.apache.poi.xwpf.converter.xhtml.XHTMLOptions;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFPictureData;

public class Word07ToHtml {

	public static void parseToHtml() throws IOException {
		File f = new File("tmp/xxxxx.docx");
		if (!f.exists()) {
			System.out.println("Sorry File does not Exists!");
		} else {
			if (f.getName().endsWith(".docx") || f.getName().endsWith(".DOCX")) {
				
				// 1) 加载XWPFDocument及文件
				InputStream in = new FileInputStream(f);
				XWPFDocument document = new XWPFDocument(in);

				// 2) 实例化XHTML内容(这里将会把图片等文件放到生成的"word/media"目录)
				File imageFolderFile = new File("f:/opt");
				XHTMLOptions options = XHTMLOptions.create().URIResolver(
						new FileURIResolver(imageFolderFile));
				options.setExtractor(new FileImageExtractor(imageFolderFile));
				//options.setIgnoreStylesIfUnused(false);
				//options.setFragment(true);
				
			// 3) 将XWPFDocument转成XHTML并生成文件  --> 我此时不想让它生成文件,所以我注释掉了,按需求定
            /*OutputStream out = new FileOutputStream(new File(
                    path, "result.html"));
            XHTMLConverter.getInstance().convert(document, out, null);*/
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            XHTMLConverter.getInstance().convert(document, baos, options);
            String content = baos.toString();
            baos.close();
			} else {
				System.out.println("Enter only MS Office 2007+ files");
			}
		}
	}
}

其中 content 就是我们想要的 HTML 数据

点击 submit 我是在后端将其转换成了 pdf 文件(按需求定义)

POI的jar包下载路径:https://archive.apache.org/dist/poi/release/bin/poi-bin-3.9-20121203.zip

至此 富文本编辑器增加导入 word 模板就结束了, 无论是导入文件还导入图片都是一个道理.

注: 前端项目使用方式

git clone https://github.com/haoxiaoyong1014/editor-ui.git

进入项目执行:

npm install

npm run dev

前提: 需要安装 npm

前端项目地址: https://github.com/haoxiaoyong1014/editor-ui

后端项目地址: https://github.com/haoxiaoyong1014/editor-service

如果对您有帮助还请给个星星哦!

2018/10/19更新,更新内容修复 bug

放到项目中遇到的问题修复

  • 问题描述1:

当上传模板之后点击浏览器刷新编辑框中的内容会变为之前上传的内容

  • 解决方法:

 if (localStorage.editorContent) {
                tinymce.get('tinymceEditer').setContent(localStorage.editorContent);
              }
              

将这段代码注释掉即可,因为编辑器会自动的将内容保存到本地,当你去点击浏览器刷新的时候他会去本地取出并赋值到编辑框中

  • 问题描述2:

当你在编辑框中进行编辑的时候tinymce编辑器监听了键盘按下的事件,但是键盘按下的前一个字符没有保存,例如:

你在编辑框中输入4个字符 aaaa 你再点击submit生成pdf文件,但是 pdf文件中就只有3个字符aaa

  • 解决方法:

因为编辑器只监听了keydown事件,并没有去监听keyup事件
所以加上如下代码即可

editor.on('keyup', function (e) {
              localStorage.editorContent = tinymce.get('tinymceEditer').getContent();
              vm.editorModel.content = tinymce.get('tinymceEditer').getContent();
            });

  • 问题描述3:

当点击submit 生成pdf文件时,生成的 pdf 文件样式改变了

  • 解决方法:

这是因为将 word 文档转换成 html 的时候自动的加上了这段样式

解决方法可以在前端解决也可以在后端去解决,这里我选择了在后端解决

后端在返回给前端html 的时候,在返回的内容上加上

respInfo.setContent("

"+content+"
")

总结何尝不是一种学习

你可能感兴趣的