Selaa lähdekoodia

feat: 增强Word排版还原能力

后端改进 (WordStructuredExtractionService):
1. 新增段前间距 (spacingBefore) 和段后间距 (spacingAfter) 提取
2. 新增右缩进 (indentRight) 和悬挂缩进 (indentHanging) 提取
3. 新增行距规则 (lineSpacingRule) 和精确行距值 (lineSpacingValue)
4. 新增列表属性 (listLevel, listId) 提取

前端改进 (Editor.vue):
1. wrapWithParagraphTag 支持更多样式:
   - 段前/段后间距 -> margin-top/margin-bottom
   - 右缩进 -> padding-right
   - 悬挂缩进 -> 负text-indent + 补偿margin-left
   - 行距规则区分: exact(固定值), atLeast(最小), auto(倍数)
   - 对齐方式增加 both -> justify 映射
2. 列表项改用 div.doc-list-item 实现,支持自定义样式
3. 新增列表项、块引用、代码块的CSS样式
何文松 1 kuukausi sitten
vanhempi
commit
0ddf512c8b

+ 65 - 3
backend/parse-service/src/main/java/com/lingyue/parse/service/WordStructuredExtractionService.java

@@ -319,19 +319,81 @@ public class WordStructuredExtractionService {
                 style.put("alignment", alignment.name().toLowerCase());
             }
             
-            // 缩进
+            // 缩进(twips,1/20 pt)
             int indentLeft = paragraph.getIndentationLeft();
             if (indentLeft > 0) {
                 style.put("indentLeft", indentLeft);
             }
+            
+            // 右缩进
+            int indentRight = paragraph.getIndentationRight();
+            if (indentRight > 0) {
+                style.put("indentRight", indentRight);
+            }
+            
+            // 首行缩进
             int indentFirstLine = paragraph.getIndentationFirstLine();
             if (indentFirstLine > 0) {
                 style.put("indentFirstLine", indentFirstLine);
             }
             
+            // 悬挂缩进
+            int indentHanging = paragraph.getIndentationHanging();
+            if (indentHanging > 0) {
+                style.put("indentHanging", indentHanging);
+            }
+            
+            // 段前间距(twips)
+            int spacingBefore = paragraph.getSpacingBefore();
+            if (spacingBefore > 0) {
+                style.put("spacingBefore", spacingBefore);
+            }
+            
+            // 段后间距(twips)
+            int spacingAfter = paragraph.getSpacingAfter();
+            if (spacingAfter > 0) {
+                style.put("spacingAfter", spacingAfter);
+            }
+            
             // 行距
