企业网站后台管理系统富文本编辑器功能扩展开发记录

一、需求分析与技术选型

作为新疆某软件公司的前端工程师,最近接到客户需求:在企业网站后台管理系统的文章发布模块中增加Word粘贴、Word文档导入和微信公众号内容粘贴功能。经过详细分析,需求可拆解为:

  1. Word粘贴功能:支持从Word复制内容直接粘贴到UEditor,保留样式(表格、字体、颜色等),图片自动上传至服务器(二进制存储)
  2. 文档导入功能:支持Word/Excel/PPT/PDF导入,保留图片和样式
  3. 微信公众号粘贴:优化微信内容粘贴体验,保留基本格式

技术选型评估

  1. 富文本编辑器:现有UEditor(百度开源)支持较好,但原生功能有限
  2. Word处理库
    • Mammoth.js:轻量级,专注Word文档转换,但功能较基础
    • docx-preview:纯前端方案,适合简单需求
    • Pandoc:全功能文档转换,但需要后端集成
    • Apache POI(后端):Java生态,适合SpringBoot项目
  3. Office文件处理
    • Aspose.Words:商业库,功能强大但成本高
    • Apache POI + docx4j:开源组合,功能全面
  4. PDF处理
    • PDF.js:Mozilla开源库,适合前端预览
    • Apache PDFBox:后端Java处理

最终选择方案:

  • 前端:UEditor + 自定义插件 + Mammoth.js(基础转换)
  • 后端:SpringBoot集成Apache POI + Aspose.Cells(Excel处理) + OpenPDF(PDF处理)
  • 存储:阿里云OSS(初期),设计可迁移至多云接口

二、开发过程记录

1. 前端实现(Vue2 + UEditor)

1.1 安装与配置UEditor
npm install ueditor --save
# 或使用CDN引入

配置UEditor双编辑器实例(主编辑器+导入预览):

// src/plugins/UEditor.js
import Vue from 'vue'
import 'ueditor/dist/ueditor.config.js'
import 'ueditor/dist/ueditor.min.js'
import 'ueditor/dist/lang/zh-cn/zh-cn.js'

const UEditor = {
  install(Vue, options) {
    Vue.prototype.$getUEditorInstance = function(id, config) {
      return UE.getEditor(id, {
        serverUrl: '/api/ueditor/upload', // 后端接口
        toolbars: [
          // 自定义工具栏
          ['source', 'undo', 'redo', 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 
           'removeformat', 'formatmatch', 'autotypeset', 'pasteplain', '|', 
           'customWordPaste', 'customDocImport'] // 自定义按钮
        ],
        ...config
      })
    }
  }
}

Vue.use(UEditor)
1.2 开发Word粘贴插件

创建自定义按钮插件:

// src/plugins/ueditor/word-paste-plugin.js
UE.registerUI('customWordPaste', function(editor, uiName) {
  const btn = new UE.ui.Button({
    name: uiName,
    title: 'Word粘贴',
    cssRules: 'background-position: -726px -40px;',
    onclick: function() {
      // 提示用户使用Ctrl+V粘贴
      editor.execCommand('pasteplain')
      
      // 监听粘贴事件
      editor.addListener('afterPaste', function() {
        // 获取粘贴的HTML
        const html = editor.getContent()
        
        // 使用Mammoth提取图片并上传
        processWordContent(html, editor)
      })
    }
  })
  
  editor.addListener('ready', function() {
    editor.registerCommand(uiName, {
      execCommand: function() {
        alert('请从Word复制内容后直接粘贴')
      }
    })
  })
  
  return btn
}, 10)

