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

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.28",
3
+ "version": "1.0.30",
4
4
  "description": "一个支持多种Office文件格式预览的Vue3组件SDK,包括PDF、Word、Excel、图片、OFD、TIF等格式",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -395,6 +395,9 @@ const annotationButtonRef = ref<HTMLElement>();
395
395
  // 图片尺寸(存储每页的尺寸)
396
396
  const imageSizes = new Map<number, { width: number; height: number }>();
397
397
 
398
+ // 已渲染的页面集合(用于跟踪哪些页面已渲染)
399
+ const renderedPages = ref<Set<number>>(new Set());
400
+
398
401
  // 设置图片引用
399
402
  const setImageRef = (el: any, pageNum: number) => {
400
403
  if (el) {
@@ -426,12 +429,18 @@ const imageSize = computed({
426
429
  });
427
430
 
428
431
  // 计算图片放大后的实际尺寸(考虑旋转)
432
+ // 在自适应宽度模式下,使用第一页的尺寸;否则使用当前页的尺寸
429
433
  const scaledImageSize = computed(() => {
430
- if (imageSize.value.width === 0 || imageSize.value.height === 0) {
434
+ // 如果启用自适应宽度,使用第一页的尺寸作为基准
435
+ const baseSize = props.autoFitWidth
436
+ ? (imageSizes.get(1) || { width: 0, height: 0 })
437
+ : imageSize.value;
438
+
439
+ if (baseSize.width === 0 || baseSize.height === 0) {
431
440
  return { width: 0, height: 0 };
432
441
  }
433
442
 
434
- const { width, height } = imageSize.value;
443
+ const { width, height } = baseSize;
435
444
  const scaledWidth = width * scale.value;
436
445
  const scaledHeight = height * scale.value;
437
446
 
@@ -817,6 +826,9 @@ const reset = () => {
817
826
  annotationInput.value = "";
818
827
  activeBlockDiv.value = null;
819
828
  isHighlighted.value = false;
829
+
830
+ // 清除已渲染页面集合
831
+ renderedPages.value.clear();
820
832
  };
821
833
 
822
834
  const original = () => {
@@ -825,11 +837,13 @@ const original = () => {
825
837
 
826
838
  // 计算自适应宽度的缩放比例
827
839
  const calculateAutoFitScale = () => {
828
- if (
829
- !props.autoFitWidth ||
830
- !containerRef.value ||
831
- imageSize.value.width === 0
832
- ) {
840
+ if (!props.autoFitWidth || !containerRef.value) {
841
+ return 1;
842
+ }
843
+
844
+ // 使用第一页的图片尺寸作为基准(所有页面使用相同的缩放比例)
845
+ const firstPageSize = imageSizes.get(1);
846
+ if (!firstPageSize || firstPageSize.width === 0) {
833
847
  return 1;
834
848
  }
835
849
 
@@ -846,7 +860,7 @@ const calculateAutoFitScale = () => {
846
860
  const normalizedRotation = ((rotation.value % 360) + 360) % 360;
847
861
  const isRotated = normalizedRotation === 90 || normalizedRotation === 270;
848
862
 
849
- const imageWidth = isRotated ? imageSize.value.height : imageSize.value.width;
863
+ const imageWidth = isRotated ? firstPageSize.height : firstPageSize.width;
850
864
 
851
865
  if (imageWidth <= 0) {
852
866
  return 1;
@@ -884,10 +898,18 @@ const onImageLoad = (event: Event, pageNum: number) => {
884
898
  scale.value = autoScale;
885
899
  initialAutoFitScale.value = autoScale; // 记录初始自适应缩放比例
886
900
  }
887
- }, 50);
901
+ }, 100); // 增加延迟,确保所有图片都已加载
888
902
  });
889
903
  });
890
904
  }
905
+
906
+ // 如果第一页已经加载完成,且当前页不是第一页,也应用自适应宽度
907
+ if (pageNum > 1 && props.autoFitWidth && initialAutoFitScale.value !== null) {
908
+ // 确保后续页面也使用相同的缩放比例
909
+ if (Math.abs(scale.value - initialAutoFitScale.value) > 0.01) {
910
+ scale.value = initialAutoFitScale.value;
911
+ }
912
+ }
891
913
 
892
914
  emit("load", {
893
915
  width: img.naturalWidth,
@@ -959,7 +981,7 @@ const calculateFontSize = (
959
981
  };
960
982
 
961
983
  /**
962
- * 渲染文本图层(使用 blocksData 数据)
984
+ * 渲染文本图层
963
985
  */
964
986
  const renderTextLayer = (pageNum?: number) => {
965
987
  const targetPage = pageNum || currentPage.value;
@@ -970,15 +992,35 @@ const renderTextLayer = (pageNum?: number) => {
970
992
  return;
971
993
  }
972
994
 
995
+ // 如果图片还没加载完成,等待加载完成后再渲染
996
+ if (!image.complete || image.naturalWidth === 0) {
997
+ return;
998
+ }
999
+
1000
+ // 如果没有提供分块数据,跳过渲染(不标记为已渲染)
1001
+ if (!props.blocksData || props.blocksData.length === 0) {
1002
+ return;
1003
+ }
1004
+
973
1005
  const pageBlocksData = getPageBlocksData(targetPage);
974
- console.log("renderTextLayer", targetPage, pageBlocksData);
975
1006
 
976
- // 如果没有提供分块数据,跳过渲染
1007
+ // 如果当前页面没有数据,跳过渲染(不标记为已渲染)
977
1008
  if (!pageBlocksData || pageBlocksData.length === 0) {
978
- textLayer.innerHTML = "";
979
1009
  return;
980
1010
  }
981
1011
 
1012
+ // 如果页面已渲染且用户正在交互,跳过渲染(避免打断用户)
1013
+ if (renderedPages.value.has(targetPage) && showAnnotationPopup.value) {
1014
+ return;
1015
+ }
1016
+
1017
+ // 如果页面已渲染,但当前有数据,需要检查是否真的渲染过
1018
+ // 如果textLayer是空的,说明之前没有真正渲染过,应该重新渲染
1019
+ if (renderedPages.value.has(targetPage) && textLayer.children.length === 0) {
1020
+ // 从已渲染集合中移除,允许重新渲染
1021
+ renderedPages.value.delete(targetPage);
1022
+ }
1023
+
982
1024
  try {
983
1025
  // 设置文本图层的尺寸与图片尺寸一致
984
1026
  textLayer.style.width = `${image.naturalWidth}px`;
@@ -987,7 +1029,19 @@ const renderTextLayer = (pageNum?: number) => {
987
1029
  // 清空文本图层
988
1030
  textLayer.innerHTML = "";
989
1031
 
990
- // 使用指定页码的 blocksData 创建可交互的块
1032
+ // 如果清空前有激活的文本块,且该文本块属于当前页面,则清除引用
1033
+ if (activeBlockDiv.value) {
1034
+ const activePageElement = activeBlockDiv.value.closest(
1035
+ ".image-page-container"
1036
+ );
1037
+ const currentPageElement = textLayer.closest(".image-page-container");
1038
+ if (activePageElement === currentPageElement) {
1039
+ activeBlockDiv.value = null;
1040
+ showAnnotationPopup.value = false;
1041
+ }
1042
+ }
1043
+
1044
+ // 使用 blocksData 数据创建可交互的块
991
1045
  pageBlocksData.forEach((block, index) => {
992
1046
  const { content, bbox } = block;
993
1047
 
@@ -996,14 +1050,11 @@ const renderTextLayer = (pageNum?: number) => {
996
1050
  const width = x2 - x1;
997
1051
  const height = y2 - y1;
998
1052
 
999
- // 自动计算字体大小
1000
- const calculatedFontSize = calculateFontSize(content, width, height);
1001
-
1002
- // 创建文本块
1053
+ // 创建文本块(不显示内容,只用于 hover 交互和批注功能)
1003
1054
  const blockDiv = document.createElement("div");
1004
1055
  blockDiv.className = "text-block";
1005
- blockDiv.dataset.text = content;
1006
1056
  blockDiv.dataset.bbox = JSON.stringify(bbox);
1057
+ blockDiv.dataset.page = String(targetPage);
1007
1058
 
1008
1059
  // 设置基础样式
1009
1060
  blockDiv.style.position = "absolute";
@@ -1011,38 +1062,16 @@ const renderTextLayer = (pageNum?: number) => {
1011
1062
  blockDiv.style.top = `${y1}px`;
1012
1063
  blockDiv.style.width = `${width}px`;
1013
1064
  blockDiv.style.height = `${height}px`;
1014
- blockDiv.style.zIndex = "20"; // 确保文本块在图片上方
1015
- blockDiv.style.cursor = "text"; // 改为文本选择光标
1016
- // 设置文本内容(使用 textContent 而不是 innerHTML,避免 XSS 风险)
1017
- blockDiv.textContent = content;
1018
- // 设置文本样式,确保可以选择和显示
1019
- blockDiv.style.color = "red"; // 红色文字
1020
- blockDiv.style.fontSize = `${calculatedFontSize}px`; // 使用计算出的字体大小
1021
- blockDiv.style.fontFamily = "Arial, sans-serif"; // 设置明确的字体
1022
- blockDiv.style.lineHeight = "1.2"; // 设置合适的行高
1023
- blockDiv.style.whiteSpace = "pre-wrap"; // 保留换行和空格
1024
- blockDiv.style.overflow = "visible"; // 确保文字不被裁剪
1025
- blockDiv.style.display = "block"; // 确保是块级元素
1026
- blockDiv.style.visibility = "visible"; // 确保可见
1027
- // 允许文本选择
1028
- blockDiv.style.userSelect = "text";
1029
- blockDiv.style.webkitUserSelect = "text";
1030
- blockDiv.style.mozUserSelect = "text";
1031
- blockDiv.style.msUserSelect = "text";
1032
- // 允许指针事件,但不阻止文本选择
1065
+ blockDiv.style.zIndex = "20";
1066
+ blockDiv.style.cursor = "pointer";
1067
+ blockDiv.style.borderRadius = "2px";
1068
+ blockDiv.style.transition = "all 0.2s ease";
1069
+ blockDiv.style.backgroundColor = "transparent";
1033
1070
  blockDiv.style.pointerEvents = "auto";
1034
-
1035
- // 右键菜单:显示批注按钮
1036
- blockDiv.addEventListener("contextmenu", (e) => {
1037
- e.preventDefault();
1038
- // 设置当前文本块为激活状态
1039
- activeBlockDiv.value = blockDiv;
1040
- // 显示批注按钮
1041
- showAnnotationButtonForBlock(e, blockDiv);
1042
- });
1071
+ blockDiv.style.userSelect = "none";
1043
1072
 
1044
1073
  // 检查是否有已有批注,如果有则显示批注标记
1045
- const existingAnnotation = getAnnotationForBlock(bbox);
1074
+ const existingAnnotation = getAnnotationForBlock(bbox, targetPage);
1046
1075
  if (existingAnnotation) {
1047
1076
  // 添加批注标记样式类
1048
1077
  blockDiv.classList.add("has-annotation");
@@ -1073,13 +1102,57 @@ const renderTextLayer = (pageNum?: number) => {
1073
1102
  annotationMarker.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.25)";
1074
1103
  annotationMarker.style.lineHeight = "1";
1075
1104
  blockDiv.appendChild(annotationMarker);
1076
- } else {
1077
- // 没有批注时设置为透明背景
1078
- blockDiv.style.backgroundColor = "transparent";
1079
1105
  }
1080
1106
 
1107
+ // Hover 和点击事件
1108
+ blockDiv.addEventListener("mouseenter", (e) => {
1109
+ // 取消之前的隐藏定时器
1110
+ if (hideTimer) {
1111
+ clearTimeout(hideTimer);
1112
+ hideTimer = null;
1113
+ }
1114
+
1115
+ // 如果有之前激活的文本块,先恢复其样式
1116
+ if (activeBlockDiv.value && activeBlockDiv.value !== blockDiv) {
1117
+ restoreBlockStyle(activeBlockDiv.value);
1118
+ }
1119
+
1120
+ // 设置当前文本块为激活状态
1121
+ activeBlockDiv.value = blockDiv;
1122
+
1123
+ // 检查是否有批注,如果有批注,hover 时在批注样式基础上添加蓝色边框
1124
+ const existingAnnotation = getAnnotationForBlock(bbox, targetPage);
1125
+ if (existingAnnotation) {
1126
+ // 如果有批注,保持批注背景色,但添加蓝色边框表示 hover
1127
+ blockDiv.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1128
+ blockDiv.style.setProperty("border", "2px solid rgba(30, 144, 255, 0.8)", "important");
1129
+ blockDiv.style.setProperty("border-radius", "3px", "important");
1130
+ blockDiv.style.setProperty("padding", "1px 3px", "important");
1131
+ 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
+ } else {
1133
+ // 如果没有批注,使用 hover 样式(与 PdfPreview 保持一致)
1134
+ blockDiv.style.backgroundColor =
1135
+ "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
1136
+ blockDiv.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
1137
+ blockDiv.style.borderRadius = "2px";
1138
+ blockDiv.style.padding = "1px 3px";
1139
+ blockDiv.style.border = "none";
1140
+ }
1141
+
1142
+ // 显示批注按钮浮层
1143
+ showAnnotationButtonForBlock(e, blockDiv);
1144
+ });
1145
+
1146
+ blockDiv.addEventListener("mouseleave", () => {
1147
+ // 延迟隐藏,给用户时间移动到批注按钮
1148
+ hideAnnotationButton();
1149
+ });
1150
+
1081
1151
  textLayer.appendChild(blockDiv);
1082
1152
  });
1153
+
1154
+ // 标记该页面已渲染
1155
+ renderedPages.value.add(targetPage);
1083
1156
  } catch (error) {
1084
1157
  console.error("❌ 文本图层渲染失败:", error);
1085
1158
  }
@@ -1089,12 +1162,14 @@ const renderTextLayer = (pageNum?: number) => {
1089
1162
  * 获取文本块对应的批注
1090
1163
  */
1091
1164
  const getAnnotationForBlock = (
1092
- bbox: [number, number, number, number]
1165
+ bbox: [number, number, number, number],
1166
+ pageNum?: number
1093
1167
  ): AnnotationInfo | null => {
1094
1168
  if (!props.annotations || props.annotations.length === 0) {
1095
1169
  return null;
1096
1170
  }
1097
1171
 
1172
+ const targetPage = pageNum !== undefined ? pageNum : currentPage.value;
1098
1173
  const tolerance = 2; // 容差
1099
1174
  return (
1100
1175
  props.annotations.find((annotation) => {
@@ -1105,7 +1180,7 @@ const getAnnotationForBlock = (
1105
1180
  Math.abs(x2 - bbox[2]) < tolerance &&
1106
1181
  Math.abs(y2 - bbox[3]) < tolerance &&
1107
1182
  (annotation.blockPage === undefined ||
1108
- annotation.blockPage === currentPage.value)
1183
+ annotation.blockPage === targetPage)
1109
1184
  );
1110
1185
  }) || null
1111
1186
  );
@@ -1118,12 +1193,26 @@ const showAnnotationButtonForBlock = (
1118
1193
  event: MouseEvent,
1119
1194
  blockDiv: HTMLElement
1120
1195
  ) => {
1121
- const text = blockDiv.dataset.text || "";
1122
1196
  const bboxStr = blockDiv.dataset.bbox || "";
1123
- if (!text || !bboxStr) return;
1197
+ if (!bboxStr) return;
1124
1198
 
1125
1199
  try {
1126
1200
  const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1201
+
1202
+ // 从 blocksData 中查找对应的 content
1203
+ const pageBlocksData = getPageBlocksData(currentPage.value);
1204
+ const blockData = pageBlocksData.find((block) => {
1205
+ const [x1, y1, x2, y2] = block.bbox;
1206
+ const tolerance = 2;
1207
+ return (
1208
+ Math.abs(x1 - bbox[0]) < tolerance &&
1209
+ Math.abs(y1 - bbox[1]) < tolerance &&
1210
+ Math.abs(x2 - bbox[2]) < tolerance &&
1211
+ Math.abs(y2 - bbox[3]) < tolerance
1212
+ );
1213
+ });
1214
+
1215
+ if (!blockData) return;
1127
1216
 
1128
1217
  const rect = blockDiv.getBoundingClientRect();
1129
1218
  const containerRect = containerRef.value?.getBoundingClientRect();
@@ -1206,7 +1295,78 @@ const showAnnotationButtonForBlock = (
1206
1295
  };
1207
1296
 
1208
1297
  /**
1209
- * 隐藏批注按钮
1298
+ * 恢复文本块样式(根据是否有批注)
1299
+ */
1300
+ const restoreBlockStyle = (blockDiv: HTMLElement) => {
1301
+ const bboxStr = blockDiv.dataset.bbox;
1302
+ if (!bboxStr) {
1303
+ console.log("restoreBlockStyle: 没有 bbox");
1304
+ return;
1305
+ }
1306
+
1307
+ try {
1308
+ const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1309
+ const pageStr = blockDiv.dataset.page;
1310
+ const pageNum = pageStr ? parseInt(pageStr, 10) : undefined;
1311
+ const existingAnnotation = getAnnotationForBlock(bbox, pageNum);
1312
+
1313
+ console.log("restoreBlockStyle:", {
1314
+ bbox,
1315
+ pageNum,
1316
+ existingAnnotation,
1317
+ blockDiv: blockDiv,
1318
+ width: blockDiv.style.width,
1319
+ height: blockDiv.style.height,
1320
+ computedStyle: {
1321
+ backgroundColor: window.getComputedStyle(blockDiv).backgroundColor,
1322
+ width: window.getComputedStyle(blockDiv).width,
1323
+ height: window.getComputedStyle(blockDiv).height,
1324
+ display: window.getComputedStyle(blockDiv).display,
1325
+ visibility: window.getComputedStyle(blockDiv).visibility,
1326
+ },
1327
+ inlineStyle: {
1328
+ backgroundColor: blockDiv.style.backgroundColor,
1329
+ border: blockDiv.style.border,
1330
+ },
1331
+ });
1332
+
1333
+ if (existingAnnotation) {
1334
+ // 如果有批注,保持批注样式(使用 !important 确保优先级)
1335
+ blockDiv.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1336
+ blockDiv.style.setProperty("border", "1px solid rgba(255, 193, 7, 0.7)", "important");
1337
+ blockDiv.style.setProperty("border-radius", "3px", "important");
1338
+ blockDiv.style.setProperty("padding", "1px 3px", "important");
1339
+ blockDiv.style.setProperty("box-shadow", "0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1340
+
1341
+ // 确保元素可见
1342
+ blockDiv.style.setProperty("display", "block", "important");
1343
+ blockDiv.style.setProperty("visibility", "visible", "important");
1344
+ blockDiv.style.setProperty("opacity", "1", "important");
1345
+
1346
+ console.log("restoreBlockStyle: 已设置批注样式", {
1347
+ backgroundColor: blockDiv.style.backgroundColor,
1348
+ border: blockDiv.style.border,
1349
+ computedAfter: window.getComputedStyle(blockDiv).backgroundColor,
1350
+ });
1351
+ } else {
1352
+ // 如果没有批注,恢复透明背景
1353
+ blockDiv.style.backgroundColor = "transparent";
1354
+ blockDiv.style.border = "none";
1355
+ blockDiv.style.padding = "0";
1356
+ blockDiv.style.boxShadow = "none";
1357
+ }
1358
+ } catch (error) {
1359
+ console.error("restoreBlockStyle 错误:", error);
1360
+ // 如果解析失败,恢复透明背景
1361
+ blockDiv.style.backgroundColor = "transparent";
1362
+ blockDiv.style.border = "none";
1363
+ blockDiv.style.padding = "0";
1364
+ blockDiv.style.boxShadow = "none";
1365
+ }
1366
+ };
1367
+
1368
+ /**
1369
+ * 隐藏批注按钮和高亮
1210
1370
  */
1211
1371
  const hideAnnotationButton = () => {
1212
1372
  // 如果正在输入批注,不自动隐藏
@@ -1217,9 +1377,41 @@ const hideAnnotationButton = () => {
1217
1377
  hideTimer = setTimeout(() => {
1218
1378
  showAnnotationPopup.value = false;
1219
1379
 
1220
- if (activeBlockDiv.value && !isHighlighted.value) {
1221
- activeBlockDiv.value.style.backgroundColor = "transparent";
1222
- activeBlockDiv.value.style.boxShadow = "none";
1380
+ // 恢复文本块的样式(如果有批注则保持批注样式)
1381
+ if (activeBlockDiv.value) {
1382
+ const bboxStr = activeBlockDiv.value.dataset.bbox;
1383
+ if (bboxStr) {
1384
+ try {
1385
+ const bbox = JSON.parse(bboxStr) as [
1386
+ number,
1387
+ number,
1388
+ number,
1389
+ number
1390
+ ];
1391
+ const existingAnnotation = getAnnotationForBlock(bbox);
1392
+ if (existingAnnotation) {
1393
+ // 如果有批注,恢复批注样式
1394
+ activeBlockDiv.value.style.backgroundColor =
1395
+ "rgba(255, 243, 205, 0.5)";
1396
+ activeBlockDiv.value.style.border =
1397
+ "1px solid rgba(255, 193, 7, 0.7)";
1398
+ activeBlockDiv.value.style.padding = "1px 3px";
1399
+ activeBlockDiv.value.style.boxShadow =
1400
+ "0 1px 2px rgba(255, 193, 7, 0.25)";
1401
+ } else {
1402
+ // 如果没有批注,恢复透明背景
1403
+ activeBlockDiv.value.style.backgroundColor = "transparent";
1404
+ activeBlockDiv.value.style.border = "none";
1405
+ activeBlockDiv.value.style.padding = "0";
1406
+ activeBlockDiv.value.style.boxShadow = "none";
1407
+ }
1408
+ } catch (error) {
1409
+ activeBlockDiv.value.style.backgroundColor = "transparent";
1410
+ activeBlockDiv.value.style.border = "none";
1411
+ activeBlockDiv.value.style.padding = "0";
1412
+ activeBlockDiv.value.style.boxShadow = "none";
1413
+ }
1414
+ }
1223
1415
  activeBlockDiv.value = null;
1224
1416
  }
1225
1417
  }, 300);
@@ -1244,14 +1436,36 @@ const openAnnotationInput = (e?: Event) => {
1244
1436
  e.stopPropagation();
1245
1437
  }
1246
1438
 
1247
- // 获取选中的文本
1439
+ // 获取选中的文本或从文本块中获取
1248
1440
  const selection = window.getSelection();
1249
1441
  let selectedText = "";
1250
1442
 
1251
1443
  if (selection && selection.toString().trim().length > 0) {
1252
1444
  selectedText = selection.toString().trim();
1253
1445
  } else if (activeBlockDiv.value) {
1254
- selectedText = activeBlockDiv.value.dataset.text || "";
1446
+ // blocksData 中查找对应的 content
1447
+ const bboxStr = activeBlockDiv.value.dataset.bbox || "";
1448
+ if (bboxStr) {
1449
+ try {
1450
+ const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1451
+ const pageBlocksData = getPageBlocksData(currentPage.value);
1452
+ const blockData = pageBlocksData.find((block) => {
1453
+ const [x1, y1, x2, y2] = block.bbox;
1454
+ const tolerance = 2;
1455
+ return (
1456
+ Math.abs(x1 - bbox[0]) < tolerance &&
1457
+ Math.abs(y1 - bbox[1]) < tolerance &&
1458
+ Math.abs(x2 - bbox[2]) < tolerance &&
1459
+ Math.abs(y2 - bbox[3]) < tolerance
1460
+ );
1461
+ });
1462
+ if (blockData) {
1463
+ selectedText = blockData.content || "";
1464
+ }
1465
+ } catch (error) {
1466
+ console.error("解析 bbox 失败:", error);
1467
+ }
1468
+ }
1255
1469
  }
1256
1470
 
1257
1471
  if (!selectedText && !activeBlockDiv.value) return;
@@ -1412,6 +1626,12 @@ const closeAnnotationInput = () => {
1412
1626
  currentAnnotationBlock.value = null;
1413
1627
  annotationInput.value = "";
1414
1628
  showAnnotationPopup.value = false;
1629
+
1630
+ // 关闭批注输入弹窗后,恢复文本块的样式
1631
+ if (activeBlockDiv.value && !isHighlighted.value) {
1632
+ restoreBlockStyle(activeBlockDiv.value);
1633
+ activeBlockDiv.value = null;
1634
+ }
1415
1635
  };
1416
1636
 
1417
1637
  /**
@@ -1777,65 +1997,58 @@ const jumpToPosition = (
1777
1997
  };
1778
1998
 
1779
1999
  /**
1780
- * 监听 blocksData 变化,重新渲染文本图层
2000
+ * 监听 blocksData 变化,增量渲染所有页面的文本图层(避免闪烁)
1781
2001
  */
1782
2002
  watch(
1783
2003
  () => props.blocksData,
1784
2004
  () => {
1785
2005
  nextTick(() => {
1786
- renderTextLayer();
2006
+ // 渲染所有页面的文本图层
2007
+ for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
2008
+ renderTextLayer(pageNum);
2009
+ }
1787
2010
  });
1788
2011
  },
1789
- { deep: true, immediate: false }
2012
+ { deep: true, immediate: true }
1790
2013
  );
1791
2014
 
1792
2015
  /**
1793
- * 监听 annotations 变化,重新渲染文本图层以显示批注标记
2016
+ * 监听 annotations 变化,增量更新所有页面的文本图层以显示批注标记
1794
2017
  */
1795
2018
  watch(
1796
2019
  () => props.annotations,
1797
2020
  () => {
1798
2021
  nextTick(() => {
1799
- renderTextLayer();
2022
+ // 更新所有页面的文本图层
2023
+ for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
2024
+ renderTextLayer(pageNum);
2025
+ }
1800
2026
  });
1801
2027
  },
1802
2028
  { deep: true }
1803
2029
  );
1804
2030
 
1805
2031
  /**
1806
- * 监听当前页码变化,重新渲染文本图层
2032
+ * 监听当前页码变化,确保当前页的文本图层已渲染
1807
2033
  */
1808
2034
  watch(
1809
2035
  () => currentPage.value,
1810
2036
  () => {
1811
2037
  nextTick(() => {
1812
- renderTextLayer();
2038
+ renderTextLayer(currentPage.value);
1813
2039
  });
1814
2040
  }
1815
2041
  );
1816
2042
 
1817
2043
  /**
1818
- * 监听当前页码的blocksData变化,重新渲染文本图层
1819
- */
1820
- watch(
1821
- () => currentPageBlocksData.value,
1822
- () => {
1823
- nextTick(() => {
1824
- renderTextLayer();
1825
- });
1826
- },
1827
- { deep: true }
1828
- );
1829
-
1830
- /**
1831
- * 监听图片尺寸变化,重新渲染文本图层
2044
+ * 监听图片尺寸变化,重新渲染当前页的文本图层
1832
2045
  */
1833
2046
  watch(
1834
2047
  () => imageSize.value,
1835
2048
  () => {
1836
2049
  if (imageSize.value.width > 0 && imageSize.value.height > 0) {
1837
2050
  nextTick(() => {
1838
- renderTextLayer();
2051
+ renderTextLayer(currentPage.value);
1839
2052
  });
1840
2053
  }
1841
2054
  },
@@ -1858,18 +2071,23 @@ watch(
1858
2071
  * 组件挂载时的初始化
1859
2072
  */
1860
2073
  onMounted(() => {
1861
- // 如果图片已经加载完成,立即渲染文本图层和自适应宽度
1862
- if (imageRef.value && imageRef.value.complete) {
1863
- nextTick(() => {
1864
- // 如果启用自适应宽度,计算并设置初始缩放比例
1865
- if (props.autoFitWidth && imageSize.value.width > 0) {
1866
- const autoScale = calculateAutoFitScale();
1867
- if (autoScale !== 1) {
1868
- scale.value = autoScale;
1869
- }
1870
- }
1871
- renderTextLayer();
1872
- });
2074
+ // 如果第一页图片已经加载完成,立即计算自适应宽度
2075
+ const firstPageImage = imageRefs.get(1);
2076
+ if (firstPageImage && firstPageImage.complete && props.autoFitWidth) {
2077
+ const firstPageSize = imageSizes.get(1);
2078
+ if (firstPageSize && firstPageSize.width > 0) {
2079
+ nextTick(() => {
2080
+ nextTick(() => {
2081
+ setTimeout(() => {
2082
+ const autoScale = calculateAutoFitScale();
2083
+ if (autoScale !== 1 && autoScale > 0) {
2084
+ scale.value = autoScale;
2085
+ initialAutoFitScale.value = autoScale;
2086
+ }
2087
+ }, 100);
2088
+ });
2089
+ });
2090
+ }
1873
2091
  }
1874
2092
  });
1875
2093