Vue 自定義富文字編輯器 tinymce 支援匯入 word 模板

haoxiaoyong1014發表於2018-09-13

自定義富文字編輯器分為前端專案和後端專案兩個部分,首先先說一下前端專案

前端

前端專案地址: 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')">Submit</Button>
            <Button type="ghost" @click="handleReset('editorModel')">Reset</Button>
          </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 資料,將其設定到

<textarea class='tinymce-textarea' id="tinymceEditer" style="height: 800px"></textarea>
這個標籤中,所有的編輯器都是這個道理.

上傳成功之後:

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

看下效果圖:

image

點選 submit 我是在後端將其轉換成了 pdf 檔案(按需求定義)

如果在整合中出現: Uncaught SyntaxError: Unexpected token < 這個錯誤

解決方法:

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

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

如果編輯器的樣式還是沒有出來,只出來一個編輯框的話 ,就在你的根目錄下的 index.html 中引入:

<script src="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>

後端

後端(服務端)專案地址: 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.poi</groupId>
      <artifactId>poi</artifactId>
      <version>3.12</version>
    </dependency>
    
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-scratchpad</artifactId>
      <version>3.12</version>
    </dependency>
    
    <dependency>
      <groupId>fr.opensagres.xdocreport</groupId>
      <artifactId>fr.opensagres.xdocreport.document</artifactId>
      <version>1.0.5</version>
    </dependency>
    
    <dependency>
      <groupId>fr.opensagres.xdocreport</groupId>
      <artifactId>org.apache.poi.xwpf.converter.xhtml</artifactId>
      <version>1.0.5</version>
    </dependency>
    
      <!-- https://mvnrepository.com/artifact/org.apache.commons.io/commonsIO -->
      <dependency>
        <groupId>org.apache.commons.io</groupId>
        <artifactId>commonsIO</artifactId>
        <version>2.6</version>
      </dependency>
      
    <dependency>
      <groupId>com.aspose.words</groupId>
      <artifactId>aspose-words</artifactId>
      <version>15.8.0</version>
    </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 的時候自動的加上了這段樣式

<div style="width: 595.0pt; margin: 72.0pt 90.0pt 72.0pt 90.0pt;"></div>

解決方法可以在前端解決也可以在後端去解決,這裡我選擇了在後端解決

後端在返回給前端html 的時候,在返回的內容上加上

respInfo.setContent("<div style=\"width: 595.0pt; margin: -72.0pt -90.0pt -72.0pt -90.0pt !important;\">"+content+"</div>")

總結何嘗不是一種學習

相關文章