@koi-br/ocr-web-sdk 1.0.30 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koi-br/ocr-web-sdk",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "一个支持多种Office文件格式预览的Vue3组件SDK,包括PDF、Word、Excel、图片、OFD、TIF等格式",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -272,6 +272,15 @@ interface AnnotationInfo {
272
272
  createTime: number; // 创建时间戳
273
273
  }
274
274
 
275
+ /**
276
+ * 高亮样式配置
277
+ */
278
+ interface HighlightStyle {
279
+ backgroundColor?: string; // 背景色
280
+ border?: string; // 边框样式(如 "2px solid rgba(30, 144, 255, 0.8)")
281
+ boxShadow?: string; // 阴影样式
282
+ }
283
+
275
284
  const props = defineProps({
276
285
  // 支持单个URL(向后兼容)或URL数组
277
286
  url: {
@@ -1112,9 +1121,13 @@ const renderTextLayer = (pageNum?: number) => {
1112
1121
  hideTimer = null;
1113
1122
  }
1114
1123
 
1115
- // 如果有之前激活的文本块,先恢复其样式
1116
- if (activeBlockDiv.value && activeBlockDiv.value !== blockDiv) {
1117
- restoreBlockStyle(activeBlockDiv.value);
1124
+ // 清除当前页面所有文本块的高亮样式(除了当前文本块)
1125
+ // 这样可以防止多个文本块同时高亮的竞态条件
1126
+ clearAllHighlights(blockDiv);
1127
+
1128
+ // 清除跳转高亮标志(如果之前是通过跳转高亮的)
1129
+ if (isHighlighted.value) {
1130
+ isHighlighted.value = false;
1118
1131
  }
1119
1132
 
1120
1133
  // 设置当前文本块为激活状态
@@ -1130,7 +1143,10 @@ const renderTextLayer = (pageNum?: number) => {
1130
1143
  blockDiv.style.setProperty("padding", "1px 3px", "important");
1131
1144
  blockDiv.style.setProperty("box-shadow", "0 0 0 2px rgba(30, 144, 255, 0.6), 0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1132
1145
  } else {
1133
- // 如果没有批注,使用 hover 样式(与 PdfPreview 保持一致)
1146
+ // 如果没有批注,先清除可能残留的样式,再设置 hover 样式(与 PdfPreview 保持一致)
1147
+ blockDiv.style.removeProperty("background-color");
1148
+ blockDiv.style.removeProperty("border");
1149
+ blockDiv.style.removeProperty("box-shadow");
1134
1150
  blockDiv.style.backgroundColor =
1135
1151
  "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
1136
1152
  blockDiv.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
@@ -1294,6 +1310,75 @@ const showAnnotationButtonForBlock = (
1294
1310
  }
1295
1311
  };
1296
1312
 
1313
+ /**
1314
+ * 清除指定页面所有文本块的高亮样式(除了指定的文本块)
1315
+ * @param excludeBlockDiv 要排除的文本块(不清除它的样式)
1316
+ * @param pageNum 要清除的页码,如果不提供则从 excludeBlockDiv 中获取
1317
+ */
1318
+ const clearAllHighlights = (excludeBlockDiv?: HTMLElement, pageNum?: number) => {
1319
+ // 确定要清除的页码
1320
+ let targetPage = pageNum;
1321
+ if (!targetPage && excludeBlockDiv) {
1322
+ const pageStr = excludeBlockDiv.dataset.page;
1323
+ targetPage = pageStr ? parseInt(pageStr, 10) : currentPage.value;
1324
+ }
1325
+ if (!targetPage) {
1326
+ targetPage = currentPage.value;
1327
+ }
1328
+
1329
+ const textLayer = textLayerRefs.get(targetPage);
1330
+ if (!textLayer) return;
1331
+
1332
+ const blockDivs = textLayer.querySelectorAll(".text-block");
1333
+ blockDivs.forEach((div) => {
1334
+ const el = div as HTMLElement;
1335
+ // 如果是指定的文本块,跳过
1336
+ if (excludeBlockDiv && el === excludeBlockDiv) {
1337
+ return;
1338
+ }
1339
+
1340
+ // 检查是否有批注
1341
+ const bboxStr = el.dataset.bbox;
1342
+ if (!bboxStr) return;
1343
+
1344
+ try {
1345
+ const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1346
+ const pageStr = el.dataset.page;
1347
+ const pageNum = pageStr ? parseInt(pageStr, 10) : undefined;
1348
+ const existingAnnotation = getAnnotationForBlock(bbox, pageNum);
1349
+
1350
+ if (existingAnnotation) {
1351
+ // 如果有批注,恢复批注样式(不使用 hover 样式)
1352
+ el.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1353
+ el.style.setProperty("border", "1px solid rgba(255, 193, 7, 0.7)", "important");
1354
+ el.style.setProperty("border-radius", "3px", "important");
1355
+ el.style.setProperty("padding", "1px 3px", "important");
1356
+ el.style.setProperty("box-shadow", "0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1357
+ } else {
1358
+ // 如果没有批注,清除所有高亮样式
1359
+ el.style.backgroundColor = "transparent";
1360
+ el.style.border = "none";
1361
+ el.style.borderRadius = "2px";
1362
+ el.style.padding = "0";
1363
+ el.style.boxShadow = "none";
1364
+ el.style.removeProperty("background-color");
1365
+ el.style.removeProperty("border");
1366
+ el.style.removeProperty("box-shadow");
1367
+ }
1368
+ } catch (error) {
1369
+ // 如果解析失败,清除所有高亮样式
1370
+ el.style.backgroundColor = "transparent";
1371
+ el.style.border = "none";
1372
+ el.style.borderRadius = "2px";
1373
+ el.style.padding = "0";
1374
+ el.style.boxShadow = "none";
1375
+ el.style.removeProperty("background-color");
1376
+ el.style.removeProperty("border");
1377
+ el.style.removeProperty("box-shadow");
1378
+ }
1379
+ });
1380
+ };
1381
+
1297
1382
  /**
1298
1383
  * 恢复文本块样式(根据是否有批注)
1299
1384
  */
@@ -1349,19 +1434,29 @@ const restoreBlockStyle = (blockDiv: HTMLElement) => {
1349
1434
  computedAfter: window.getComputedStyle(blockDiv).backgroundColor,
1350
1435
  });
1351
1436
  } else {
1352
- // 如果没有批注,恢复透明背景
1437
+ // 如果没有批注,恢复透明背景(清除所有高亮相关样式)
1353
1438
  blockDiv.style.backgroundColor = "transparent";
1354
1439
  blockDiv.style.border = "none";
1440
+ blockDiv.style.borderRadius = "2px"; // 恢复默认值
1355
1441
  blockDiv.style.padding = "0";
1356
1442
  blockDiv.style.boxShadow = "none";
1443
+ // 清除可能残留的样式属性
1444
+ blockDiv.style.removeProperty("background-color");
1445
+ blockDiv.style.removeProperty("border");
1446
+ blockDiv.style.removeProperty("box-shadow");
1357
1447
  }
1358
1448
  } catch (error) {
1359
1449
  console.error("restoreBlockStyle 错误:", error);
1360
- // 如果解析失败,恢复透明背景
1450
+ // 如果解析失败,恢复透明背景(清除所有高亮相关样式)
1361
1451
  blockDiv.style.backgroundColor = "transparent";
1362
1452
  blockDiv.style.border = "none";
1453
+ blockDiv.style.borderRadius = "2px"; // 恢复默认值
1363
1454
  blockDiv.style.padding = "0";
1364
1455
  blockDiv.style.boxShadow = "none";
1456
+ // 清除可能残留的样式属性
1457
+ blockDiv.style.removeProperty("background-color");
1458
+ blockDiv.style.removeProperty("border");
1459
+ blockDiv.style.removeProperty("box-shadow");
1365
1460
  }
1366
1461
  };
1367
1462
 
@@ -1399,19 +1494,32 @@ const hideAnnotationButton = () => {
1399
1494
  activeBlockDiv.value.style.boxShadow =
1400
1495
  "0 1px 2px rgba(255, 193, 7, 0.25)";
1401
1496
  } else {
1402
- // 如果没有批注,恢复透明背景
1497
+ // 如果没有批注,恢复透明背景(清除所有高亮相关样式)
1403
1498
  activeBlockDiv.value.style.backgroundColor = "transparent";
1404
1499
  activeBlockDiv.value.style.border = "none";
1500
+ activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
1405
1501
  activeBlockDiv.value.style.padding = "0";
1406
1502
  activeBlockDiv.value.style.boxShadow = "none";
1503
+ // 清除可能残留的样式属性
1504
+ activeBlockDiv.value.style.removeProperty("background-color");
1505
+ activeBlockDiv.value.style.removeProperty("border");
1506
+ activeBlockDiv.value.style.removeProperty("box-shadow");
1407
1507
  }
1408
1508
  } catch (error) {
1509
+ // 如果解析失败,恢复透明背景(清除所有高亮相关样式)
1409
1510
  activeBlockDiv.value.style.backgroundColor = "transparent";
1410
1511
  activeBlockDiv.value.style.border = "none";
1512
+ activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
1411
1513
  activeBlockDiv.value.style.padding = "0";
1412
1514
  activeBlockDiv.value.style.boxShadow = "none";
1515
+ // 清除可能残留的样式属性
1516
+ activeBlockDiv.value.style.removeProperty("background-color");
1517
+ activeBlockDiv.value.style.removeProperty("border");
1518
+ activeBlockDiv.value.style.removeProperty("box-shadow");
1413
1519
  }
1414
1520
  }
