wangEditor学校OA系统中如何实现word粘贴并转存?

【集团级富文本编辑器国产化集成项目纪实——从需求拆解到全栈信创落地】
2023年X月X日 周X 上海·浦东新区


一、项目背景与核心需求

作为某集团技术负责人,近期承接政府数字化项目时,客户提出关键需求:

  1. 功能需求

    • Word粘贴/导入:支持从Word复制内容(含表格/形状/字体样式)粘贴至编辑器,图片自动上传至华为云OBS
    • 微信公众号抓取:解析公众号文章HTML,自动下载图片并替换为OBS链接
    • 多格式导入:Word/Excel/PPT/PDF全格式支持,保留原始排版和图片
  2. 技术约束

    • 前端:Vue2-cli + wangEditor4(需无缝集成,不破坏现有JSP架构)
    • 后端:JSP + Servlet(需兼容IE8及国产浏览器)
    • 部署:华为云ECS(CentOS 7) + OBS对象存储(未来扩展至阿里云/Azure)
  3. 合规要求

    • 信创兼容:支持中标麒麟/统信UOS等国产OS,龙芯/飞腾等国产CPU
    • 授权模式:买断源代码(预算≤58万),避免年度续费和商务流程

二、技术选型与风险评估

1. 富文本编辑器插件方案对比
方案优势风险国产化适配成本(买断)
wangEditor4扩展原生支持Vue2,API开放需自行开发Office解析模块完全兼容¥18万
UEditor信创版政府项目经验丰富基于Flash(IE8需polyfill)部分兼容¥45万
自定义开发完全可控开发周期长(预估8人月)需适配¥80万+
WordPaster源码版完全开源(下载源码)
功能完整
集成简单
部署简单
使用简单需要终端安装插件完全兼容¥9999

决策:选择WordPaster源码版,理由:

  • 集团年项目量1000+,买断授权可摊薄成本至单项目¥0.058万
  • 华为云OBS SDK提供Java/JS双版本,与JSP后端无缝集成
2. 信创环境验证清单
  • 浏览器
      
      
    
  • CPU架构
    # 龙芯3A5000测试命令  
    cat /proc/cpuinfo | grep "model name"  
    java -version  # 验证龙芯JDK运行环境  
    

三、核心功能开发实现

1. 前端集成(Vue2 + wangEditor4)

步骤1:扩展编辑器工具栏

// src/components/RichTextEditor.vue  
import E from 'wangeditor'  

export default {  
  mounted() {  
    const editor = new E('#editor-container')  
    // 添加自定义按钮  
    editor.config.menus = [  
      ...editor.config.menus,  
      'word-paste',  
      'wechat-paste'  
    ]  

    // 拦截粘贴事件  
    editor.config.pasteTextHandle = (content) => {  
      return this.processClipboardData(content)  
    }  
    editor.create()  
  },  
  methods: {  
    async processClipboardData(html) {  
      // 处理Word粘贴内容  
      const doc = new DOMParser().parseFromString(html, 'text/html')  
      const images = doc.querySelectorAll('img[src^="data:image"]')  

      for (let img of images) {  
        const blob = await this.dataURLtoBlob(img.src)  
        const formData = new FormData()  
        formData.append('file', blob, 'paste-image.png')  

        // 调用JSP后端上传接口  
        const res = await fetch('/file/upload', {  
          method: 'POST',  
          body: formData  
        })  
        const url = await res.text()  
        img.src = url  
      }  
      return doc.body.innerHTML  
    },  
    dataURLtoBlob(dataUrl) {  
      // BASE64转Blob实现(略)  
    }  
  }  
}  

步骤2:微信公众号内容抓取(跨域代理)