// 处理Word内容函数
async function processWordContent(html, editor) {
  // 提取所有img标签(Word粘贴通常使用base64)
  const imgRegex = /]+src="data:image\/([^;]+);base64,([^"]+)"[^>]*>/g
  let match
  const promises = []
  
  while ((match = imgRegex.exec(html)) !== null) {
    const mimeType = match[1]
    const base64Data = match[2]
    promises.push(uploadImage(base64Data, mimeType, editor))
  }
  
  // 等待所有图片上传完成
  await Promise.all(promises)
  
  // 可选:使用Mammoth进一步清理HTML结构
  // const cleanedHtml = mammoth.extractRawText({value: html}).value
  // editor.setContent(cleanedHtml)
}

// 图片上传函数
async function uploadImage(base64, mimeType, editor) {
  try {
    const binaryData = atob(base64)
    const array = new Uint8Array(binaryData.length)
    for (let i = 0; i < binaryData.length; i++) {
      array[i] = binaryData.charCodeAt(i)
    }
    
    const blob = new Blob([array], { type: `image/${mimeType}` })
    const formData = new FormData()
    formData.append('file', blob, `word-image-${Date.now()}.${mimeType}`)
    
    const response = await axios.post('/api/upload/word-image', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    })
    
    if (response.data.success) {
      const imgUrl = response.data.url
      // 替换编辑器中的base64图片为URL
      const newHtml = editor.getContent().replace(
        new RegExp(`data:image/${mimeType};base64,${base64}`, 'g'), 
        imgUrl
      )
      editor.setContent(newHtml)
    }
  } catch (error) {
    console.error('图片上传失败:', error)
  }
}
1.3 文档导入功能实现

创建导入对话框组件:





export default {
  data() {
    return {
      dialogVisible: false,
      previewHtml: '',
      fileInfo: null
    }
  },
  methods: {
    beforeUpload(file) {
      const isOffice = [
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'application/vnd.ms-powerpoint',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'application/pdf'
      ].includes(file.type)
      
      if (!isOffice) {
        this.$message.error('只能上传Office文档或PDF文件!')
        return false
      }
      return true
    },
    
    handleImportSuccess(response, file) {
      if (response.success) {
        this.previewHtml = response.html
        this.fileInfo = response.fileInfo
      } else {
        this.$message.error(response.message || '导入失败')
      }
    },
    
    confirmImport() {
      if (this.fileInfo && this.previewHtml) {
        this.$emit('import-confirmed', {
          html: this.previewHtml,
          fileInfo: this.fileInfo
        })
        this.dialogVisible = false
      }
    }
  }
}



.preview-area {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #eee;
  max-height: 400px;
  overflow-y: auto;
}

2. 后端实现(SpringBoot)

2.1 配置文件上传
// application.yml
file:
  upload:
    path: /tmp/uploads/
    word-images: /tmp/word-images/
    doc-temp: /tmp/doc-temp/
  aliyun:
    oss:
      endpoint: your-oss-endpoint
      accessKeyId: your-access-key
      accessKeySecret: your-secret
      bucketName: your-bucket
2.2 图片上传控制器
// src/main/java/com/example/controller/UploadController.java
@RestController
@RequestMapping("/api/upload")
public class UploadController {
    
    @Value("${file.upload.word-images}")
    private String wordImagePath;
    
    @Autowired
    private AliyunOssService aliyunOssService;
    
    @PostMapping("/word-image")
    public ResponseEntity> uploadWordImage(@RequestParam("file") MultipartFile file) {
        Map result = new HashMap<>();
        
        try {
            // 保存临时文件
            String originalFilename = file.getOriginalFilename();
            String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
            String newFilename = "word-img-" + System.currentTimeMillis() + fileExt;
            Path path = Paths.get(wordImagePath, newFilename);
            Files.write(path, file.getBytes());
            
            // 上传到OSS
            String ossUrl = aliyunOssService.uploadFile(path.toFile());
            
            result.put("success", true);
            result.put("url", ossUrl);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "图片上传失败: " + e.getMessage());
            return ResponseEntity.status(500).body(result);
        }
    }
}
2.3 文档导入服务
// src/main/java/com/example/service/DocImportService.java
@Service
public class DocImportService {
    