-            if (paragraph.getSpacingBetween() > 0) {
-                style.put("lineSpacing", paragraph.getSpacingBetween());
+            double spacingBetween = paragraph.getSpacingBetween();
+            if (spacingBetween > 0) {
+                // getSpacingBetween 返回行距倍数(如 1.0, 1.5, 2.0)
+                style.put("lineSpacing", spacingBetween);
+            }
+            
+            // 尝试获取行距规则和精确行距值
+            try {
+                if (paragraph.getCTP().getPPr() != null && 
+                    paragraph.getCTP().getPPr().getSpacing() != null) {
+                    var spacing = paragraph.getCTP().getPPr().getSpacing();
+                    // 行距规则:auto(倍数), exact(固定), atLeast(最小)
+                    if (spacing.getLineRule() != null) {
+                        style.put("lineSpacingRule", spacing.getLineRule().toString());
+                    }
+                    // 精确行距值(twips)
+                    if (spacing.getLine() != null) {
+                        style.put("lineSpacingValue", spacing.getLine().intValue());
+                    }
+                }
+            } catch (Exception e) {
+                // 忽略
+            }
+            
+            // 列表属性
+            try {
+                if (paragraph.getCTP().getPPr() != null && 
+                    paragraph.getCTP().getPPr().getNumPr() != null) {
+                    var numPr = paragraph.getCTP().getPPr().getNumPr();
+                    if (numPr.getIlvl() != null) {
+                        style.put("listLevel", numPr.getIlvl().getVal().intValue());
+                    }
+                    if (numPr.getNumId() != null) {
+                        style.put("listId", numPr.getNumId().getVal().intValue());
+                    }
+                }
+            } catch (Exception e) {
+                // 忽略
             }
             
             // 字体(从第一个 run 获取)

+ 124 - 6
frontend/vue-demo/src/views/Editor.vue

@@ -644,17 +644,80 @@ function wrapWithParagraphTag(content, type, style) {
   // 段落样式
   const styleAttrs = []
   if (style) {
+    // 对齐方式
     if (style.alignment) {
-      styleAttrs.push(`text-align:${style.alignment}`)
+      const alignMap = {
+        'left': 'left',
+        'center': 'center',
+        'right': 'right',
+        'both': 'justify',  // 两端对齐
+        'justify': 'justify'
+      }
+      styleAttrs.push(`text-align:${alignMap[style.alignment] || style.alignment}`)
     }
+    
+    // 左缩进(twips -> pt,1 twip = 1/20 pt)
     if (style.indentLeft) {
       styleAttrs.push(`padding-left:${style.indentLeft / 20}pt`)
     }
+    
+    // 右缩进
+    if (style.indentRight) {
+      styleAttrs.push(`padding-right:${style.indentRight / 20}pt`)
+    }
+    
+    // 首行缩进
     if (style.indentFirstLine) {
       styleAttrs.push(`text-indent:${style.indentFirstLine / 20}pt`)
     }
+    
+    // 悬挂缩进(负的首行缩进 + 增加左边距)
+    if (style.indentHanging) {
+      const hangingPt = style.indentHanging / 20
+      styleAttrs.push(`text-indent:-${hangingPt}pt`)
+      // 如果没有左缩进,需要增加左边距来补偿
+      if (!style.indentLeft) {
+        styleAttrs.push(`margin-left:${hangingPt}pt`)
+      }
+    }
+    
+    // 段前间距
+    if (style.spacingBefore) {
+      styleAttrs.push(`margin-top:${style.spacingBefore / 20}pt`)
+    }
+    
+    // 段后间距
+    if (style.spacingAfter) {
+      styleAttrs.push(`margin-bottom:${style.spacingAfter / 20}pt`)
+    }
+    
+    // 行距处理
     if (style.lineSpacing) {
-      styleAttrs.push(`line-height:${style.lineSpacing}`)
+      // getSpacingBetween 返回的是倍数(如 1.0, 1.5, 2.0)
+      if (style.lineSpacing >= 1 && style.lineSpacing <= 5) {
+        styleAttrs.push(`line-height:${style.lineSpacing}`)
+      }
+    } else if (style.lineSpacingValue && style.lineSpacingRule) {
+      // 精确行距值处理
+      const rule = style.lineSpacingRule.toLowerCase()
+      if (rule === 'exact') {
+        // 固定行距(twips -> pt)
+        styleAttrs.push(`line-height:${style.lineSpacingValue / 20}pt`)
+      } else if (rule === 'atleast' || rule === 'at_least') {
+        // 最小行距
+        styleAttrs.push(`min-height:${style.lineSpacingValue / 20}pt`)
+      } else if (rule === 'auto') {
+        // 倍数行距(240 twips = 1 倍行距)
+        styleAttrs.push(`line-height:${style.lineSpacingValue / 240}`)
+      }
+    }
+    
+    // 字体信息(段落级别的默认字体)
+    if (style.fontFamily) {
+      styleAttrs.push(`font-family:${style.fontFamily}`)
+    }
+    if (style.fontSize) {
+      styleAttrs.push(`font-size:${style.fontSize}pt`)
     }
   }
   
@@ -671,9 +734,9 @@ function wrapWithParagraphTag(content, type, style) {
       return `<h2${styleAttr}>${content}</h2>`
     case 'bullet':
     case 'list_item':
-      return `<li${styleAttr}>${content}</li>`
+      return `<div class="doc-list-item bullet"${styleAttr}>${content}</div>`
     case 'ordered':
-      return `<li${styleAttr}>${content}</li>`
+      return `<div class="doc-list-item ordered"${styleAttr}>${content}</div>`
     case 'quote':
       return `<blockquote${styleAttr}>${content}</blockquote>`
     case 'code':
@@ -1202,8 +1265,8 @@ onUnmounted(() => {
     }
 
     :deep(p) {
-      margin-bottom: 16px;
-      line-height: 1.8;
+      margin-bottom: 12px;
+      line-height: 1.6;
     }
 
     :deep(ul) {
@@ -1214,6 +1277,61 @@ onUnmounted(() => {
         margin-bottom: 8px;
       }
     }
+    
+    // 列表项样式
+    :deep(.doc-list-item) {
+      position: relative;
+      margin-bottom: 8px;
+      line-height: 1.6;
+      
+      &.bullet {
+        padding-left: 1.5em;
+        &::before {
+          content: '•';
+          position: absolute;
+          left: 0;
+        }
+      }
+      
+      &.ordered {
+        padding-left: 2em;
+        counter-increment: doc-list;
+        &::before {
+          content: counter(doc-list) '.';
+          position: absolute;
+          left: 0;
+        }
+      }
+    }
+    
+    // 重置列表计数器
+    :deep(p + .doc-list-item.ordered:first-of-type),
+    :deep(.doc-list-item.bullet + .doc-list-item.ordered) {
+      counter-reset: doc-list;
+    }
+    
+    // 块引用样式
+    :deep(blockquote) {
+      margin: 16px 0;
+      padding: 12px 20px;
+      border-left: 4px solid #ddd;
+      background: #f9f9f9;
+      color: #666;
+    }
+    
+    // 代码块样式
+    :deep(pre) {
+      margin: 16px 0;
+      padding: 16px;
+      background: #f5f5f5;
+      border-radius: 4px;
+      overflow-x: auto;
+      
+      code {
+        font-family: 'Consolas', 'Monaco', monospace;
+        font-size: 13px;
+      }
+    }
   }
 }