// Web.xml配置代理Servlet(解决跨域问题)  
  
  WechatProxy  
  com.group.proxy.WechatProxyServlet  
  
  
  WechatProxy  
  /proxy/wechat/*  
  
2. 后端实现(JSP + OBS SDK)

文件上传接口

// src/com/group/servlet/FileUploadServlet.java  
@WebServlet("/file/upload")  
@MultipartConfig  
public class FileUploadServlet extends HttpServlet {  

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)  
        throws ServletException, IOException {  
        
        Part filePart = req.getPart("file");  
        String fileName = UUID.randomUUID().toString() + ".png";  

        // 华为云OBS配置  
        String endPoint = "obs.cn-east-3.myhuaweicloud.com";  
        String ak = "your-access-key";  
        String sk = "your-secret-key";  

        ObsClient obsClient = new ObsClient(ak, sk, endPoint);  
        obsClient.putObject("group-bucket", fileName, filePart.getInputStream());  

        // 返回OBS访问URL  
        String url = String.format("https://%s.obs.%s.myhuaweicloud.com/%s",  
            "group-bucket",  
            endPoint.split("\\.")[1],  
            fileName);  

        resp.setContentType("text/plain");  
        resp.getWriter().print(url);  
    }  
}  

Office文档解析(Apache POI)

// 处理PPT中的图片  
public void extractImagesFromPpt(InputStream pptStream, String outputDir) throws IOException {  
    XMLSlideShow ppt = new XMLSlideShow(pptStream);  
    for (XSLFSlide slide : ppt.getSlides()) {  
        for (XSLFShape shape : slide.getShapes()) {  
            if (shape instanceof XSLFPictureShape) {  
                XSLFPictureData picData = ((XSLFPictureShape) shape).getPictureData();  
                byte[] data = picData.getData();  
                Files.write(Paths.get(outputDir, picData.getFileName()), data);  
            }  
        }  
    }  
}  

四、信创环境专项优化

1. 中文字体渲染优化
  • Linux服务器配置
    # 统信UOS安装微软雅黑字体  
    mkdir -p /usr/share/fonts/windows  
    wget https://example.com/msyh.ttf -O /usr/share/fonts/windows/msyh.ttf  
    fc-cache -fv  
    
2. 多云存储抽象层
public interface CloudStorage {  
    String upload(byte[] data, String fileName);  
    void delete(String key);  
}  

public class ObsStorage implements CloudStorage {  
    // 华为云OBS实现  
}  

public class OssStorage implements CloudStorage {  
    // 阿里云OSS实现(预留)  
}  

五、项目交付与验收标准

  1. 功能验收

    • 在龙芯3A5000 + 奇安信浏览器中成功导入200页PPT
    • 微信公众号图片抓取准确率100%
  2. 性能指标

    • 50MB Word文件导入耗时≤5秒
    • 并发上传支持50QPS
  3. 交付物

    • 完整源代码(含前端Vue组件+JSP后端服务)
    • 《信创环境部署指南》(含龙芯/飞腾适配说明)
    • 压力测试报告(LoadRunner脚本及结果)

最终成本:通过开源组件复用+华为云OBS免费额度,项目总成本控制在42万元内,较预算节约27.6%。


【项目总结】

  1. 选型关键:优先选择支持国产化的成熟开源框架,避免重复造轮子
  2. 兼容性策略:采用"渐进增强"方案,核心功能支持IE8,新特性面向现代浏览器
  3. 授权管理:建立集团内部开源代码库,实现授权一次复用无限

(附:项目私有GitLab仓库地址已同步至集团知识管理系统,授权码:GOV-GROUP-2023-XXXXX)

复制插件文件

WordPaster插件文件夹
安装jquery

npm install jquery

导入组件

import E from 'wangeditor'
const { $, BtnMenu, DropListMenu, PanelMenu, DropList, Panel, Tooltip } = E
import {WordPaster} from '../../static/WordPaster/js/w'
import {zyCapture} from '../../static/zyCapture/z'
import {zyOffice} from '../../static/zyOffice/js/o'

初始化组件




//zyCapture Button
class zyCaptureBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="截屏">
                <img src="../../static/zyCapture/z.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        window.zyCapture.setEditor(this.editor).Capture();
    }
    tryChangeActive() {this.active()}
}
//zyOffice Button
class importWordBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入Word文档(docx)">
                <img src="../../static/zyOffice/css/w.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        window.zyOffice.SetEditor(this.editor).api.openDoc();
    }
    tryChangeActive() {this.active()}
}
//zyOffice Button
class exportWordBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导出Word文档(docx)">
                <img src="../../static/zyOffice/css/exword.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        window.zyOffice.SetEditor(this.editor).api.exportWord();
    }
    tryChangeActive() {this.active()}
}
//zyOffice Button
class importPdfBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入PDF文档">
                <img src="../../static/zyOffice/css/pdf.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        window.zyOffice.SetEditor(this.editor).api.openPdf();
    }
    tryChangeActive() {this.active()}
}

//WordPaster Button
class WordPasterBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="Word一键粘贴">
                <img src="../../static/WordPaster/w.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor).Paste();
    }
    tryChangeActive() {this.active()}
}
//wordImport Button
class WordImportBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入Word文档">
                <img src="../../static/WordPaster/css/doc.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor).importWord();
    }
    tryChangeActive() {this.active()}
}
//excelImport Button
class ExcelImportBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入Excel文档">
                <img src="../../static/WordPaster/css/xls.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor).importExcel();
    }
    tryChangeActive() {this.active()}
}
//ppt paster Button
class PPTImportBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入PPT文档">
                <img src="../../static/WordPaster/css/ppt1.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor).importPPT();
    }
    tryChangeActive() {this.active()}
}
//pdf paster Button
class PDFImportBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="导入PDF文档">
                <img src="../../static/WordPaster/css/pdf.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor);
        WordPaster.getInstance().ImportPDF();
    }
    tryChangeActive() {this.active()}
}
//importWordToImg Button
class ImportWordToImgBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="Word转图片">
                <img src="../../static/WordPaster/word1.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor).importWordToImg();
    }
    tryChangeActive() {this.active()}
}
//network paster Button
class NetImportBtn extends BtnMenu {
    constructor(editor) {
        const $elem = E.$(
            `<div class="w-e-menu" data-title="网络图片一键上传">
                <img src="../../static/WordPaster/net.png"/>
            </div>`
        )
        super($elem, editor)
    }
    clickHandler() {
        WordPaster.getInstance().SetEditor(this.editor);
        WordPaster.getInstance().UploadNetImg();
    }
    tryChangeActive() {this.active()}
}

export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  mounted(){
    var editor = new E('#editor');
    WordPaster.getInstance({
        //上传接口:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
        PostUrl: "http://localhost:8891/upload.aspx",
        License2:"",
        //为图片地址增加域名:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
        ImageUrl:"http://localhost:8891{url}",
        //设置文件字段名称:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
        FileFieldName: "file",
        //提取图片地址:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
        ImageMatch: ''
    });

    zyCapture.getInstance({
        config: {
            PostUrl: "http://localhost:8891/upload.aspx",
            License2: '',
            FileFieldName: "file",
            Fields: { uname: "test" },
            ImageUrl: 'http://localhost:8891{url}'
        }
    })

    // zyoffice,
    // 使用前请在服务端部署zyoffice,
    // http://www.ncmem.com/doc/view.aspx?id=82170058de824b5c86e2e666e5be319c
    zyOffice.getInstance({
        word: 'http://localhost:13710/zyoffice/word/convert',
        wordExport: 'http://localhost:13710/zyoffice/word/export',
        pdf: 'http://localhost:13710/zyoffice/pdf/upload'
    })

    // 注册菜单
    E.registerMenu("zyCaptureBtn", zyCaptureBtn)
    E.registerMenu("WordPasterBtn", WordPasterBtn)
    E.registerMenu("ImportWordToImgBtn", ImportWordToImgBtn)
    E.registerMenu("NetImportBtn", NetImportBtn)
    E.registerMenu("WordImportBtn", WordImportBtn)
    E.registerMenu("ExcelImportBtn", ExcelImportBtn)
    E.registerMenu("PPTImportBtn", PPTImportBtn)
    E.registerMenu("PDFImportBtn", PDFImportBtn)
    E.registerMenu("importWordBtn", importWordBtn)
    E.registerMenu("exportWordBtn", exportWordBtn)
    E.registerMenu("importPdfBtn", importPdfBtn)


    //挂载粘贴事件
    editor.txt.eventHooks.pasteEvents.length=0;
    editor.txt.eventHooks.pasteEvents.push(function(){
      WordPaster.getInstance().SetEditor(editor).Paste();
      e.preventDefault();
    });
    editor.create();

    var edt2 = new E('#editor2');
    //挂载粘贴事件
    edt2.txt.eventHooks.pasteEvents.length=0;
    edt2.txt.eventHooks.pasteEvents.push(function(){
      WordPaster.getInstance().SetEditor(edt2).Paste();
      e.preventDefault();
      return;
    });
    edt2.create();
  }
}




h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}

测试前请配置图片上传接口并测试成功
接口测试
接口返回JSON格式参考

为编辑器添加按钮

  components: { Editor, Toolbar },
  data () {
    return {
      editor: null,
      html: 'dd',
      toolbarConfig: {
        insertKeys: {
          index: 0,
          keys: ['zycapture', 'wordpaster', 'pptimport', 'pdfimport', 'netimg', 'importword', 'exportword', 'importpdf']
        }
      },
      editorConfig: {
        placeholder: ''
      },
      mode: 'default' // or 'simple'
    }
  },

整合效果

wangEditor4整合效果

导入Word文档,支持doc,docx

粘贴Word和图片

导入Excel文档,支持xls,xlsx

粘贴Word和图片

粘贴Word

一键粘贴Word内容,自动上传Word中的图片,保留文字样式。
粘贴Word和图片

Word转图片

一键导入Word文件,并将Word文件转换成图片上传到服务器中。
导入Word转图片

导入PDF

一键导入PDF文件,并将PDF转换成图片上传到服务器中。
导入PDF转图片

导入PPT

一键导入PPT文件,并将PPT转换成图片上传到服务器中。
导入PPT转图片

上传网络图片

一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片
自动上传网络图片

下载示例

点击下载完整示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值