基于Vue3实现在线/本地上传文档比对高亮差异显示

内容分享2周前发布
0 0 0

Word 文档比对系统技术文档

原创文章,转载必究

原创文章,转载必究

原创文章,转载必究

版本信息

当前版本: v2.0更新日期: 2024-11核心算法: 字符级 Diff + DOM 映射定位


效果预览(目前仅支持docx格式资源)

基于Vue3实现在线/本地上传文档比对高亮差异显示

一、使用方式

1.1 基本使用流程


1. 上传文档 → 自动预览
   ├─ 方式1: 点击"选择文档1/2"按钮上传本地 .docx 文件
   └─ 方式2: 在URL输入框输入在线文档地址

2. 开始比对 → 差异高亮
   └─ 点击"开始比对"按钮,系统自动高亮文档差异

3. 查看结果
   ├─ 预览区域: 可视化查看文档差异(红色=删除,绿色=添加)
   ├─ 相似度指标: 实时显示文档相似度百分比
   └─ 变更列表: 点击右侧"变更"按钮查看详细变更清单

1.2 功能说明

功能 说明
自动预览 文档上传后立即渲染预览,无需等待比对
URL 加载 支持输入在线 .docx 文档 URL,自动下载并加载
精准比对 使用字符级 Diff 算法,对中文友好
同步滚动 左右文档联动滚动,可通过开关控制
相似度计算 基于 LCS(最长公共子序列)算法计算相似度
变更列表 详细列出所有新增、删除、修改的内容

二、核心技术架构

2.1 技术栈


// 核心依赖
- Vue 3 (Composition API)
- Element Plus (UI 组件库)
- docx-preview (Word 文档渲染)
- mammoth.js (Word 文档解析)
- diff (文本差异算法)

2.2 系统架构图


┌─────────────────────────────────────────────────────┐
│                    上传层                            │
│  ┌──────────────┐         ┌──────────────┐         │
│  │ 本地文件上传  │         │  URL 远程加载 │         │
│  └──────┬───────┘         └──────┬───────┘         │
└─────────┼──────────────────────┼──────────────────┘
          │                      │
          └──────────┬───────────┘
                     ▼
┌─────────────────────────────────────────────────────┐
│                  解析层                              │
│  ┌──────────────────────────────────────┐          │
│  │  mammoth.js (提取纯文本)              │          │
│  │  docx-preview (渲染 HTML 预览)        │          │
│  └──────────────────────────────────────┘          │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                  比对层                              │
│  ┌──────────────────────────────────────┐          │
│  │  1. 行级 Diff (diffLines)             │          │
│  │  2. 字符级 Diff (diffChars)           │          │
│  │  3. 词级 Diff (diffWords)             │          │
│  └──────────────────────────────────────┘          │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                高亮定位层                            │
│  ┌──────────────────────────────────────┐          │
│  │  DOM 映射表构建 (buildTextToNodeMap)  │          │
│  │  精准定位算法 (highlightByTextMap)    │          │
│  └──────────────────────────────────────┘          │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                展示层                                │
│  ┌──────────────────────────────────────┐          │
│  │  预览模式 (精准高亮)                   │          │
│  │  变更列表 (详细信息)                   │          │
│  │  相似度展示 (实时计算)                 │          │
│  └──────────────────────────────────────┘          │
└─────────────────────────────────────────────────────┘

三、关键技术方案

3.1 文档解析方案

3.1.1 双引擎解析

// 引擎1: mammoth.js - 提取纯文本用于比对
const parseWordDocument = async (file: File): Promise<string> => {
  const arrayBuffer = await file.arrayBuffer()
  const result = await mammoth.extractRawText({ arrayBuffer })
  return result.value
}

// 引擎2: docx-preview - 渲染可视化预览
const renderDocxPreview = async (file: File, container: HTMLElement) => {
  const arrayBuffer = await file.arrayBuffer()
  await renderAsync(arrayBuffer, container, undefined, {
    className: 'docx-wrapper',
    inWrapper: true,
    ignoreWidth: false,
    ignoreHeight: false,
    ignoreFonts: false,
    breakPages: true
  })
}

