Browse Source

✨ feat(WordMerge): 实现真实的Word文档合并功能

- 替换模拟合并实现为真实的Word文档合并功能
- 使用pizzip和jszip库处理文档压缩与内容提取
- 添加文件选择验证,防止空文件列表时执行合并
- 优化文档内容合并逻辑,确保XML结构完整

♻️ refactor(WordPreview): 优化文档合并逻辑

- 使用arrayBuffer替代text方式读取XML内容,避免编码问题
- 改进正则表达式,增强对不同格式Word文档的兼容性
- 添加XML声明和编码处理,确保文档格式正确
yourname 3 months ago
parent
commit
85496ab9e1
2 changed files with 78 additions and 17 deletions
  1. 62 11
      src/client/admin/pages/WordMerge.tsx
  2. 16 6
      src/client/home/pages/WordPreview.tsx

+ 62 - 11
src/client/admin/pages/WordMerge.tsx

@@ -83,20 +83,71 @@ const WordMergePage: React.FC = () => {
 
     setIsMerging(true);
     try {
-      // 这里使用简单的模拟合并,实际项目中需要调用后端API或使用前端库
-      // 由于Word文档合并需要复杂的处理,这里使用模拟方式
+      // 使用真实的Word文档合并功能
+      // 加载必要的库
+      const PizZip = (await import('pizzip')).default;
+      const JSZip = (await import('jszip')).default;
       
-      // 模拟合并过程
-      await new Promise(resolve => setTimeout(resolve, 2000));
+      if (files.length === 0) {
+        toast.error('请先选择要合并的Word文档');
+        return;
+      }
+      
+      // 使用第一个文档作为基础模板
+      const baseFile = files[0].file;
+      const baseArrayBuffer = await baseFile.arrayBuffer();
+      const baseZip = new PizZip(baseArrayBuffer);
+      
+      // 创建一个新的JSZip实例来合并内容
+      const mergedZip = new JSZip();
+      
+      // 加载基础文档
+      const baseDocx = await mergedZip.loadAsync(baseArrayBuffer);
+      
+      // 获取基础文档的word/document.xml内容,使用arrayBuffer避免编码问题
+      const baseXmlArrayBuffer = await baseDocx.file('word/document.xml').async('arraybuffer');
+      const textDecoder = new TextDecoder('utf-8');
+      let mergedContent = textDecoder.decode(baseXmlArrayBuffer);
+      
+      // 移除基础文档的结束标签,以便添加其他文档内容
+      mergedContent = mergedContent.replace(/<\/w:body>\s*<\/w:document>\s*$/, '');
+      
+      // 逐个添加其他文档的内容(从第二个文件开始)
+      for (let i = 1; i < files.length; i++) {
+        const file = files[i].file;
+        const fileArrayBuffer = await file.arrayBuffer();
+        const fileZip = await mergedZip.loadAsync(fileArrayBuffer);
+        const fileXmlArrayBuffer = await fileZip.file('word/document.xml').async('arraybuffer');
+        let fileContent = textDecoder.decode(fileXmlArrayBuffer);
+        
+        // 提取文档主体内容
+        const bodyMatch = fileContent.match(/<w:body(?:\s+[^>]*)?>([\s\S]*?)<\/w:body>/);
+        if (bodyMatch && bodyMatch[1]) {
+          mergedContent += bodyMatch[1];
+        }
+      }
+      
+      // 添加完整的结束标签,确保XML结构完整
+      mergedContent += '</w:body></w:document>';
+      
+      // 确保XML声明和编码正确
+      if (!mergedContent.startsWith('<?xml')) {
+        mergedContent = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + mergedContent;
+      }
+      
+      // 更新合并后的内容
+      baseDocx.file('word/document.xml', mergedContent);
       
-      // 创建一个模拟的合并后的Blob对象
-      const mergedContent = new Blob(
-        [new Uint8Array([0x50, 0x4B, 0x03, 0x04])], // 模拟DOCX文件头
-        { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
-      );
+      // 生成合并后的Word文档
+      const mergedDoc = await baseDocx.generateAsync({
+        type: 'blob',
+        mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+        compression: 'DEFLATE',
+        compressionOptions: { level: 6 }
+      });
       
-      setMergedFile(mergedContent);
-      toast.success('文档合并成功!');
+      setMergedFile(mergedDoc);
+      toast.success(`已成功合并 ${files.length} 个Word文档为一个文件`);
     } catch (error) {
       console.error('合并失败:', error);
       toast.error('文档合并失败,请重试');

+ 16 - 6
src/client/home/pages/WordPreview.tsx

@@ -809,28 +809,38 @@ export default function WordPreview() {
       // 加载基础文档
       const baseDocx = await mergedZip.loadAsync(await baseContent.arrayBuffer());
       
-      // 获取基础文档的word/document.xml内容
-      let mergedContent = await baseDocx.file('word/document.xml').async('text');
+      // 获取基础文档的word/document.xml内容,使用arrayBuffer避免编码问题
+      const baseXmlArrayBuffer = await baseDocx.file('word/document.xml').async('arraybuffer');
+      const textDecoder = new TextDecoder('utf-8');
+      let mergedContent = textDecoder.decode(baseXmlArrayBuffer);
       
       // 移除基础文档的结束标签,以便添加其他文档内容
-      mergedContent = mergedContent.replace(/<\/w:body><\/w:document>$/, '');
+      // 更健壮的正则表达式匹配Word文档的结束标签,处理可能的空白字符
+      mergedContent = mergedContent.replace(/<\/w:body>\s*<\/w:document>\s*$/, '');
       
       // 逐个添加其他文档的内容
       for (const file of filesToMerge) {
         const fileArrayBuffer = await file.content.arrayBuffer();
         const fileZip = await mergedZip.loadAsync(fileArrayBuffer);
-        let fileContent = await fileZip.file('word/document.xml').async('text');
+        const fileXmlArrayBuffer = await fileZip.file('word/document.xml').async('arraybuffer');
+        let fileContent = textDecoder.decode(fileXmlArrayBuffer);
         
         // 提取文档主体内容(去掉xml声明和文档标签)
-        const bodyMatch = fileContent.match(/<w:body[^>]*>([\s\S]*?)<\/w:body>/);
+        // 更健壮的正则表达式匹配body内容,处理命名空间和属性
+        const bodyMatch = fileContent.match(/<w:body(?:\s+[^>]*)?>([\s\S]*?)<\/w:body>/);
         if (bodyMatch && bodyMatch[1]) {
           mergedContent += bodyMatch[1];
         }
       }
       
-      // 添加结束标签
+      // 添加完整的结束标签,确保XML结构完整
       mergedContent += '</w:body></w:document>';
       
+      // 确保XML声明和编码正确
+      if (!mergedContent.startsWith('<?xml')) {
+        mergedContent = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + mergedContent;
+      }
+      
       // 更新合并后的内容
       baseDocx.file('word/document.xml', mergedContent);