Bläddra i källkod

fix: 修复Word软换行(Shift+Enter)丢失问题

问题:Word中的软换行(<w:br>)在解析时丢失,导致文本连成一行

修复:
1. 新增 extractRunText 方法,解析 Run 的 XML 结构提取换行符
2. XWPFRun.text() 不包含 <w:br>,改为遍历 CTR 内容
3. 前端 renderTextRun 将 \n 转换为 <br>
4. 前端纯文本渲染也添加换行符处理
何文松 4 veckor sedan
förälder
incheckning
dd3d7b682d

+ 53 - 1
backend/parse-service/src/main/java/com/lingyue/parse/service/WordStructuredExtractionService.java

@@ -436,7 +436,9 @@ public class WordStructuredExtractionService {
         
         List<TextRun> runs = new ArrayList<>();
         for (XWPFRun xwpfRun : xwpfRuns) {
-            String text = xwpfRun.text();
+            // 使用 toString() 而不是 text(),因为 toString() 会包含换行符
+            // 或者手动处理 Run 内容
+            String text = extractRunText(xwpfRun);
             if (text == null || text.isEmpty()) {
                 continue;
             }
@@ -573,10 +575,60 @@ public class WordStructuredExtractionService {
             if (runs != null) {
                 allRuns.addAll(runs);
             }
+            // 段落之间添加换行
+            if (!allRuns.isEmpty()) {
+                TextRun lastRun = allRuns.get(allRuns.size() - 1);
+                if (lastRun.getText() != null && !lastRun.getText().endsWith("\n")) {
+                    lastRun.setText(lastRun.getText() + "\n");
+                }
+            }
         }
         return allRuns.isEmpty() ? null : allRuns;
     }
     
+    /**
+     * 提取 Run 的文本内容,包含换行符
+     * XWPFRun.text() 不包含 <w:br> 换行,需要手动处理
+     */
+    private String extractRunText(XWPFRun xwpfRun) {
+        StringBuilder sb = new StringBuilder();
+        
+        try {
+            // 获取 Run 的 XML 结构
+            var ctr = xwpfRun.getCTR();
+            if (ctr == null) {
+                return xwpfRun.text();
+            }
+            
+            // 遍历 Run 内的所有元素
+            for (Object obj : ctr.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' ./*")) {
+                if (obj instanceof org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText) {
+                    // 普通文本
+                    org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText ctText = 
+                        (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText) obj;
+                    sb.append(ctText.getStringValue());
+                } else if (obj instanceof org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBr) {
+                    // 换行符 <w:br>
+                    sb.append("\n");
+                } else if (obj instanceof org.openxmlformats.schemas.wordprocessingml.x2006.main.CTEmpty) {
+                    // Tab 或其他空元素,检查元素名称
+                    // 暂时忽略
+                }
+            }
+        } catch (Exception e) {
+            // 如果 XML 解析失败,回退到简单方法
+            log.debug("提取 Run 文本时出错,使用简单方法: {}", e.getMessage());
+            return xwpfRun.text();
+        }
+        
+        // 如果结果为空,使用默认方法
+        if (sb.length() == 0) {
+            return xwpfRun.text();
+        }
+        
+        return sb.toString();
+    }
+    
     /**
      * 结构化提取结果
      */

+ 4 - 3
frontend/vue-demo/src/views/Editor.vue

@@ -531,8 +531,8 @@ function renderStructuredDocument(structuredDoc) {
  */
 function renderParagraphWithRuns(para) {
   if (!para.runs || para.runs.length === 0) {
-    // 没有 runs,使用纯文本
-    const content = escapeHtml(para.content || '')
+    // 没有 runs,使用纯文本(将换行符转为 <br>)
+    const content = escapeHtml(para.content || '').replace(/\n/g, '<br>')
     return wrapWithParagraphTag(content, para.type, para.style)
   }
   
@@ -547,7 +547,8 @@ function renderParagraphWithRuns(para) {
 function renderTextRun(run) {
   if (!run || !run.text) return ''
   
-  let text = escapeHtml(run.text)
+  // 转义 HTML 并将换行符转换为 <br>
+  let text = escapeHtml(run.text).replace(/\n/g, '<br>')
   const styles = []
   
   // 字体