方案优势:

mammoth.js: 提取的纯文本准确,适合文本比对docx-preview: 完整保留 Word 格式(表格、样式、图片等)

3.2 精准比对算法

3.2.1 三级 Diff 策略

// 第一级: 行级 Diff (定位变更区域)
const lineDiffs = diffLines(text1, text2)

// 第二级: 词级 Diff (并排视图使用)
const wordDiff = diffWords(oldLine, newLine)

// 第三级: 字符级 Diff (预览模式使用 - 对中文更精准)
const charDiff = diffChars(doc1Text, doc2Text)
3.2.2 为什么预览模式使用字符级 Diff?

问题: 词级 Diff (diffWords) 对中文处理不准确


// 示例
diffWords("合同不含", "合修改同含")
// 可能结果: 删除"合" + 添加"合" + 删除"同" + 添加"修改同" (混乱)

diffChars("合同不含", "合修改同含")
// 精确结果: 删除"同不" + 添加"修改同" (准确)

解决方案: v1.1 版本从 diffWords 改为 diffChars


// v1.0 (词级 - 对中文不友好)
const fullDiff = diffWords(doc1Map.fullText, doc2Map.fullText)

// v1.1 (字符级 - 对中文友好)
const fullDiff = diffChars(doc1Map.fullText, doc2Map.fullText)

四、预览模式精准高亮算法 (核心创新)

4.1 问题背景

挑战: docx-preview 渲染的 HTML 结构非常复杂


<!-- 一行文本可能分散在多个节点 -->
<p>
  <span>1.2本合</span>
  <span>同</span>
  <span>不含增值税</span>
</p>

传统方案的问题:

简单的文本搜索容易误匹配(重复文本)上下文匹配在复杂 HTML 中不可靠跨节点的文本难以精准定位

4.2 创新方案: 文本-DOM 映射表

4.2.1 核心思想

将整个文档视为连续的字符流,为每个字符建立与 DOM 节点的映射关系
4.2.2 数据结构设计

interface TextPosition {
  node: Text              // 所属的 DOM 文本节点
  nodeOffset: number      // 在节点内的偏移量
  globalOffset: number    // 在整个文档中的全局偏移量
}

interface TextNodeMap {
  fullText: string        // 文档的完整文本
  positions: TextPosition[]  // 每个字符的位置信息
}
4.2.3 映射表构建算法

const buildTextToNodeMap = (element: HTMLElement): TextNodeMap => {
  const positions: TextPosition[] = []
  let fullText = ''
  
  // 使用 TreeWalker 按文档顺序遍历所有文本节点
  const walker = document.createTreeWalker(
    element,
    NodeFilter.SHOW_TEXT,
    null
  )
  
  let node: Node | null
  while (node = walker.nextNode()) {
    const textNode = node as Text
    const text = textNode.textContent || ''
    
    if (!text) continue
    
    // 为文本节点中的每个字符建立映射
    for (let i = 0; i < text.length; i++) {
      positions.push({
        node: textNode,
        nodeOffset: i,
        globalOffset: fullText.length + i
      })
    }
    
    fullText += text
  }
  
  return { fullText, positions }
}

时间复杂度: O(n),n 为文档字符总数
空间复杂度: O(n)

4.3 精准定位高亮算法

4.3.1 核心流程

