@koi-br/ocr-web-sdk 1.0.31 → 1.0.33

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.31",
3
+ "version": "1.0.33",
4
4
  "description": "一个支持多种Office文件格式预览的Vue3组件SDK,包括PDF、Word、Excel、图片、OFD、TIF等格式",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -123,6 +123,7 @@
123
123
  :key="pageIndex"
124
124
  :data-page-number="pageIndex + 1"
125
125
  class="image-page-container"
126
+ :style="getPageContainerStyle(pageIndex + 1)"
126
127
  >
127
128
  <div
128
129
  class="image-wrapper"
@@ -514,6 +515,35 @@ const getPageBlocksData = (pageNo: number) => {
514
515
  return props.blocksData.filter((block) => block.pageNo === pageNo);
515
516
  };
516
517
 
518
+ // 计算指定页面缩放后的尺寸(考虑旋转)
519
+ const getPageScaledSize = (pageNo: number) => {
520
+ const pageSize = imageSizes.get(pageNo);
521
+ if (!pageSize || pageSize.width === 0 || pageSize.height === 0) {
522
+ return { width: 0, height: 0 };
523
+ }
524
+
525
+ const { width, height } = pageSize;
526
+ const scaledWidth = width * scale.value;
527
+ const scaledHeight = height * scale.value;
528
+
529
+ // 如果旋转了 90 度或 270 度,交换宽高
530
+ const normalizedRotation = ((rotation.value % 360) + 360) % 360;
531
+ const isRotated = normalizedRotation === 90 || normalizedRotation === 270;
532
+
533
+ return {
534
+ width: isRotated ? scaledHeight : scaledWidth,
535
+ height: isRotated ? scaledWidth : scaledHeight,
536
+ };
537
+ };
538
+
539
+ // 获取页面容器的样式(响应式,会根据缩放和旋转自动更新)
540
+ const getPageContainerStyle = (pageNo: number) => {
541
+ const scaledSize = getPageScaledSize(pageNo);
542
+ return {
543
+ height: scaledSize.height > 0 ? `${scaledSize.height}px` : 'auto',
544
+ };
545
+ };
546
+
517
547
  // 隐藏定时器
518
548
  let hideTimer: any = null;
519
549
 
@@ -1121,9 +1151,13 @@ const renderTextLayer = (pageNum?: number) => {
1121
1151
  hideTimer = null;
1122
1152
  }
1123
1153
 