    @Value("${file.upload.doc-temp}")
    private String docTempPath;
    
    public Map importDocument(MultipartFile file) throws IOException {
        Map result = new HashMap<>();
        String originalFilename = file.getOriginalFilename();
        String fileExt = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
        String tempFilePath = docTempPath + "doc-import-" + System.currentTimeMillis() + fileExt;
        
        // 保存临时文件
        Files.write(Paths.get(tempFilePath), file.getBytes());
        
        try {
            String htmlContent = "";
            Map fileInfo = new HashMap<>();
            fileInfo.put("originalName", originalFilename);
            fileInfo.put("size", String.valueOf(file.getSize()));
            fileInfo.put("type", fileExt);
            
            switch (fileExt) {
                case ".doc":
                case ".docx":
                    htmlContent = convertWordToHtml(tempFilePath);
                    break;
                case ".xls":
                case ".xlsx":
                    htmlContent = convertExcelToHtml(tempFilePath);
                    break;
                case ".ppt":
                case ".pptx":
                    htmlContent = convertPptToHtml(tempFilePath);
                    break;
                case ".pdf":
                    htmlContent = convertPdfToHtml(tempFilePath);
                    break;
                default:
                    throw new IllegalArgumentException("不支持的文件类型: " + fileExt);
            }
            
            // 处理HTML中的图片(如果有)
            htmlContent = processHtmlImages(htmlContent);
            
            result.put("success", true);
            result.put("html", htmlContent);
            result.put("fileInfo", fileInfo);
        } finally {
            // 删除临时文件
            Files.deleteIfExists(Paths.get(tempFilePath));
        }
        
        return result;
    }
    
    private String convertWordToHtml(String filePath) throws IOException {
        // 使用Apache POI或Aspose.Words转换
        // 简化示例,实际应使用更完整的转换逻辑
        try (InputStream is = new FileInputStream(filePath);
             XWPFDocument document = new XWPFDocument(is)) {
            
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            XWPFHTMLConverter htmlConverter = new XWPFHTMLConverter(
                OutlookMessageParser.getInstance(), 
                document, 
                out, 
                new HTMLSettings()
            );
            htmlConverter.processDocument();
            return out.toString("UTF-8");
        }
    }
    
    // 其他转换方法类似...
    
    private String processHtmlImages(String html) {
        // 提取HTML中的base64图片并上传到OSS
        // 返回替换后的HTML
        // 实际实现应与前端图片上传逻辑一致
        return html; // 简化示例
    }
}
2.4 控制器端点
// src/main/java/com/example/controller/DocImportController.java
@RestController
@RequestMapping("/api/upload")
public class DocImportController {
    
    @Autowired
    private DocImportService docImportService;
    
    @PostMapping("/doc-import")
    public ResponseEntity> importDocument(@RequestParam("file") MultipartFile file) {
        try {
            Map result = docImportService.importDocument(file);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            Map error = new HashMap<>();
            error.put("success", false);
            error.put("message", "文档导入失败: " + e.getMessage());
            return ResponseEntity.status(500).body(error);
        }
    }
}

三、综合评估与优化

1. 性能优化

  1. 图片上传

    • 使用Web Worker处理大图片上传
    • 实现分片上传大文件
    • 添加上传进度显示
  2. 文档转换

    • 对于大文档,使用异步处理+轮询结果
    • 添加转换队列避免并发过高
  3. 缓存机制

    • 缓存常用文档转换结果
    • 实现增量更新机制

2. 安全性考虑

  1. 文件类型验证

    • 严格检查文件MIME类型
    • 限制文件大小
  2. XSS防护

    • 对导入的HTML进行净化
    • 使用DOMPurify等库处理用户内容
  3. 权限控制

    • 添加JWT认证
    • 实现细粒度权限管理

3. 跨云兼容设计