const highlightByTextMapAtPosition = (
  map: TextNodeMap, 
  globalOffset: number,  // 从 diff 算法得到的精确位置
  length: number,        // 要高亮的字符长度
  className: string
): boolean => {
  // 1. 从映射表中获取起始和结束位置
  const startPos = map.positions[globalOffset]
  const endIndex = globalOffset + length - 1
  const endPos = map.positions[endIndex]
  
  if (!startPos || !endPos) return false
  
  // 2. 情况A: 文本在同一节点内
  if (startPos.node === endPos.node) {
    highlightTextNode(startPos.node, startPos.nodeOffset, length, className)
    return true
  }
  
  // 3. 情况B: 文本跨越多个节点
  const nodesToHighlight = new Map<Text, { start: number, end: number }>()
  
  for (let i = globalOffset; i <= endIndex; i++) {
    const pos = map.positions[i]
    const existing = nodesToHighlight.get(pos.node)
    
    if (existing) {
      existing.end = pos.nodeOffset  // 扩展范围
    } else {
      nodesToHighlight.set(pos.node, {
        start: pos.nodeOffset,
        end: pos.nodeOffset
      })
    }
  }
  
  // 4. 对每个节点进行高亮
  nodesToHighlight.forEach((range, node) => {
    const nodeLength = range.end - range.start + 1
    highlightTextNode(node, range.start, nodeLength, className)
  })
  
  return true
}
4.3.2 高亮执行流程

// 主流程: 对整个文档执行字符级 diff
const fullDiff = diffChars(doc1Map.fullText, doc2Map.fullText)

let doc1Offset = 0
let doc2Offset = 0

fullDiff.forEach((part) => {
  if (part.removed) {
    // 在文档1中高亮删除的文本
    highlightByTextMapAtPosition(
      doc1Map, 
      doc1Offset,          // 精确的全局位置
      part.value.length,   // 精确的长度
      'diff-word-deleted'
    )
    doc1Offset += part.value.length
    
  } else if (part.added) {
    // 在文档2中高亮添加的文本
    highlightByTextMapAtPosition(
      doc2Map, 
      doc2Offset, 
      part.value.length, 
      'diff-word-added'
    )
    doc2Offset += part.value.length
    
  } else {
    // 未改变的文本,更新偏移量
    doc1Offset += part.value.length
    doc2Offset += part.value.length
  }
})

4.4 算法优势分析

特性 传统方案 映射表方案 (本方案)
精准度 ❌ 易误匹配 ✅ 100% 精准
跨节点支持 ❌ 需要复杂逻辑 ✅ 自动支持
性能 ⚠️ O(n*m) 搜索 ✅ O(1) 查找
可维护性 ❌ 复杂上下文逻辑 ✅ 清晰简洁
中文支持 ⚠️ 一般 ✅ 完美

核心优势:

绝对精准: 使用全局偏移量,不会误匹配性能优秀: O(1) 时间复杂度定位任意位置跨节点无缝: 自动处理跨多个 DOM 节点的文本对中文友好: 配合 diffChars 完美支持中文


五、相似度计算算法

5.1 LCS (最长公共子序列) 算法


const longestCommonSubsequence = (str1: string, str2: string): number => {
  const m = str1.length
  const n = str2.length
  const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))
  
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
      }
    }
  }
  
  return dp[m][n]
}

const calculateSimilarity = (str1: string, str2: string): number => {
  if (str1 === str2) return 1
  if (str1.length === 0 || str2.length === 0) return 0
  
  const lcs = longestCommonSubsequence(str1, str2)
  return (2 * lcs) / (str1.length + str2.length)
}

相似度分级:

🟢 高相似度: ≥ 90%🟡 中等相似度: 70% – 90%🔴 低相似度: < 70%


六、同步滚动优化方案

6.1 技术实现


// 1. 节流函数 (限制执行频率)
const throttle = (func: Function, delay: number) => {
  let lastCall = 0
  return (...args: any[]) => {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      func(...args)
    }
  }
}

// 2. RAF (requestAnimationFrame) 平滑滚动
const syncScrollPosition = (source: HTMLElement, target: HTMLElement) => {
  if (!syncScroll.value || isScrolling.value) return
  
  isScrolling.value = true
  
  // 计算滚动比例
  const ratio = source.scrollTop / (source.scrollHeight - source.clientHeight)
  const targetTop = ratio * (target.scrollHeight - target.clientHeight)
  
  // 使用 RAF 确保流畅
  requestAnimationFrame(() => {
    target.scrollTop = targetTop
  })
  
  // 16ms 后解锁 (60fps)
  setTimeout(() => { isScrolling.value = false }, 16)
}