1124
- // 如果有之前激活的文本块,先恢复其样式
1125
- if (activeBlockDiv.value && activeBlockDiv.value !== blockDiv) {
1126
- restoreBlockStyle(activeBlockDiv.value);
1154
+ // 清除当前页面所有文本块的高亮样式(除了当前文本块)
1155
+ // 这样可以防止多个文本块同时高亮的竞态条件
1156
+ clearAllHighlights(blockDiv);
1157
+
1158
+ // 清除跳转高亮标志(如果之前是通过跳转高亮的)
1159
+ if (isHighlighted.value) {
1160
+ isHighlighted.value = false;
1127
1161
  }
1128
1162
 
1129
1163
  // 设置当前文本块为激活状态
@@ -1139,7 +1173,10 @@ const renderTextLayer = (pageNum?: number) => {
1139
1173
  blockDiv.style.setProperty("padding", "1px 3px", "important");
1140
1174
  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");
1141
1175
  } else {
1142
- // 如果没有批注,使用 hover 样式(与 PdfPreview 保持一致)
1176
+ // 如果没有批注,先清除可能残留的样式,再设置 hover 样式(与 PdfPreview 保持一致)
1177
+ blockDiv.style.removeProperty("background-color");
1178
+ blockDiv.style.removeProperty("border");
1179
+ blockDiv.style.removeProperty("box-shadow");
1143
1180
  blockDiv.style.backgroundColor =
1144
1181
  "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
1145
1182
  blockDiv.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
@@ -1303,6 +1340,75 @@ const showAnnotationButtonForBlock = (
1303
1340
  }
1304
1341
  };
1305
1342
 
1343
+ /**
1344
+ * 清除指定页面所有文本块的高亮样式(除了指定的文本块)
1345
+ * @param excludeBlockDiv 要排除的文本块(不清除它的样式)
1346
+ * @param pageNum 要清除的页码,如果不提供则从 excludeBlockDiv 中获取
1347
+ */
1348
+ const clearAllHighlights = (excludeBlockDiv?: HTMLElement, pageNum?: number) => {
1349
+ // 确定要清除的页码
1350
+ let targetPage = pageNum;
1351
+ if (!targetPage && excludeBlockDiv) {
1352
+ const pageStr = excludeBlockDiv.dataset.page;
1353
+ targetPage = pageStr ? parseInt(pageStr, 10) : currentPage.value;
1354
+ }
1355
+ if (!targetPage) {
1356
+ targetPage = currentPage.value;
1357
+ }
1358
+
1359
+ const textLayer = textLayerRefs.get(targetPage);
1360
+ if (!textLayer) return;
1361
+
1362
+ const blockDivs = textLayer.querySelectorAll(".text-block");
1363
+ blockDivs.forEach((div) => {
1364
+ const el = div as HTMLElement;
1365
+ // 如果是指定的文本块,跳过
1366
+ if (excludeBlockDiv && el === excludeBlockDiv) {
1367
+ return;
1368
+ }
1369
+
1370
+ // 检查是否有批注
1371
+ const bboxStr = el.dataset.bbox;
1372
+ if (!bboxStr) return;
1373
+
1374
+ try {
1375
+ const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1376
+ const pageStr = el.dataset.page;
1377
+ const pageNum = pageStr ? parseInt(pageStr, 10) : undefined;
1378
+ const existingAnnotation = getAnnotationForBlock(bbox, pageNum);
1379
+
1380
+ if (existingAnnotation) {
1381
+ // 如果有批注,恢复批注样式(不使用 hover 样式)
1382
+ el.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1383
+ el.style.setProperty("border", "1px solid rgba(255, 193, 7, 0.7)", "important");
1384
+ el.style.setProperty("border-radius", "3px", "important");
1385
+ el.style.setProperty("padding", "1px 3px", "important");
1386
+ el.style.setProperty("box-shadow", "0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1387
+ } else {
1388
+ // 如果没有批注,清除所有高亮样式
1389
+ el.style.backgroundColor = "transparent";
1390
+ el.style.border = "none";
1391
+ el.style.borderRadius = "2px";
1392
+ el.style.padding = "0";
1393
+ el.style.boxShadow = "none";
1394
+ el.style.removeProperty("background-color");
1395
+ el.style.removeProperty("border");
1396
+ el.style.removeProperty("box-shadow");
1397
+ }
1398
+ } catch (error) {
1399
+ // 如果解析失败,清除所有高亮样式
1400
+ el.style.backgroundColor = "transparent";
1401
+ el.style.border = "none";
1402
+ el.style.borderRadius = "2px";
1403
+ el.style.padding = "0";
1404
+ el.style.boxShadow = "none";
1405
+ el.style.removeProperty("background-color");
1406
+ el.style.removeProperty("border");
1407
+ el.style.removeProperty("box-shadow");
1408
+ }
1409
+ });
1410
+ };
1411
+
1306
1412
  /**
1307
1413
  * 恢复文本块样式(根据是否有批注)
1308
1414
  */
@@ -1358,19 +1464,29 @@ const restoreBlockStyle = (blockDiv: HTMLElement) => {
1358
1464
  computedAfter: window.getComputedStyle(blockDiv).backgroundColor,
1359
1465
  });
1360
1466
  } else {
1361
- // 如果没有批注,恢复透明背景
1467
+ // 如果没有批注,恢复透明背景(清除所有高亮相关样式)
1362
1468
  blockDiv.style.backgroundColor = "transparent";
1363
1469
  blockDiv.style.border = "none";
1470
+ blockDiv.style.borderRadius = "2px"; // 恢复默认值
1364
1471
  blockDiv.style.padding = "0";
1365
1472
  blockDiv.style.boxShadow = "none";
1473
+ // 清除可能残留的样式属性
1474
+ blockDiv.style.removeProperty("background-color");
1475
+ blockDiv.style.removeProperty("border");
1476
+ blockDiv.style.removeProperty("box-shadow");
1366
1477
  }
