@yuwenovo/ocr-result-view
v2.2.2
Published
Vue 3 OCR document comparison result viewer with virtualized Canvas rendering
Readme
@yuwenovo/ocr-result-view
Vue 3 OCR 文档比对结果查看器。传入两份文档的页面图片和差异数据,即可展示双栏预览、差异标注、连线、差异列表和分页导航。
组件负责展示与交互;OCR 识别、文档比对、接口请求和审阅保存由接入项目负责。默认只读,无需 Vue Router、Pinia、Axios 或 Tailwind。
预览区提供默认开启的“同步滚动”开关。开启时以最近滚动的一侧为基准(初始为标准文档),按可滚动距离的百分比同步另一侧;滚动任意一侧或使用分页按钮均可联动。两份文档页数不同时仍按整体进度同步,不表示内容逐段对应。点击差异仍分别定位两侧的实际标注,关闭开关后恢复独立滚动。
1. 安装
在 Vue 3.5+ 项目中安装:
pnpm add @yuwenovo/ocr-result-view2. 最小接入示例
将两张文档页面图片放到接入项目的 public/previews/source-1.png 和 public/previews/target-1.png,然后创建以下 Vue 页面。示例中的尺寸、文本和标注坐标需替换为实际结果。
<script setup lang="ts">
import { OcrResultView, type DiffDetail, type PageMeta } from '@yuwenovo/ocr-result-view'
import '@yuwenovo/ocr-result-view/style.css'
const sourcePages: PageMeta[] = [
{ imageUrl: '/previews/source-1.png', width: 1200, height: 1600 },
]
const targetPages: PageMeta[] = [
{ imageUrl: '/previews/target-1.png', width: 1200, height: 1600 },
]
const diffDetail: DiffDetail = [
{
diffId: 'diff-1',
type: 'modify',
ignored: false,
delChars: '原文',
insChars: '新文',
sourceText: '原文',
targetText: '新文',
sourceLine: 0,
targetLine: 0,
sourceBox: [100, 100, 180, 130],
targetBox: [100, 100, 180, 130],
srcPage: 0,
tgtPage: 0,
},
]
</script>
<template>
<div style="height: 720px">
<OcrResultView
:diff-detail="diffDetail"
:source-pages="sourcePages"
:target-pages="targetPages"
source-filename="原文件.pdf"
target-filename="比对文件.pdf"
/>
</div>
</template>接入时必须导入 style.css,并给容器设置明确高度。组件高度为 100%。
带自定义导航按钮的示例见 examples/Basic.vue。它接收一个 result 属性,内容为本组件的 Props。
3. 准备数据
三个必填属性是 sourcePages、targetPages 和 diffDetail。无差异时传 diffDetail: [],仍然可以预览文档。
页面图片
每页使用一个 PageMeta 对象,数组顺序即页码顺序:
interface PageMeta {
imageUrl: string
width?: number
height?: number
}imageUrl 必须是浏览器可以访问的图片地址,不是 PDF 地址。width 和 height 是原图像素尺寸,建议提供,避免图片加载前后布局变化。
差异列表
DiffDetail 是已排好展示顺序的扁平数组。每一项必须包含以下公共字段:
| 字段 | 含义 |
| --- | --- |
| diffId | 本次结果内唯一且稳定的字符串 ID |
| type | modify(修改)、delete(删除)或 insert(新增) |
| ignored | 是否已忽略;未审阅时为 false |
| sourceText / targetText | 两侧对应的上下文文本;无文本时使用空字符串 |
| sourceLine / targetLine | 两侧行索引,从 0 开始 |
| srcPage / tgtPage | 两侧页面数组下标,从 0 开始 |
不同类型还需要以下字段:
| 类型 | 必填字段 | 可选字段 |
| --- | --- | --- |
| modify | delChars、insChars、sourceBox、targetBox | sourceChangeRanges、targetChangeRanges |
| delete | chars、sourceBox、tgtGap | — |
| insert | chars、targetBox、srcGap | — |
delChars/insChars是修改前后的文本,chars是删除或新增的文本。sourceBox/targetBox使用原图像素坐标[x1, y1, x2, y2],无对应框时传null。不要传页面缩放后的坐标。srcGap/tgtGap表示另一侧对应的空缺位置,格式为{ x, y, w, h },无位置时传null。- 修改项可以提供字符高亮范围
{ start, end }[],使用 Unicode 字符索引,包含start、不包含end。 - 表格差异可额外传
domain: 'table'和tableChange,可从包入口导入TableChange类型查看可选值。
公共类型均可从包入口导入:
import type { DiffDetail, DiffItem, PageMeta, ComparisonReview } from '@yuwenovo/ocr-result-view'如果后端返回的是 snake_case 字段或包含 code/message/data 的响应,应在接入项目中转换为上述结构,不能将整个响应直接传给 diffDetail。
4. 接入忽略 / 撤销审阅
只预览时可以跳过本节。需要审阅时,设置 :readonly="false" 并监听 ignore。
审阅流程为:用户点击 → 组件发出事件 → 接入项目保存到服务端 → 回传最新差异和统计。组件不会自行修改 ignored,也不会发送保存请求。
下面是审阅容器示例。父页面提供初始数据和 saveReview 函数,该函数封装你自己的保存接口,并返回服务端最新结果。
<script setup lang="ts">
import { ref } from 'vue'
import {
OcrResultView,
type ComparisonReview,
type OcrResultViewProps,
} from '@yuwenovo/ocr-result-view'
import '@yuwenovo/ocr-result-view/style.css'
type ReviewResult = OcrResultViewProps & { review: ComparisonReview }
const props = defineProps<{
initialResult: ReviewResult
saveReview: (request: {
diffId: string | null
ignored: boolean
reviewVersion: number
}) => Promise<ReviewResult>
}>()
const result = ref(props.initialResult)
const busy = ref(false)
const error = ref('')
async function handleIgnore(diffId: string | null, ignored: boolean) {
if (busy.value) return
busy.value = true
error.value = ''
try {
result.value = await props.saveReview({
diffId,
ignored,
reviewVersion: result.value.review.review_version,
})
} catch {
error.value = '保存失败,请重试'
} finally {
busy.value = false
}
}
</script>
<template>
<p v-if="error" role="alert">{{ error }}</p>
<div style="height: 720px">
<OcrResultView
v-bind="result"
:readonly="false"
:review-busy="busy"
@ignore="handleIgnore"
/>
</div>
</template>diffId === null 表示操作整个任务的全部差异,包含当前筛选条件外的项;ignored === true 表示忽略,false 表示撤销忽略。批量确认、版本冲突和重试策略由接入项目处理。
review 使用服务端返回的汇总:
interface ComparisonReview {
review_version: number
total_difference_count: number
ignored_difference_count: number
remaining_difference_count: number
comparison_passed: boolean
}不传 review 时,组件不会自行推断比对是否通过。切换任务时给组件或上述审阅容器设置 :key="taskId";同一任务保存成功后替换差异数组和审阅汇总。
5. API 参考
Props
| 属性 | 类型 | 默认值 / 说明 |
| --- | --- | --- |
| diffDetail | DiffDetail | 必填,差异数组 |
| sourcePages / targetPages | PageMeta[] | 必填,两侧页面图片 |
| sourceFilename / targetFilename | string | 默认显示“标准文档” / “对比文档” |
| readonly | boolean | true;设为 false 显示审阅按钮 |
| review | ComparisonReview | 可选,审阅统计及通过结论 |
| reviewBusy | boolean | false;保存期间禁用审阅按钮 |
| imageCrossOrigin | 'anonymous' \| 'use-credentials' \| null | 'anonymous';null 表示不设置图片 crossOrigin |
事件
| 事件 | 回调参数 | 说明 |
| --- | --- | --- |
| ignore | (diffId: string \| null, ignored: boolean) | 请求忽略或撤销,详见审阅接入 |
| select | (item: DiffItem \| null, index: number) | 选中项变化;index 为原始差异数组下标,无选中项时为 -1 |
| page-change | (side: 'source' \| 'target', index: number) | 当前页变化;index 从 0 开始 |
| filter-change | (tab: DiffTab, review: ReviewFilter) | 差异类型或审阅筛选变化 |
| image-error | (url: string) | 图片加载失败 |
实例方法
通过组件 ref 调用,例如:
<script setup lang="ts">
import { ref } from 'vue'
import { OcrResultView, type OcrResultViewExpose } from '@yuwenovo/ocr-result-view'
const viewer = ref<OcrResultViewExpose>()
// 组件挂载后调用:
// await viewer.value?.selectDiff('diff-1')
// viewer.value?.setFilter('all', 'all')
</script>
<!-- 将 ref="viewer" 加到已传入页面和差异数据的 OcrResultView 上。 -->| 方法 | 说明 |
| --- | --- |
| selectDiff(indexOrId): Promise<boolean> | 按原数组下标或 ID 定位;被筛选隐藏时切换为全部,无效参数返回 false |
| nextDiff(): boolean / previousDiff(): boolean | 按当前筛选结果导航,到边界返回 false |
| goToPage(side, index): boolean | 跳至指定侧页面,index 从 0 开始;越界返回 false |
| setFilter(tab, review?) | 设置筛选;省略 review 时保留当前审阅筛选 |
| resize() | 容器布局变化后请求重新布局;内部也监听 ResizeObserver |
| retryImages() | 清除图片失败记录,重新加载可见及缓冲页面 |
DiffTab 为 all / modify / delete / insert;ReviewFilter 为 pending / ignored / all。初始筛选为 all + all,显示全部差异,包含已忽略项。
插槽
| 插槽 | 参数 | 用途 |
| --- | --- | --- |
| toolbar | activeIndex, selectDiff, setFilter | 顶部工具栏 |
| source-title / target-title | filename | 自定义文档标题,保留内置页码导航 |
| diff-card | item, index, active, select, ignore | 替换差异卡片内容,保留外层容器 |
| empty | 无 | 没有页面数据时的内容 |
| footer | activeIndex, review | 底部业务信息 |
例如,将以下插槽放在 OcrResultView 内:
<template #diff-card="{ item, index, active, select }">
<button :aria-pressed="active" @click="select()">
{{ index + 1 }} · {{ item.type }} · {{ item.diffId }}
</button>
</template>卡片插槽的 ignore(boolean) 会触发审阅事件,只读或忙碌时不触发。自定义业务按钮的权限由接入项目控制。
6. 常见问题
组件空白或高度不对?
确认已导入 @yuwenovo/ocr-result-view/style.css,容器有明确高度,页面数组非空。弹窗或折叠面板展开后可调用 resize()。
图片加载失败?
先在浏览器网络面板检查图片地址、状态码和跨域响应头。默认使用 anonymous;跨域 Cookie 图片需要设置 image-cross-origin="use-credentials",并让图片服务允许当前来源及凭证。修复访问问题后调用 retryImages()。
图片请求不能自定义 Authorization 请求头。需要这类鉴权时,由接入项目提供同源代理地址、短期签名 URL,或先请求图片再传入 Blob URL;Blob URL 由接入项目负责释放。
为什么差异没有显示,或忽略后消失了?
默认显示全部差异,包含已忽略项。使用筛选栏查看“未忽略”或“已忽略”,或调用 setFilter('all', 'pending') 只查看未忽略差异。标注错位时检查原图尺寸、坐标和从 0 开始的页码。
如何用于 Nuxt / SSR?
查看器使用浏览器 Canvas 和图片 API,在客户端挂载,例如放在 Nuxt 的 ClientOnly 中,并为外层容器设置高度。
如何修改主题?
默认深色。可在组件或祖先元素上覆盖 CSS 变量,例如 --viewer-bg、--viewer-panel、--viewer-border、--viewer-card-text。例如:
<template>
<div class="viewer-container">
<!-- 在此放入已配置数据的 OcrResultView -->
</div>
</template>
<style scoped>
.viewer-container {
height: 720px;
--viewer-bg: #111827;
--viewer-panel: #1f2937;
--viewer-border: #374151;
--viewer-card-text: #e5e7eb;
}
</style>自定义图片加载
imageLoader?: (url: string) => Promise<HTMLImageElement> 可由宿主提供带鉴权的图片加载器,返回已完成加载/解码的图片。组件仍控制可见页调度、四路并发和图片缓存;不提供时沿用 imageCrossOrigin 和原始图片地址。请求头、会话、取消和临时 Blob URL 的释放由宿主管理,组件不持有业务凭据。