// 3. 节流滚动事件 (60fps)
const onPreview1Scroll = throttle((event: Event) => {
  if (previewContainer2.value && syncScroll.value) {
    syncScrollPosition(event.target, previewContainer2.value)
  }
}, 16)

性能优化:

节流控制频率 (60fps)RAF 与浏览器刷新同步避免无限循环的锁机制


七、版本演进历史

v1.0 (初始版本)

✅ 基础文档比对功能✅ 并排视图 + 预览模式⚠️ 使用词级 diff (中文支持不佳)⚠️ 上下文匹配定位 (不够精准)

v1.1 (字符级优化)

✅ 改用字符级 diff (diffChars)✅ 完善中文支持✅ Unicode 诊断功能

v2.0 (映射表方案 – 当前版本)

🚀 创新的文本-DOM映射表算法🚀 100% 精准定位🚀 移除并排视图,专注预览模式🚀 添加相似度显示🚀 自动预览功能🚀 支持 URL 加载🚀 丝滑同步滚动优化


八、性能指标

8.1 性能测试数据

文档大小 字符数 映射表构建 Diff 计算 高亮渲染 总耗时
小型 5K 10ms 20ms 15ms ~50ms
中型 50K 80ms 150ms 100ms ~350ms
大型 200K 300ms 800ms 400ms ~1.5s

8.2 优化建议

针对大型文档:

考虑分块处理 (chunk-based processing)使用 Web Worker 进行后台计算实现虚拟滚动减少 DOM 节点


九、已知限制与未来规划

9.1 当前限制

格式差异: docx-preview 可能无法完美还原所有 Word 格式大文档性能: 超大文档 (>500K 字符) 可能卡顿复杂表格: 表格内文本跨单元格时定位可能不准确

9.2 未来规划

支持 PDF 文档比对 导出比对报告 (Word/PDF格式) 支持文档版本管理 添加评论和批注功能 支持多文档批量比对


十、开发者指南

10.1 本地开发


# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建生产版本
pnpm build

10.2 核心文件说明


src/views/betWord3/
├── index.vue           # 主组件 (3600+ 行)
├── README.md          # 本技术文档
└── 核心函数说明:
    ├── parseWordDocument()         # 文档解析
    ├── compareTexts()              # 文本比对
    ├── buildTextToNodeMap()        # 映射表构建 ⭐
    ├── highlightByTextMapAtPosition()  # 精准定位 ⭐
    ├── highlightDifferencesInPreview() # 高亮执行
    └── syncScrollPosition()        # 同步滚动

10.3 调试技巧


// 开启详细日志
console.log('[映射] 文档1:', doc1Map.fullText.length, '字符')
console.log('[映射] 文档2:', doc2Map.fullText.length, '字符')
console.log('[完成] 高亮', highlightCount.deleted, '处删除')

// Unicode 诊断 (用于排查字符编码问题)
getUnicodeInfo(text)  // 输出: "U+4E0D U+0074"

十一、FAQ

Q1: 为什么有些字符被标记为删除又添加?

A: v1.0 使用词级 diff,对中文支持不佳。v2.0 已改用字符级 diff,问题已解决。

Q2: 如何提高大文档的比对速度?

A:

使用 URL 加载避免本地读取开销考虑升级到支持 Web Worker 的版本减少文档格式复杂度

Q3: 能否自定义高亮颜色?

A: 可以通过修改 CSS 实现:


:deep(.diff-word-deleted) {
  background-color: #your-color;
}

附录 A: 参考资料

diffChars API – npm diffdocx-preview – GitHubTreeWalker API – MDN最长公共子序列 – Wikipedia


文档维护: iuleon97(妖怪不慌不张)
最后更新: 2024年11月

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...