1367
1478
  } catch (error) {
1368
1479
  console.error("restoreBlockStyle 错误:", error);
1369
- // 如果解析失败,恢复透明背景
1480
+ // 如果解析失败,恢复透明背景(清除所有高亮相关样式)
1370
1481
  blockDiv.style.backgroundColor = "transparent";
1371
1482
  blockDiv.style.border = "none";
1483
+ blockDiv.style.borderRadius = "2px"; // 恢复默认值
1372
1484
  blockDiv.style.padding = "0";
1373
1485
  blockDiv.style.boxShadow = "none";
1486
+ // 清除可能残留的样式属性
1487
+ blockDiv.style.removeProperty("background-color");
1488
+ blockDiv.style.removeProperty("border");
1489
+ blockDiv.style.removeProperty("box-shadow");
1374
1490
  }
1375
1491
  };
1376
1492
 
@@ -1408,19 +1524,32 @@ const hideAnnotationButton = () => {
1408
1524
  activeBlockDiv.value.style.boxShadow =
1409
1525
  "0 1px 2px rgba(255, 193, 7, 0.25)";
1410
1526
  } else {
1411
- // 如果没有批注,恢复透明背景
1527
+ // 如果没有批注,恢复透明背景(清除所有高亮相关样式)
1412
1528
  activeBlockDiv.value.style.backgroundColor = "transparent";
1413
1529
  activeBlockDiv.value.style.border = "none";
1530
+ activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
1414
1531
  activeBlockDiv.value.style.padding = "0";
1415
1532
  activeBlockDiv.value.style.boxShadow = "none";
1533
+ // 清除可能残留的样式属性
1534
+ activeBlockDiv.value.style.removeProperty("background-color");
1535
+ activeBlockDiv.value.style.removeProperty("border");
1536
+ activeBlockDiv.value.style.removeProperty("box-shadow");
1416
1537
  }
1417
1538
  } catch (error) {
1539
+ // 如果解析失败,恢复透明背景(清除所有高亮相关样式)
1418
1540
  activeBlockDiv.value.style.backgroundColor = "transparent";
1419
1541
  activeBlockDiv.value.style.border = "none";
1542
+ activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
1420
1543
  activeBlockDiv.value.style.padding = "0";
1421
1544
  activeBlockDiv.value.style.boxShadow = "none";
1545
+ // 清除可能残留的样式属性
1546
+ activeBlockDiv.value.style.removeProperty("background-color");
1547
+ activeBlockDiv.value.style.removeProperty("border");
1548
+ activeBlockDiv.value.style.removeProperty("box-shadow");
1422
1549
  }
1423
1550
  }
1551
+ // 清除高亮标志
1552
+ isHighlighted.value = false;
1424
1553
  activeBlockDiv.value = null;
1425
1554
  }
1426
1555
  }, 300);