1521
+ // 清除高亮标志
1522
+ isHighlighted.value = false;
1415
1523
  activeBlockDiv.value = null;
1416
1524
  }
1417
1525
  }, 300);
@@ -1732,10 +1840,55 @@ const handleScroll = (e: Event) => {
1732
1840
  // 记录上次滚动位置,用于判断滚动方向
1733
1841
  let lastScrollTop = 0;
1734
1842
 
1843
+ /**
1844
+ * 根据滚动位置更新当前页码
1845
+ * 通过找到距离视口中心最近的页面来确定当前页
1846
+ * 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
1847
+ */
1848
+ const updateCurrentPageFromScroll = () => {
1849
+ if (!containerRef.value) return;
1850
+
1851
+ const container = containerRef.value;
1852
+ const containerRect = container.getBoundingClientRect();
1853
+ const containerTop = containerRect.top;
1854
+ const containerHeight = container.clientHeight;
1855
+ const containerCenter = containerTop + containerHeight / 2;
1856
+
1857
+ // 遍历所有页面,找到距离视口中心最近的页面
1858
+ let closestPage = 1;
1859
+ let closestDistance = Infinity;
1860
+
1861
+ for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
1862
+ const pageElement = container.querySelector(
1863
+ `[data-page-number="${pageNum}"]`
1864
+ ) as HTMLElement;
1865
+
1866
+ if (pageElement) {
1867
+ // 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
1868
+ // 而不是使用 offsetTop(不考虑 transform)
1869
+ const pageRect = pageElement.getBoundingClientRect();
1870
+ const pageTop = pageRect.top;
1871
+ const pageHeight = pageRect.height;
1872
+ const pageCenter = pageTop + pageHeight / 2;
1873
+ const distance = Math.abs(pageCenter - containerCenter);
1874
+
1875
+ if (distance < closestDistance) {
1876
+ closestDistance = distance;
1877
+ closestPage = pageNum;
1878
+ }
1879
+ }
1880
+ }
1881
+
1882
+ // 只有当页码真正改变时才更新
1883
+ if (closestPage !== currentPage.value) {
1884
+ currentPage.value = closestPage;
1885
+ emit("page-change", closestPage, totalPages.value);
1886
+ }
1887
+ };
1888
+
1735
1889
  /**
1736
1890
  * 处理滚动翻页(通过滚动位置判断当前页)
1737
- * 向下滑动:当视口顶部到达下一页的顶部时,切换到下一页
1738
- * 向上滑动:当视口底部到达上一页的底部时,切换到上一页
1891
+ * 使用视口中心最近的页面来确定当前页,更准确
1739
1892
  */
