国企OA系统怎样集成WangEditor的PDF标注功能?
大家好,我是四川一个被火锅熏晕的Java程序员(代码界的郫县豆瓣,越陈越香)。最近接了个CMS企业官网改(keng)造(die)项目,客户提出了个"比火锅还烫手"的需求…
·
Java程序员的Word一键粘贴奇幻漂流记
大家好,我是四川一个被火锅熏晕的Java程序员(代码界的郫县豆瓣,越陈越香)。最近接了个CMS企业官网改(keng)造(die)项目,客户提出了个"比火锅还烫手"的需求…
需求分析(客户想让我上天)
客户想要在后台新闻编辑器实现:
- Office全家桶一键导入(Word/Excel/PPT/PDF)
- Word直接粘贴(包括公式、表格、图片等)
- 公式支持要逆天(Latex↔MathML,兼容MathType)
- 微信公众号内容也能导入
- 预算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);
}
}
}
部署指南(小白都能懂)
- 前端安装插件:
npm install office-import-plugin --save
- 在wangEditor中注册:
import OfficeImportPlugin from 'office-import-plugin';
const editor = new Editor({...});
editor.use(OfficeImportPlugin);
- 后端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元
但为了群里兄弟们的信任,我决定:
- 开源核心代码(反正也赚不到钱)
- 收费插件版卖680(就当交个朋友)
- 靠代理提成回本(奸商の微笑)
加群福利(套路时间)
QQ群:223813913 现在加入:
- 送1-99元红包(99元仅限前3名,其实就是3个1元红包)
- 推荐客户提成20%(你赚钱我赚吆喝)
- 共享外包资源(大家一起007)
最后说句掏心窝子的话:这功能真的是政府/企业网站刚需!现在上车当代理,明年就能全款买火锅店!(手动狗头)
注:本文纯属娱乐,实际开发请做好心理准备。真要680做这功能,建议改行卖串串更赚钱。代码仅供参考,生产环境请备好救心丸!
复制插件文件
安装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'
}
},
整合效果
导入Word文档,支持doc,docx
导入Excel文档,支持xls,xlsx
粘贴Word
一键粘贴Word内容,自动上传Word中的图片,保留文字样式。
Word转图片
一键导入Word文件,并将Word文件转换成图片上传到服务器中。
导入PDF
一键导入PDF文件,并将PDF转换成图片上传到服务器中。
导入PPT
一键导入PPT文件,并将PPT转换成图片上传到服务器中。
上传网络图片
一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片
下载示例
更多推荐
所有评论(0)