Java程序员的Word一键粘贴奇幻漂流记

大家好,我是四川一个被火锅熏晕的Java程序员(代码界的郫县豆瓣,越陈越香)。最近接了个CMS企业官网改(keng)造(die)项目,客户提出了个"比火锅还烫手"的需求…

需求分析(客户想让我上天)

客户想要在后台新闻编辑器实现:

  1. Office全家桶一键导入(Word/Excel/PPT/PDF)
  2. Word直接粘贴(包括公式、表格、图片等)
  3. 公式支持要逆天(Latex↔MathML,兼容MathType)
  4. 微信公众号内容也能导入
  5. 预算680元(我:这钱连火锅底料都买不起啊!)

技术选型(穷到吃土套餐)

经过九九八十一天的调研(其实就是刷了3天GitHub),发现:

  • 现成方案对公式支持≈0
  • 商业方案报价680后面要加个0
  • 最终决定:自己动手,丰衣足食!

解决方案(真香警告)

前端方案(Vue3 + wangEditor插件)

// word-import-plugin.js
export default {
  install(editor) {
    // 添加Office全家桶菜单
    editor.menus.extend('officeImport', {
      icon: '📎',
      tip: 'Office一键导入',
      onClick: () => this.handleOfficeImport(editor)
    });
    
    // 处理Office文件导入
    handleOfficeImport(editor) {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf';
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (!file) return;
        
        // 显示加载动画
        editor.showLoading('正在解析文档...');
        
        // 调用后端解析接口
        this.parseOfficeFile(file).then(html => {
          editor.txt.html(html);
          editor.hideLoading();
        });
      };
      input.click();
    },
    
    // 处理Word粘贴(专治各类Office"牛皮癣"样式)
    handleWordPaste(editor, html) {
      // 1. 清洗Word特有的垃圾样式
      const cleanedHtml = html
        .replace(/class="MsoNormal"/g, '')
        .replace(/style="[^"]*"/g, match => 
          match.includes('mso-') ? '' : match);
      
      // 2. 处理公式转换
      return this.convertFormulas(cleanedHtml);
    },
    
    // 公式转换(Latex ↔ MathML)
    convertFormulas(content) {
      // 匹配Latex公式($...$格式)
      return content.replace(/\$([^$]+)\$/g, (match, latex) => {
        // 调用后端转换接口
        const mathml = await this.convertLatexToMathML(latex);
        return mathml || match;
      });
    }
  }
}

后端Java方案(Spring Boot + POI全家桶)

// WordImportController.java
@RestController
@RequestMapping("/api/import")
public class WordImportController {
    
    @Autowired
    private AliyunOssService ossService;
    
    @PostMapping("/office")
    public ResponseEntity importOfficeFile(@RequestParam("file") MultipartFile file) {
        try {
            String htmlContent;
            String filename = file.getOriginalFilename();
            String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
            
            switch(extension) {
                case "doc":
                case "docx":
                    htmlContent = new WordParser().parse(file.getInputStream());
                    break;
                case "xls":
                case "xlsx":
                    htmlContent = new ExcelParser().parse(file.getInputStream());
                    break;
                case "ppt":
                case "pptx":
                    htmlContent = new PowerPointParser().parse(file.getInputStream());
                    break;
                case "pdf":
                    htmlContent = new PdfParser().parse(file.getInputStream());
                    break;
                default:
                    throw new IllegalArgumentException("不支持的文档格式");
            }
            
            // 处理文档中的图片上传
            htmlContent = processImages(htmlContent);
            
            return ResponseEntity.ok(htmlContent);
        } catch (Exception e) {
            return ResponseEntity.status(500).body("文档解析失败: " + e.getMessage());
        }
    }
    
    // 处理文档中的图片并上传到OSS
    private String processImages(String html) {
        // 使用Jsoup解析HTML并查找图片
        Document doc = Jsoup.parse(html);
        Elements imgs = doc.select("img");
        
        for (Element img : imgs) {
            String src = img.attr("src");
            if (src.startsWith("data:")) {
                // Base64图片上传到OSS
                String ossUrl = ossService.uploadBase64Image(src);
                img.attr("src", ossUrl);
            }
        }
        
        return doc.body().html();
    }
    
    // Latex转MathML
    @PostMapping("/latex-to-mathml")
    public String convertLatexToMathML(@RequestBody String latex) {
        try {
            // 调用Python服务转换(Node.js也行)
            Process process = Runtime.getRuntime().exec(
                new String[]{"python", "latex2mathml.py", latex});
            
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            
            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
            
            return builder.toString();
        } catch (IOException e) {
            throw new RuntimeException("公式转换失败", e);
        }
    }
}

部署指南(小白都能懂)

  1. 前端安装插件:
npm install office-import-plugin --save
  1. 在wangEditor中注册:
import OfficeImportPlugin from 'office-import-plugin';

const editor = new Editor({...});
editor.use(OfficeImportPlugin);
  1. 后端Java依赖(pom.xml):

    
    
        org.apache.poi
        poi
        5.2.3
    
    
        org.apache.poi
        poi-ooxml
        5.2.3
    
    
    
    
        org.apache.pdfbox
        pdfbox
        2.0.27
    
    
    
    
        com.aliyun.oss
        aliyun-sdk-oss
        3.15.1
    

价格真相(程序员の愤怒)

原本680的预算,实际开发成本:

  • 3天调研 × 600元/天 = 1800元
  • 7天开发 × 800元/天 = 5600元
  • 2天测试 × 600元/天 = 1200元
    总成本:8600元

但为了群里兄弟们的信任,我决定:

  1. 开源核心代码(反正也赚不到钱)
  2. 收费插件版卖680(就当交个朋友)
  3. 靠代理提成回本(奸商の微笑)

加群福利(套路时间)

QQ群:223813913 现在加入:

  • 送1-99元红包(99元仅限前3名,其实就是3个1元红包)
  • 推荐客户提成20%(你赚钱我赚吆喝)
  • 共享外包资源(大家一起007)

最后说句掏心窝子的话:这功能真的是政府/企业网站刚需!现在上车当代理,明年就能全款买火锅店!(手动狗头)


注:本文纯属娱乐,实际开发请做好心理准备。真要680做这功能,建议改行卖串串更赚钱。代码仅供参考,生产环境请备好救心丸!

复制插件文件

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转图片

上传网络图片

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

下载示例

点击下载完整示例

Logo

欢迎大家加入成都城市开发者社区,“和我在成都的街头走一走”,让我们一起携手,汇聚IT技术潮流,共建社区文明生态!

更多推荐