// 存储服务接口
public interface CloudStorageService {
    String uploadFile(File file);
    String getFileUrl(String key);
    // 其他方法...
}

// 阿里云实现
@Service("aliyunOssService")
public class AliyunOssService implements CloudStorageService {
    // 实现阿里云OSS上传
}

// 华为云实现
@Service("huaweiObsService")
public class HuaweiObsService implements CloudStorageService {
    // 实现华为云OBS上传
}

// 工厂模式选择存储服务
@Component
public class StorageServiceFactory {
    @Autowired
    private AliyunOssService aliyunOssService;
    @Autowired
    private HuaweiObsService huaweiObsService;
    // 其他云服务...
    
    public CloudStorageService getStorageService(String provider) {
        switch (provider.toLowerCase()) {
            case "aliyun": return aliyunOssService;
            case "huawei": return huaweiObsService;
            // 其他case...
            default: throw new IllegalArgumentException("不支持的云存储提供商");
        }
    }
}

四、部署与测试

1. 部署流程

  1. 前端构建

    npm run build
    
  2. 后端打包

    mvn clean package
    
  3. 容器化部署(可选):

    # Dockerfile示例
    FROM openjdk:8-jdk-alpine
    VOLUME /tmp
    ARG JAR_FILE=target/*.jar
    COPY ${JAR_FILE} app.jar
    ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
    

2. 测试用例

  1. Word粘贴测试

    • 复制包含表格、图片、不同字体的Word内容
    • 验证图片是否上传成功
    • 检查样式保留情况
  2. 文档导入测试

    • 测试各种Office文档和PDF
    • 验证复杂格式(嵌套表格、图表等)
    • 检查大文件处理能力
  3. 兼容性测试

    • 不同浏览器测试
    • 移动端适配测试

五、总结与展望

本次开发成功实现了企业网站后台管理系统的富文本编辑器扩展功能,包括:

  1. Word粘贴功能:支持复杂格式保留和图片自动上传
  2. 文档导入功能:支持多种Office文档和PDF导入
  3. 云存储兼容:设计支持多云存储提供商

未来改进方向:

  1. 性能优化:实现更高效的大文档处理
  2. 协作编辑:添加实时协作功能
  3. AI辅助:集成AI内容生成和优化功能
  4. 移动端适配:完善移动端编辑体验

通过本次开发,我们积累了丰富的富文本编辑器扩展经验,为后续类似项目打下了坚实基础。

在工具栏中增加插件按钮

//工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义
    toolbars: [
      [
        "fullscreen",
        "source",
        "|",
        "zycapture",
        "|",
        "wordpaster","importwordtoimg","netpaster","wordimport","excelimport","pptimport","pdfimport",
        "|",
        "importword","exportword","importpdf"
      ]
    ]

初始化控件

image

        var pos = window.location.href.lastIndexOf("/");
        var api = [
            window.location.href.substr(0, pos + 1),
            "asp/upload.asp"
        ].join("");
        WordPaster.getInstance({
			//上传接口:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
            PostUrl: api,
			//为图片地址增加域名:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
            ImageUrl: "",
            //设置文件字段名称:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
            FileFieldName: "file",
            //提取图片地址:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
            ImageMatch: ''			
        });//加载控件

注意

如果接口字段名称不是file,请配置FileFieldName。ueditor接口中使用的upfile字段
image
点击查看详细教程

配置ImageMatch

匹配图片地址,如果服务器返回的是JSON则需要通过正则匹配

ImageMatch: '',

点击参考链接

配置ImageUrl

为图片地址增加域名,如果服务器返回的图片地址是相对路径,可通过此属性添加自定义域名。

ImageUrl: "",

点击查看详细教程

配置SESSION

如果接口有权限验证(登陆验证,SESSION验证),请配置COOKIE。或取消权限验证。
参考:http://www.ncmem.com/doc/view.aspx?id=8602DDBF62374D189725BF17367125F3

效果

编辑器界面

image

导入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

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