1740
1893
  const handleScrollPaging = (e: Event) => {
1741
1894
  const container = e.target as HTMLElement;
@@ -1758,73 +1911,8 @@ const handleScrollPaging = (e: Event) => {
1758
1911
  return;
1759
1912
  }
1760
1913
 
1761
- const scrollTop = container.scrollTop;
1762
- const clientHeight = container.clientHeight;
1763
- const scrollBottom = scrollTop + clientHeight;
1764
-
1765
- // 判断滚动方向
1766
- const isScrollingDown = scrollTop > lastScrollTop;
1767
- lastScrollTop = scrollTop;
1768
-
1769
- // 获取当前页的元素
1770
- const currentPageElement = container.querySelector(
1771
- `[data-page-number="${currentPage.value}"]`
1772
- ) as HTMLElement;
1773
-
1774
- if (!currentPageElement) return;
1775
-
1776
- const currentPageTop = currentPageElement.offsetTop;
1777
- const currentPageHeight = currentPageElement.offsetHeight;
1778
- const currentPageBottom = currentPageTop + currentPageHeight;
1779
-
1780
- let newPage = currentPage.value;
1781
-
1782
- if (isScrollingDown) {
1783
- // 向下滑动:当视口顶部到达或超过下一页的顶部时,切换到下一页
1784
- if (currentPage.value < totalPages.value) {
1785
- const nextPageElement = container.querySelector(
1786
- `[data-page-number="${currentPage.value + 1}"]`
1787
- ) as HTMLElement;
1788
-
1789
- if (nextPageElement) {
1790
- const nextPageTop = nextPageElement.offsetTop;
1791
- // 当视口顶部到达或超过下一页的顶部时,切换到下一页
1792
- if (scrollTop >= nextPageTop) {
1793
- newPage = currentPage.value + 1;
1794
- }
1795
- }
1796
- }
1797
- } else {
1798
- // 向上滑动:当视口底部到达或超过上一页的底部时,切换到上一页
1799
- if (currentPage.value > 1) {
1800
- const prevPageElement = container.querySelector(
1801
- `[data-page-number="${currentPage.value - 1}"]`
1802
- ) as HTMLElement;
1803
-
1804
- if (prevPageElement) {
1805
- const prevPageTop = prevPageElement.offsetTop;
1806
- const prevPageHeight = prevPageElement.offsetHeight;
1807
- const prevPageBottom = prevPageTop + prevPageHeight;
1808
-
1809
- // 当视口底部到达或超过上一页的底部时,切换到上一页
1810
- if (scrollBottom <= prevPageBottom) {
1811
- newPage = currentPage.value - 1;
1812
- }
1813
- }
1814
- }
1815
- }
1816
-
1817
- // 如果页码发生变化,更新当前页
1818
- if (newPage !== currentPage.value) {
1819
- isScrollPaging.value = true;
1820
- currentPage.value = newPage;
1821
- emit("page-change", newPage, totalPages.value);
1822
-
1823
- // 延迟解锁,确保页面切换完成
1824
- setTimeout(() => {
1825
- isScrollPaging.value = false;
1826
- }, 100);
1827
- }
1914
+ // 更新当前页码(基于视口中心最近的页面)
1915
+ updateCurrentPageFromScroll();
1828
1916
  }, 100);