@@ -1741,10 +1870,55 @@ const handleScroll = (e: Event) => {
1741
1870
  // 记录上次滚动位置,用于判断滚动方向
1742
1871
  let lastScrollTop = 0;
1743
1872
 
1873
+ /**
1874
+ * 根据滚动位置更新当前页码
1875
+ * 通过找到距离视口中心最近的页面来确定当前页
1876
+ * 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
1877
+ */
1878
+ const updateCurrentPageFromScroll = () => {
1879
+ if (!containerRef.value) return;
1880
+
1881
+ const container = containerRef.value;
1882
+ const containerRect = container.getBoundingClientRect();
1883
+ const containerTop = containerRect.top;
1884
+ const containerHeight = container.clientHeight;
1885
+ const containerCenter = containerTop + containerHeight / 2;
1886
+
1887
+ // 遍历所有页面,找到距离视口中心最近的页面
1888
+ let closestPage = 1;
1889
+ let closestDistance = Infinity;
1890
+
1891
+ for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
1892
+ const pageElement = container.querySelector(
1893
+ `[data-page-number="${pageNum}"]`
1894
+ ) as HTMLElement;
1895
+
1896
+ if (pageElement) {
1897
+ // 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
1898
+ // 而不是使用 offsetTop(不考虑 transform)
1899
+ const pageRect = pageElement.getBoundingClientRect();
1900
+ const pageTop = pageRect.top;
1901
+ const pageHeight = pageRect.height;
1902
+ const pageCenter = pageTop + pageHeight / 2;
1903
+ const distance = Math.abs(pageCenter - containerCenter);
1904
+
1905
+ if (distance < closestDistance) {
1906
+ closestDistance = distance;
1907
+ closestPage = pageNum;
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ // 只有当页码真正改变时才更新
1913
+ if (closestPage !== currentPage.value) {
1914
+ currentPage.value = closestPage;
1915
+ emit("page-change", closestPage, totalPages.value);
1916
+ }
1917
+ };
1918
+
1744
1919
  /**
1745
1920
  * 处理滚动翻页(通过滚动位置判断当前页)
1746
- * 向下滑动:当视口顶部到达下一页的顶部时,切换到下一页
1747
- * 向上滑动:当视口底部到达上一页的底部时,切换到上一页
1921
+ * 使用视口中心最近的页面来确定当前页,更准确
1748
1922
  */
1749
1923
  const handleScrollPaging = (e: Event) => {
1750
1924
  const container = e.target as HTMLElement;
@@ -1767,73 +1941,8 @@ const handleScrollPaging = (e: Event) => {
1767
1941
  return;
1768
1942
  }
1769
1943
 
1770
- const scrollTop = container.scrollTop;
1771
- const clientHeight = container.clientHeight;
1772
- const scrollBottom = scrollTop + clientHeight;
1773
-
1774
- // 判断滚动方向
1775
- const isScrollingDown = scrollTop > lastScrollTop;
1776
- lastScrollTop = scrollTop;
1777
-
1778
- // 获取当前页的元素
1779
- const currentPageElement = container.querySelector(
1780
- `[data-page-number="${currentPage.value}"]`
1781
- ) as HTMLElement;
1782
-
1783
- if (!currentPageElement) return;
1784
-
1785
- const currentPageTop = currentPageElement.offsetTop;
1786
- const currentPageHeight = currentPageElement.offsetHeight;
1787
- const currentPageBottom = currentPageTop + currentPageHeight;
1788
-
1789
- let newPage = currentPage.value;
1790
-
1791
- if (isScrollingDown) {
1792
- // 向下滑动:当视口顶部到达或超过下一页的顶部时,切换到下一页
1793
- if (currentPage.value < totalPages.value) {
1794
- const nextPageElement = container.querySelector(
1795
- `[data-page-number="${currentPage.value + 1}"]`
1796
- ) as HTMLElement;
1797
-
1798
- if (nextPageElement) {
1799
- const nextPageTop = nextPageElement.offsetTop;
1800
- // 当视口顶部到达或超过下一页的顶部时,切换到下一页
1801
- if (scrollTop >= nextPageTop) {
1802
- newPage = currentPage.value + 1;
1803
- }
1804
- }
1805
- }
1806
- } else {
1807
- // 向上滑动:当视口底部到达或超过上一页的底部时,切换到上一页
1808
- if (currentPage.value > 1) {
1809
- const prevPageElement = container.querySelector(
1810
- `[data-page-number="${currentPage.value - 1}"]`
1811
- ) as HTMLElement;
1812
-
1813
- if (prevPageElement) {
1814
- const prevPageTop = prevPageElement.offsetTop;
1815
- const prevPageHeight = prevPageElement.offsetHeight;
1816
- const prevPageBottom = prevPageTop + prevPageHeight;
1817
-
1818
- // 当视口底部到达或超过上一页的底部时,切换到上一页
1819
- if (scrollBottom <= prevPageBottom) {
1820
- newPage = currentPage.value - 1;
1821
- }
1822
- }
1823
- }
1824
- }
1825
-
1826
- // 如果页码发生变化,更新当前页
1827
- if (newPage !== currentPage.value) {
1828
- isScrollPaging.value = true;
1829
- currentPage.value = newPage;
1830
- emit("page-change", newPage, totalPages.value);
1831
-
1832
- // 延迟解锁,确保页面切换完成
1833
- setTimeout(() => {
1834
- isScrollPaging.value = false;
1835
- }, 100);
1836
- }
1944
+ // 更新当前页码(基于视口中心最近的页面)
1945
+ updateCurrentPageFromScroll();
1837
1946
  }, 100);
1838
1947
  };
1839
1948