欢迎光临
我们一直在努力

网页富文本编辑器如何解决WangEditor粘贴Word内容时的格式错乱问题?

农业集团信息管理系统富文本增强功能开发全记录

一、需求确认与技术栈梳理

作为湖南某农业集团技术负责人,近期接到客户核心需求:在现有信息管理系统的后台文章发布模块中新增三大功能:

  • 智能粘贴功能:支持Word/微信公众号内容直接粘贴,图片自动上传至专用存储服务器(预留多云存储接口)
  • 文档导入功能:支持Word/Excel/PPT/PDF全格式导入,完整保留样式与图片
  • 性能优化:图片采用二进制存储,避免Base64编码导致的性能问题
  • 现有技术栈:

    • 前端:Vue2.6 + wangEditor 5.1(国产开源编辑器)
    • 后端:SpringBoot 2.6.x + MySQL 8.0
    • 部署环境:内网私有化部署(需支持离线环境)
    • 存储方案:初期采用文件系统存储,预留对象存储接口

    二、技术调研与方案选型

    1. 编辑器扩展方案评估

    wangEditor深度定制:

    • 优势:
      • 国产编辑器,对中文排版支持优秀
      • 模块化设计,插件机制完善
      • 与Vue集成度高(官方提供Vue组件)
    • 挑战:
      • 官方文档对Office粘贴支持不足
      • 社区资源较UEditor少

    替代方案对比:

    方案兼容性扩展性维护成本商业授权
    扩展wangEditor ★★★★☆ ★★★★☆ ★★☆☆☆ 免费
    迁移TinyMCE ★★★★☆ ★★★★★ ★★★☆☆ 企业版收费
    引入CKEditor5 ★★★☆☆ ★★★★★ ★★★★☆ 按功能收费
    泽优WordPaster ★★★★★ ★★★★★ ★☆☆ 完全开源(下载源码)
    完全免费
    免费技术支持(QQ群:223813913)

    决策结果:集成泽优WordPaster插件,不需要改动服务商量,不需要改动客户端,不需要改动现有业务逻辑,集成简单,使用简单,用户体验不错,完全开放源码,能够满足集团自主可控的需求。

    2. 文档解析技术选型

    核心组件选择:

    • Word/PPT解析:Apache POI 5.2.3 + poi-tl 1.12(最新稳定版)
    • Excel解析:EasyExcel 3.3.0(阿里开源,内存优化佳)
    • PDF解析:PDFBox 2.0.27(Apache许可) + iText 7.2.5(商业使用需授权)

    关键技术点:

    // Word段落样式提取示例
    XWPFDocument doc = new XWPFDocument(inputStream);
    doc.getParagraphs().forEach(para -> {
    // 获取段落样式
    CTParaRPr rPr = para.getCTP().getPPr().getRPr();
    if (rPr != null) {
    // 处理字体、字号、颜色等
    String font = rPr.getRFonts().getAscii();
    Integer size = rPr.getSz() != null ?
    Integer.parseInt(rPr.getSz().getVal()) / 2 : 12;
    // 存储样式信息…
    }
    });

    3. 存储架构设计

    初期方案:

    /data/storage
    /docs
    /{year}/{month}
    /{docId}/
    content.json # 文档元数据
    images/ # 二进制图片
    {md5}.jpg

    对象存储接口设计:

    public interface CloudStorageAdapter {
    String upload(byte[] data, String ext, String module);
    String getUrl(String key);
    void delete(String key);

    // 实现类示例
    public static class AliyunAdapter implements CloudStorageAdapter {
    // 实现阿里云OSS上传逻辑
    }
    }

    三、核心功能实现

    1. 智能粘贴功能开发

    前端实现(Vue组件):

    // src/components/editor/SmartPastePlugin.js
    export default class SmartPastePlugin {
    constructor(editor) {
    this.editor = editor;
    this.init();
    }

    init() {
    // 监听粘贴事件
    this.editor.on('paste', (evt) => {
    const html = evt.clipboardData.getData('text/html');
    if (!html) return;

    // 检测微信公众号内容
    if (this.isWechatContent(html)) {
    this.processWechatContent(html).then(processed => {
    this.editor.cmd.do('insertHTML', processed);
    });
    return;
    }

    // 处理Word粘贴
    this.processWordPaste(html).then(processed => {
    this.editor.cmd.do('insertHTML', processed);
    });
    });
    }

    async processWordPaste(html) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const images = doc.querySelectorAll('img[src^="blob:"]');

    // 批量上传图片
    const uploadPromises = Array.from(images).map(img => {
    return fetch(img.src).then(res => res.blob())
    .then(blob => this.uploadImage(blob));
    });

    const urls = await Promise.all(uploadPromises);

    // 替换图片URL
    let i = 0;
    return html.replace(/]+src="blob:[^"]+"/g, () => {
    return ` handlePasteImage(
    @RequestParam("file") MultipartFile file) {

    // 生成唯一文件名
    String filename = UUID.randomUUID() +
    StringUtils.getExtension(file.getOriginalFilename());

    // 存储二进制数据
    String path = storageService.storePasteImage(
    file.getBytes(),
    filename
    );

    return ResponseEntity.ok(new UploadResult(path));
    }
    }

    2. 文档导入功能实现

    统一导入接口设计:

    @Service
    public class DocumentImportService {

    @Autowired
    private WordImporter wordImporter;

    @Autowired
    private ExcelImporter excelImporter;

    @Autowired
    private PptImporter pptImporter;

    @Autowired
    private PdfImporter pdfImporter;

    public ImportResult importDocument(MultipartFile file) throws IOException {
    String ext = StringUtils.getExtension(file.getOriginalFilename());

    switch (ext.toLowerCase()) {
    case "docx":
    case "doc":
    return wordImporter.importDoc(file);
    case "xlsx":
    case "xls":
    return excelImporter.importXls(file);
    case "pptx":
    case "ppt":
    return pptImporter.importPpt(file);
    case "pdf":
    return pdfImporter.importPdf(file);
    default:
    throw new IllegalArgumentException("不支持的文档类型");
    }
    }
    }

    Word导入核心逻辑:

    @Service
    public class WordImporter {

    @Autowired
    private StorageService storageService;

    public ImportResult importDoc(MultipartFile file) throws IOException {
    try (XWPFDocument doc = new XWPFDocument(file.getInputStream())) {
    ImportResult result = new ImportResult();

    // 处理段落
    doc.getParagraphs().forEach(para -> {
    String text = para.getText();
    if (StringUtils.isNotBlank(text)) {
    result.addContent(text);
    }

    // 处理段落样式
    CTParaRPr rPr = para.getCTP().getPPr().getRPr();
    if (rPr != null) {
    StyleInfo style = new StyleInfo();
    // 提取字体信息
    if (rPr.getRFonts() != null) {
    style.setFontFamily(rPr.getRFonts().getAscii());
    }
    // 提取字号
    if (rPr.getSz() != null) {
    style.setFontSize(Integer.parseInt(rPr.getSz().getVal()) / 2);
    }
    result.addStyle(style);
    }
    });

    // 处理图片
    List pictures = doc.getAllPictures();
    for (XWPFPictureData pic : pictures) {
    String url = storageService.storeDocImage(
    pic.getData(),
    pic.suggestFileExtension()
    );
    result.addImage(url);
    }

    return result;
    }
    }
    }

    四、关键问题解决

    1. 样式保留方案

    字体映射处理:

    // 字体替换表(解决Windows特有字体问题)
    private static final Map FONT_MAP = Map.of(
    "Calibri", "Arial",
    "Microsoft YaHei", "SimHei",
    "Times New Roman", "serif"
    );

    public String resolveFontFamily(String original) {
    return FONT_MAP.getOrDefault(original, original);
    }

    颜色转换工具:

    // 前端颜色转换
    function convertOfficeColor(officeColor) {
    // 处理十六进制颜色(如#FF0000)
    if (/^#[0-9A-F]{6}$/i.test(officeColor)) {
    return officeColor;
    }

    // 处理RGB值(如rgb(255,0,0))
    const rgbMatch = officeColor.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/);
    if (rgbMatch) {
    const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0');
    const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0');
    const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0');
    return `#${r}${g}${b}`;
    }

    return '#000000'; // 默认黑色
    }

    2. 性能优化措施

    图片处理优化:

    • 后端启用异步上传队列(使用Spring Batch)
    • 前端实现图片懒加载(通过wangEditor的lazy-load插件)
    • 大文件分片上传(基于WebUploader组件)

    内存管理:

    // Excel导入时使用流式处理
    public void importLargeExcel(MultipartFile file) throws IOException {
    try (InputStream is = file.getInputStream();
    ExcelReader reader = EasyExcel.read(is).build()) {

    ReadSheet sheet = EasyExcel.readSheet().build();
    reader.read(sheet, new PageReadListener(dataList -> {
    // 分批处理数据
    processBatch(dataList);
    }));
    }
    }

    3. 安全加固方案

    文件校验机制:

    public class FileSecurityValidator {

    private static final Set ALLOWED_TYPES = Set.of(
    "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"
    );

    private static final Map FILE_SIGNATURES = Map.of(
    "doc", hexStringToByteArray("D0CF11E0A1B11AE1"),
    "pdf", hexStringToByteArray("255044462D312E"),
    "xls", hexStringToByteArray("D0CF11E0A1B11AE1")
    );

    public static void validate(MultipartFile file) throws SecurityException {
    // 1. 扩展名校验
    String ext = StringUtils.getExtension(file.getOriginalFilename());
    if (!ALLOWED_TYPES.contains(ext.toLowerCase())) {
    throw new SecurityException("不支持的文件类型");
    }

    // 2. 文件头校验
    try {
    byte[] header = new byte[8];
    try (InputStream is = file.getInputStream()) {
    is.read(header);
    }

    byte[] expected = FILE_SIGNATURES.getOrDefault(ext.toLowerCase(), new byte[0]);
    if (expected.length > 0 &&
    !Arrays.equals(Arrays.copyOf(header, expected.length), expected)) {
    throw new SecurityException("文件内容与扩展名不匹配");
    }
    } catch (IOException e) {
    throw new RuntimeException("文件读取失败", e);
    }
    }
    }

    五、部署与测试

    1. 私有化部署方案

    Docker化部署配置:

    # 后端服务
    FROM openjdk:8-jdk-alpine
    VOLUME /tmp
    ARG JAR_FILE=target/doc-manager-*.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

    # 前端服务
    FROM nginx:alpine
    COPY dist /usr/share/nginx/html
    COPY nginx.conf /etc/nginx/conf.d/default.conf

    MySQL配置优化:

    # application.properties
    spring.datasource.url=jdbc:mysql://mysql:3306/doc_db?useSSL=false&serverTimezone=Asia/Shanghai
    spring.datasource.username=doc_admin
    spring.datasource.password=${DB_PASSWORD}
    spring.datasource.hikari.maximum-pool-size=10
    spring.datasource.hikari.connection-timeout=30000

    2. 测试用例设计

    核心功能测试:

    测试类型测试用例预期结果
    粘贴功能 从Word复制带图片和样式的内容粘贴 图片正确上传,样式保留
    导入功能 导入包含复杂表格的Excel文件 表格结构正确转换
    性能测试 导入50MB的PPT文件(含50张图片) 10分钟内完成,内存不溢出
    安全测试 上传恶意脚本文件 系统拒绝并记录安全日志
    兼容性测试 在IE11/Chrome/Firefox中粘贴Word内容 各浏览器表现一致

    六、后续优化方向

  • 多云存储适配:开发阿里云/华为云等对象存储适配器
  • 移动端优化:改进H5端的粘贴体验和图片上传流程
  • AI增强:集成OCR识别实现图片文字提取
  • 版本控制:增加文档历史版本管理功能
  • 协作编辑:实现多人实时协作编辑功能
  • 整个开发周期历时8周,通过深度定制wangEditor和精心设计后端处理流程,成功实现了客户要求的复杂文档处理功能。系统在内网环境稳定运行2个月后,已开始规划向公有云环境的迁移方案,并准备申请软件著作权登记。

    复制插件文件

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

    上传网络图片

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

    下载示例

    点击下载完整示例

    赞(0)
    未经允许不得转载:171主机测评 » 网页富文本编辑器如何解决WangEditor粘贴Word内容时的格式错乱问题?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址