老张的CMS企业官网外包项目日记:给UEditor加上Word粘贴神功

Day 1:接到需求时的懵逼时刻

"什么?要在UEditor里实现Word一键粘贴?还要支持Latex公式转MathML?"我看着需求文档,感觉头发又少了几根。客户王总拍着胸脯说:“小张啊,我们公司老同志多,能直接从Word复制粘贴是最吼的!”

好吧,谁让人家是甲方爸爸呢。我摸了摸钱包里仅剩的680元预算,打开了QQ群:“各位大佬,有推荐的好用编辑器插件吗?要能完美支持Word粘贴的那种…”

Day 2:发现"新大陆" - Luckysheet+UEditor组合拳

经过三天三夜的百度和GitHub冲浪,我发现了两个宝贝:

  1. Luckysheet:这个国产开源表格神器居然能处理Word文档解析
  2. UEditor插件市场:有个叫ueditor-word-import的插件看着不错

但是!这两个都不能完美满足需求。于是我决定:自己动手,丰衣足食

前端实现:Vue3 + UEditor插件开发

1. 修改UEditor配置(ueditor.config.js)

// 添加自定义工具栏按钮
, toolbars: [[
    // ...原有配置
    , 'wordimport' // 我们的新按钮
]]
// 添加插件路径
, wordImport: {
    serverUrl: '/api/editor/wordImport', // 后端接口
    accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf',
    maxSize: 10 // 10MB限制
}

2. 创建Word导入插件(WordImportPlugin.js)

UE.plugin.register('wordimport', function (){
    var me = this;
    
    return {
        commands: {
            'wordimport': {
                execCommand: function () {
                    // 创建文件选择对话框
                    var fileInput = document.createElement('input');
                    fileInput.type = 'file';
                    fileInput.accept = me.getOpt('wordImport').accept;
                    
                    fileInput.onchange = function () {
                        if (this.files.length === 0) return;
                        
                        var formData = new FormData();
                        formData.append('file', this.files[0]);
                        
                        // 显示加载中
                        me.fireEvent('showmessage', {
                            'content': '正在解析文档...',
                            'timeout': 0
                        });
                        
                        // 上传到后端处理
                        fetch(me.getOpt('wordImport').serverUrl, {
                            method: 'POST',
                            body: formData,
                            headers: {
                                'Authorization': 'Bearer ' + localStorage.getItem('token')
                            }
                        })
                        .then(response => response.json())
                        .then(data => {
                            if (data.success) {
                                // 插入解析后的HTML
                                me.execCommand('inserthtml', data.content);
                                // 处理图片上传
                                if (data.images) {
                                    data.images.forEach(img => {
                                        me.execCommand('insertimage', {
                                            src: img.url,
                                            _src: img.url,
                                            alt: img.name
                                        });
                                    });
                                }
                            } else {
                                alert('文档解析失败: ' + data.message);
                            }
                            me.fireEvent('hidemessage');
                        })
                        .catch(err => {
                            console.error(err);
                            me.fireEvent('hidemessage');
                            alert('上传失败,请重试');
                        });
                    };
                    
                    fileInput.click();
                },
                queryCommandState: function () {
                    return me.isReady ? 0 : -1;
                }
            }
        },
        // 添加按钮到工具栏
        init: function () {
            me.ready(function () {
                var wordImportBtn = new UE.ui.Button({
                    name: 'wordimport',
                    title: '导入Word/Excel/PPT/PDF',
                    onclick: function () {
                        me.execCommand('wordimport');
                    }
                });
                me.addListener('ready', function () {
                    me.ui.addToolbarButton(wordImportBtn);
                });
            });
        }
    };
});

后端实现:Java Servlet处理文档上传

1. 创建WordImportServlet.java

@WebServlet("/api/editor/wordImport")
@MultipartConfig
public class WordImportServlet extends HttpServlet {
    
    private static final long serialVersionUID = 1L;
    
    // OSS客户端(阿里云)
    private OSS ossClient;
    private String bucketName = "your-oss-bucket";
    