1829
1917
  };
1830
1918
 
@@ -1856,12 +1944,14 @@ const isElementVisible = (
1856
1944
  const highlightPosition = (
1857
1945
  pageNum: number,
1858
1946
  bbox: [number, number, number, number],
1859
- shouldScroll: boolean = true
1947
+ shouldScroll: boolean = true,
1948
+ highlightStyle?: HighlightStyle
1860
1949
  ): boolean => {
1861
1950
  // 清除之前的高亮
1862
1951
  if (activeBlockDiv.value) {
1863
1952
  activeBlockDiv.value.style.backgroundColor = "transparent";
1864
1953
  activeBlockDiv.value.style.boxShadow = "none";
1954
+ activeBlockDiv.value.style.border = "none";
1865
1955
  activeBlockDiv.value = null;
1866
1956
  }
1867
1957
  isHighlighted.value = false;
@@ -1916,7 +2006,7 @@ const highlightPosition = (
1916
2006
  // 等待页面切换完成后再高亮
1917
2007
  nextTick(() => {
1918
2008
  setTimeout(() => {
1919
- highlightPosition(pageNum, bbox, shouldScroll);
2009
+ highlightPosition(pageNum, bbox, shouldScroll, highlightStyle);
1920
2010
  }, 300);
1921
2011
  });
1922
2012
  return true;
@@ -1928,10 +2018,24 @@ const highlightPosition = (
1928
2018
  activeBlockDiv.value = elementRef;
1929
2019
  isHighlighted.value = true;
1930
2020
 
1931
- // 使用一致的高亮样式
1932
- elementRef.style.backgroundColor =
1933
- "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
1934
- elementRef.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
2021
+ // 使用传入的高亮样式,如果没有传入则使用默认样式
2022
+ if (highlightStyle) {
2023
+ if (highlightStyle.backgroundColor) {
2024
+ elementRef.style.backgroundColor = highlightStyle.backgroundColor;
2025
+ }
2026
+ if (highlightStyle.border) {
2027
+ elementRef.style.border = highlightStyle.border;
2028
+ }
2029
+ if (highlightStyle.boxShadow) {
2030
+ elementRef.style.boxShadow = highlightStyle.boxShadow;
2031
+ }
2032
+ } else {
2033
+ // 默认高亮样式
2034
+ elementRef.style.backgroundColor =
2035
+ "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
2036
+ elementRef.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
2037
+ elementRef.style.border = "none";
2038
+ }
1935
2039
 
1936
2040
  // 只有在需要滚动且元素不在视口内时才滚动
1937
2041
  if (shouldScroll && containerRef.value) {
@@ -1946,6 +2050,7 @@ const highlightPosition = (
1946
2050
  if (activeBlockDiv.value === elementRef && isHighlighted.value) {
1947
2051
  elementRef.style.backgroundColor = "transparent";
1948
2052
  elementRef.style.boxShadow = "none";
2053
+ elementRef.style.border = "none";
1949
2054
  activeBlockDiv.value = null;
1950
2055
  isHighlighted.value = false;
1951
2056
  }
@@ -1963,7 +2068,8 @@ const highlightPosition = (
1963
2068
  const jumpToPosition = (
1964
2069
  pageNum: number,
1965
2070
  bbox: [number, number, number, number],
1966
- emitEvent: boolean = true
2071
+ emitEvent: boolean = true,
2072
+ highlightStyle?: HighlightStyle
1967
2073
  ) => {
1968
2074
  // 如果页码不在有效范围内,直接返回
1969
2075
  if (pageNum < 1 || pageNum > totalPages.value) {
@@ -1978,7 +2084,7 @@ const jumpToPosition = (
1978
2084
  const retryDelay = 200;
1979
2085
 
1980
2086
  const tryHighlight = () => {
1981
- const success = highlightPosition(pageNum, bbox, true);
2087
+ const success = highlightPosition(pageNum, bbox, true, highlightStyle);
1982
2088
  if (success) {
1983
2089
  // 高亮成功,触发事件
1984
2090
  if (emitEvent) {
package/preview/index.vue CHANGED
@@ -483,14 +483,14 @@ defineExpose({
483
483
  getCurrentPreview: getCurrentPreviewRef,
484
484
  // PDF 预览的代理方法(方便使用)
485
485
  goToPage: (pageNum) => pdfPreviewRef.value?.goToPage(pageNum),
486
- jumpToPosition: (pageNum, bbox, emitEvent) => {
486
+ jumpToPosition: (pageNum, bbox, emitEvent, highlightStyle) => {
487
487
  // PDF 预览的定位方法
488
488
  if (fileType.value === 'pdf' && pdfPreviewRef.value) {
489
- return pdfPreviewRef.value.jumpToPosition(pageNum, bbox, emitEvent);
489
+ return pdfPreviewRef.value.jumpToPosition(pageNum, bbox, emitEvent, highlightStyle);
490
490
  }
491
- // 图片预览的定位方法(现在也支持 pageNum)
491
+ // 图片预览的定位方法(现在也支持 pageNum 和 highlightStyle
492
492
  if (fileType.value === 'image' && imagePreviewRef.value) {
493
- return imagePreviewRef.value.jumpToPosition(pageNum, bbox, emitEvent);
493
+ return imagePreviewRef.value.jumpToPosition(pageNum, bbox, emitEvent, highlightStyle);
494
494
  }
495
495
  },
496
496
  getCurrentPage: () => pdfPreviewRef.value?.getCurrentPage(),