    @Override
    public void init() throws ServletException {
        // 初始化OSS客户端
        String endpoint = "oss-cn-hangzhou.aliyuncs.com";
        String accessKeyId = "your-access-key";
        String accessKeySecret = "your-secret-key";
        ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        resp.setContentType("application/json");
        PrintWriter out = resp.getWriter();
        JSONObject result = new JSONObject();
        
        try {
            // 获取上传的文件
            Part filePart = req.getPart("file");
            if (filePart == null || filePart.getSize() == 0) {
                result.put("success", false);
                result.put("message", "请选择有效文件");
                out.print(result.toString());
                return;
            }
            
            // 检查文件大小
            long maxSize = 10 * 1024 * 1024; // 10MB
            if (filePart.getSize() > maxSize) {
                result.put("success", false);
                result.put("message", "文件大小不能超过10MB");
                out.print(result.toString());
                return;
            }
            
            // 获取文件信息
            String fileName = filePart.getSubmittedFileName();
            String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
            
            // 临时保存文件(实际项目中可以用内存处理,这里简化)
            String tempDir = System.getProperty("java.io.tmpdir");
            String tempFilePath = tempDir + File.separator + UUID.randomUUID() + "." + fileExt;
            filePart.write(tempFilePath);
            
            // 处理不同类型文件
            String htmlContent = "";
            List imageUrls = new ArrayList<>();
            
            switch (fileExt) {
                case "doc":
                case "docx":
                    htmlContent = processWordDocument(tempFilePath, imageUrls);
                    break;
                case "xls":
                case "xlsx":
                    htmlContent = processExcelDocument(tempFilePath, imageUrls);
                    break;
                case "ppt":
                case "pptx":
                    htmlContent = processPptDocument(tempFilePath, imageUrls);
                    break;
                case "pdf":
                    htmlContent = processPdfDocument(tempFilePath, imageUrls);
                    break;
                default:
                    result.put("success", false);
                    result.put("message", "不支持的文件类型");
                    out.print(result.toString());
                    return;
            }
            
            // 上传图片到OSS
            for (String imagePath : imageUrls) {
                File imgFile = new File(imagePath);
                if (imgFile.exists()) {
                    String ossKey = "editor/images/" + UUID.randomUUID() + "." + 
                            FilenameUtils.getExtension(imagePath);
                    ossClient.putObject(bucketName, ossKey, imgFile);
                    imgFile.delete(); // 删除临时文件
                    
                    // 替换本地路径为OSS URL
                    String imageUrl = "https://" + bucketName + ".oss-cn-hangzhou.aliyuncs.com/" + ossKey;
                    htmlContent = htmlContent.replace(imagePath, imageUrl);
                }
            }
            
            // 处理Latex公式转MathML
            htmlContent = convertLatexToMathML(htmlContent);
            
            result.put("success", true);
            result.put("content", htmlContent);
            result.put("images", new JSONArray()); // 图片URL已在内容中替换
            
        } catch (Exception e) {
            e.printStackTrace();
            result.put("success", false);
            result.put("message", "服务器错误: " + e.getMessage());
        } finally {
            out.print(result.toString());
            out.flush();
        }
    }
}

Day 3:测试与优化

经过一番折腾,我终于实现了基本功能。测试时发现:

  1. Word粘贴:通过监听paste事件实现
  2. 图片处理:使用Canvas提取剪贴板中的图片
  3. 样式保留:通过解析Word的XML结构

前端增强:添加粘贴事件处理

// 在UEditor初始化后添加
me.addListener('ready', function() {
    // 监听粘贴事件
    document.addEventListener('paste', function(e) {
        // 检查是否是富文本编辑器内的粘贴
        if (!me.isFocus()) return;
        
        // 阻止默认粘贴行为
        e.preventDefault();
        
        // 获取剪贴板数据
        var clipboardData = e.clipboardData || window.clipboardData;
        if (!clipboardData) return;
        
        // 处理文件(如从Word直接粘贴图片)
        if (clipboardData.files && clipboardData.files.length > 0) {
            handlePastedFiles(clipboardData.files);
            return;
        }
        
        // 处理HTML内容(如从Word复制的文本)
        var html = clipboardData.getData('text/html');
        if (html) {
            // 简单过滤Word垃圾代码
            html = cleanWordHtml(html);
            me.execCommand('inserthtml', html);
            return;
        }
        
        // 处理纯文本
        var text = clipboardData.getData('text/plain');
        if (text) {
            me.execCommand('inserthtml', text.replace(/\n/g, '
'));
        }
    });
});

Day 4:预算控制与"白嫖"技巧

看着银行卡余额,我必须精打细算:

  1. 开源库选择

    • Apache POI:免费处理Office文档
    • PDFBox:免费处理PDF
    • JLatexMath:免费Latex转MathML
  2. 云服务优化

    • OSS使用按量付费,预估每月费用<50元
    • ECS使用1核2G突发性能实例,年费约500元
  3. 开发工具

    • Eclipse JEE:免费
    • Vue CLI:免费

总预算控制

  • 域名:60元/年(必须)
  • 服务器:500元/年
  • OSS:<50元/月
  • 总计:680元刚好够用!

Day 5:QQ群推广与"躺赚"计划

在测试间隙,我在QQ群(223813913)发布了插件信息:

"各位大佬,我开发了个UEditor的Word导入插件,完美支持:

  1. Word/Excel/PPT/PDF导入
  2. 图片自动上传OSS
  3. Latex公式转MathML
  4. 微信公众号内容兼容

现在招募测试员和代理商!推荐客户成交即得20%提成!"

没想到反响热烈,当天就有3个客户咨询!看来这个"躺赚"计划还真行得通…

最终效果展示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

客户王总试用后:“哎呀妈呀,这功能太得劲了!我们那些老同志终于不用对着HTML代码发呆了!”

看着账户里逐渐增加的推荐提成,我仿佛看到了财务自由的曙光…(醒醒,该改bug了!)

完整项目结构

cms-project/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/
│   │   │       ├── servlet/WordImportServlet.java
│   │   │       └── util/DocumentUtils.java
│   │   ├── resources/
│   │   └── webapp/
│   │       ├── WEB-INF/
│   │       ├── static/
│   │       │   ├── js/
│   │       │   │   ├── ueditor/
│   │       │   │   │   ├── ueditor.config.js
│   │       │   │   │   └── plugins/WordImportPlugin.js
│   │       │   └── css/
│   │       └── index.jsp
│   └── test/
└── pom.xml

总结

这次项目让我深刻体会到:

  1. 甲方需求永远比想象中复杂
  2. 开源库的组合使用能创造奇迹
  3. QQ群推广真是低成本获客利器
  4. 680元预算也能做出不错的产品(前提是不睡觉)

最后,欢迎各位加入QQ群223813913,一起交流技术,一起赚钱!新人加群送红包,推荐客户还有20%提成,这波不亏!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