@koi-br/ocr-web-sdk 1.0.43 → 1.0.45
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/README.md +117 -0
- package/dist/{index-D0vHyxHV.mjs → index-Dr29vR47.mjs} +9556 -9126
- package/dist/{index-DUXpkXKO.js → index-oglARFSv.js} +89 -89
- package/dist/index.cjs.js +2 -2
- package/dist/index.esm.js +11 -9
- package/dist/src/composables/useSyncScroll.d.ts +111 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/{tiff.min-CTWTP_jY.js → tiff.min-BZS1xd8w.js} +1 -1
- package/dist/{tiff.min-BzP8AM0k.mjs → tiff.min-FlWYar5H.mjs} +1 -1
- package/package.json +1 -1
- package/preview/ImagePreview.vue +682 -128
package/preview/ImagePreview.vue
CHANGED
|
@@ -12,12 +12,7 @@
|
|
|
12
12
|
<div class="toolbar-group">
|
|
13
13
|
<span class="scale-text"> {{ Math.round(scale * 100) }}% </span>
|
|
14
14
|
<div class="toolbar-divider"></div>
|
|
15
|
-
<ATooltip
|
|
16
|
-
v-if="showResetButton"
|
|
17
|
-
mini
|
|
18
|
-
position="bottom"
|
|
19
|
-
content="重置"
|
|
20
|
-
>
|
|
15
|
+
<ATooltip v-if="showResetButton" mini position="bottom" content="重置">
|
|
21
16
|
<AButton size="small" type="outline" @click="reset">
|
|
22
17
|
<RefreshCcw :size="16" />
|
|
23
18
|
</AButton>
|
|
@@ -96,7 +91,7 @@
|
|
|
96
91
|
<AButton
|
|
97
92
|
size="small"
|
|
98
93
|
type="outline"
|
|
99
|
-
style="padding-right: 0px
|
|
94
|
+
style="padding-right: 0px"
|
|
100
95
|
:disabled="currentPage >= totalPages"
|
|
101
96
|
@click="goToNextPage"
|
|
102
97
|
>
|
|
@@ -116,13 +111,24 @@
|
|
|
116
111
|
@mouseleave="stopPan"
|
|
117
112
|
@scroll="handleScroll"
|
|
118
113
|
>
|
|
119
|
-
|
|
114
|
+
<!-- 自适应宽度计算 Loading -->
|
|
115
|
+
<div v-if="isCalculatingAutoFit && autoFitWidth" class="auto-fit-loading">
|
|
116
|
+
<div class="loading-spinner"></div>
|
|
117
|
+
<div class="loading-text">加载中...</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
class="image-wrapper-container"
|
|
122
|
+
:style="containerStyle"
|
|
123
|
+
:class="{ 'image-hidden': !isImageReady && autoFitWidth }"
|
|
124
|
+
>
|
|
120
125
|
<!-- 渲染所有图片页面 -->
|
|
121
126
|
<div
|
|
122
127
|
v-for="(imageUrl, pageIndex) in imageUrls"
|
|
123
128
|
:key="pageIndex"
|
|
124
129
|
:data-page-number="pageIndex + 1"
|
|
125
130
|
class="image-page-container"
|
|
131
|
+
:style="getPageContainerStyle(pageIndex + 1)"
|
|
126
132
|
>
|
|
127
133
|
<div
|
|
128
134
|
class="image-wrapper"
|
|
@@ -151,7 +157,10 @@
|
|
|
151
157
|
|
|
152
158
|
<!-- 文本图层(用于文本块选择和定位) -->
|
|
153
159
|
<div
|
|
154
|
-
v-if="
|
|
160
|
+
v-if="
|
|
161
|
+
getPageBlocksData(pageIndex + 1) &&
|
|
162
|
+
getPageBlocksData(pageIndex + 1).length > 0
|
|
163
|
+
"
|
|
155
164
|
:ref="(el) => setTextLayerRef(el, pageIndex + 1)"
|
|
156
165
|
class="text-layer"
|
|
157
166
|
></div>
|
|
@@ -389,12 +398,48 @@ const position = ref({ x: 0, y: 0 });
|
|
|
389
398
|
const isPanning = ref(false);
|
|
390
399
|
const lastPosition = ref({ x: 0, y: 0 });
|
|
391
400
|
const initialAutoFitScale = ref<number | null>(null); // 记录初始自适应缩放比例
|
|
401
|
+
const isCalculatingAutoFit = ref(false); // 标记是否正在计算自适应宽度(显示 loading)
|
|
402
|
+
const isImageReady = ref(false); // 标记图片是否已准备好显示(自适应宽度计算完成)
|
|
403
|
+
|
|
404
|
+
// 监听图片URL变化,当有新图片时立即隐藏(等待自适应宽度计算)
|
|
405
|
+
watch(
|
|
406
|
+
() => imageUrls.value,
|
|
407
|
+
(newUrls, oldUrls) => {
|
|
408
|
+
console.log("[ImagePreview] imageUrls changed:", {
|
|
409
|
+
newUrls: newUrls?.length,
|
|
410
|
+
oldUrls: oldUrls?.length,
|
|
411
|
+
autoFitWidth: props.autoFitWidth,
|
|
412
|
+
isImageReady: isImageReady.value,
|
|
413
|
+
isCalculatingAutoFit: isCalculatingAutoFit.value,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// 如果有新的图片URL,且启用自适应宽度,立即隐藏图片
|
|
417
|
+
if (newUrls && newUrls.length > 0 && props.autoFitWidth) {
|
|
418
|
+
console.log("[ImagePreview] 设置图片隐藏,等待自适应宽度计算");
|
|
419
|
+
isImageReady.value = false;
|
|
420
|
+
isCalculatingAutoFit.value = true;
|
|
421
|
+
} else if (!props.autoFitWidth) {
|
|
422
|
+
// 如果没有启用自适应宽度,立即显示
|
|
423
|
+
console.log("[ImagePreview] 未启用自适应宽度,立即显示图片");
|
|
424
|
+
isImageReady.value = true;
|
|
425
|
+
isCalculatingAutoFit.value = false;
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{ immediate: true }
|
|
429
|
+
);
|
|
392
430
|
const isUserZooming = ref(false); // 标记用户是否主动缩放
|
|
393
431
|
|
|
394
432
|
// 滚动翻页相关
|
|
395
433
|
let scrollPagingTimer: any = null;
|
|
396
434
|
const isScrollPaging = ref(false); // 标记是否正在进行滚动翻页
|
|
397
435
|
|
|
436
|
+
// ResizeObserver 相关
|
|
437
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
438
|
+
let resizeTimer: any = null; // handleContainerResize 内部的定时器
|
|
439
|
+
let resizeDebounceTimer: any = null; // ResizeObserver 的防抖定时器
|
|
440
|
+
let isResizing = false; // 标记是否正在处理 resize
|
|
441
|
+
let lastContainerWidth = 0; // 记录上次容器宽度,用于判断是否真的变化了
|
|
442
|
+
|
|
398
443
|
// 图片和容器引用
|
|
399
444
|
const containerRef = ref<HTMLElement>();
|
|
400
445
|
const imageRefs = new Map<number, HTMLImageElement>();
|
|
@@ -441,10 +486,10 @@ const imageSize = computed({
|
|
|
441
486
|
// 在自适应宽度模式下,使用第一页的尺寸;否则使用当前页的尺寸
|
|
442
487
|
const scaledImageSize = computed(() => {
|
|
443
488
|
// 如果启用自适应宽度,使用第一页的尺寸作为基准
|
|
444
|
-
const baseSize = props.autoFitWidth
|
|
445
|
-
?
|
|
489
|
+
const baseSize = props.autoFitWidth
|
|
490
|
+
? imageSizes.get(1) || { width: 0, height: 0 }
|
|
446
491
|
: imageSize.value;
|
|
447
|
-
|
|
492
|
+
|
|
448
493
|
if (baseSize.width === 0 || baseSize.height === 0) {
|
|
449
494
|
return { width: 0, height: 0 };
|
|
450
495
|
}
|
|
@@ -490,6 +535,7 @@ const annotationInput = ref(""); // 批注输入内容
|
|
|
490
535
|
const currentAnnotationBlock = ref<{
|
|
491
536
|
bbox: [number, number, number, number];
|
|
492
537
|
content: string;
|
|
538
|
+
annotationId?: string; // 已有批注的ID(如果存在)
|
|
493
539
|
} | null>(null); // 当前正在添加批注的文本块
|
|
494
540
|
const annotationPopupRef = ref<HTMLElement>(); // 批注弹窗引用
|
|
495
541
|
|
|
@@ -514,6 +560,35 @@ const getPageBlocksData = (pageNo: number) => {
|
|
|
514
560
|
return props.blocksData.filter((block) => block.pageNo === pageNo);
|
|
515
561
|
};
|
|
516
562
|
|
|
563
|
+
// 计算指定页面缩放后的尺寸(考虑旋转)
|
|
564
|
+
const getPageScaledSize = (pageNo: number) => {
|
|
565
|
+
const pageSize = imageSizes.get(pageNo);
|
|
566
|
+
if (!pageSize || pageSize.width === 0 || pageSize.height === 0) {
|
|
567
|
+
return { width: 0, height: 0 };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const { width, height } = pageSize;
|
|
571
|
+
const scaledWidth = width * scale.value;
|
|
572
|
+
const scaledHeight = height * scale.value;
|
|
573
|
+
|
|
574
|
+
// 如果旋转了 90 度或 270 度,交换宽高
|
|
575
|
+
const normalizedRotation = ((rotation.value % 360) + 360) % 360;
|
|
576
|
+
const isRotated = normalizedRotation === 90 || normalizedRotation === 270;
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
width: isRotated ? scaledHeight : scaledWidth,
|
|
580
|
+
height: isRotated ? scaledWidth : scaledHeight,
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// 获取页面容器的样式(响应式,会根据缩放和旋转自动更新)
|
|
585
|
+
const getPageContainerStyle = (pageNo: number) => {
|
|
586
|
+
const scaledSize = getPageScaledSize(pageNo);
|
|
587
|
+
return {
|
|
588
|
+
height: scaledSize.height > 0 ? `${scaledSize.height}px` : "auto",
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
|
|
517
592
|
// 隐藏定时器
|
|
518
593
|
let hideTimer: any = null;
|
|
519
594
|
|
|
@@ -807,10 +882,18 @@ const switchToPage = (page: number) => {
|
|
|
807
882
|
`[data-page-number="${page}"]`
|
|
808
883
|
) as HTMLElement;
|
|
809
884
|
if (pageElement) {
|
|
885
|
+
// 标记这是翻页滚动,不应该被同步滚动干扰
|
|
886
|
+
containerRef.value.dataset.pageScrolling = "true";
|
|
810
887
|
pageElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
811
888
|
// 更新 lastScrollTop,确保滚动方向判断准确
|
|
812
889
|
nextTick(() => {
|
|
813
890
|
lastScrollTop = containerRef.value?.scrollTop || 0;
|
|
891
|
+
// 延迟清除标记,确保滚动完成
|
|
892
|
+
setTimeout(() => {
|
|
893
|
+
if (containerRef.value) {
|
|
894
|
+
delete containerRef.value.dataset.pageScrolling;
|
|
895
|
+
}
|
|
896
|
+
}, 500); // scrollIntoView 的 smooth 动画通常需要 300-500ms
|
|
814
897
|
});
|
|
815
898
|
}
|
|
816
899
|
}
|
|
@@ -835,7 +918,7 @@ const reset = () => {
|
|
|
835
918
|
annotationInput.value = "";
|
|
836
919
|
activeBlockDiv.value = null;
|
|
837
920
|
isHighlighted.value = false;
|
|
838
|
-
|
|
921
|
+
|
|
839
922
|
// 清除已渲染页面集合
|
|
840
923
|
renderedPages.value.clear();
|
|
841
924
|
};
|
|
@@ -846,13 +929,28 @@ const original = () => {
|
|
|
846
929
|
|
|
847
930
|
// 计算自适应宽度的缩放比例
|
|
848
931
|
const calculateAutoFitScale = () => {
|
|
932
|
+
console.log("[ImagePreview] calculateAutoFitScale 开始:", {
|
|
933
|
+
autoFitWidth: props.autoFitWidth,
|
|
934
|
+
hasContainerRef: !!containerRef.value,
|
|
935
|
+
containerRect: containerRef.value?.getBoundingClientRect(),
|
|
936
|
+
firstPageSize: imageSizes.get(1),
|
|
937
|
+
rotation: rotation.value,
|
|
938
|
+
minScale: props.minScale,
|
|
939
|
+
maxScale: props.maxScale,
|
|
940
|
+
});
|
|
941
|
+
|
|
849
942
|
if (!props.autoFitWidth || !containerRef.value) {
|
|
943
|
+
console.log("[ImagePreview] calculateAutoFitScale 返回 1 (条件不满足)");
|
|
850
944
|
return 1;
|
|
851
945
|
}
|
|
852
946
|
|
|
853
947
|
// 使用第一页的图片尺寸作为基准(所有页面使用相同的缩放比例)
|
|
854
948
|
const firstPageSize = imageSizes.get(1);
|
|
855
949
|
if (!firstPageSize || firstPageSize.width === 0) {
|
|
950
|
+
console.log(
|
|
951
|
+
"[ImagePreview] calculateAutoFitScale 返回 1 (第一页尺寸无效)",
|
|
952
|
+
firstPageSize
|
|
953
|
+
);
|
|
856
954
|
return 1;
|
|
857
955
|
}
|
|
858
956
|
|
|
@@ -862,6 +960,10 @@ const calculateAutoFitScale = () => {
|
|
|
862
960
|
const containerWidth = containerRect.width - 4;
|
|
863
961
|
|
|
864
962
|
if (containerWidth <= 0) {
|
|
963
|
+
console.log(
|
|
964
|
+
"[ImagePreview] calculateAutoFitScale 返回 1 (容器宽度无效)",
|
|
965
|
+
containerWidth
|
|
966
|
+
);
|
|
865
967
|
return 1;
|
|
866
968
|
}
|
|
867
969
|
|
|
@@ -872,20 +974,44 @@ const calculateAutoFitScale = () => {
|
|
|
872
974
|
const imageWidth = isRotated ? firstPageSize.height : firstPageSize.width;
|
|
873
975
|
|
|
874
976
|
if (imageWidth <= 0) {
|
|
977
|
+
console.log(
|
|
978
|
+
"[ImagePreview] calculateAutoFitScale 返回 1 (图片宽度无效)",
|
|
979
|
+
imageWidth
|
|
980
|
+
);
|
|
875
981
|
return 1;
|
|
876
982
|
}
|
|
877
983
|
|
|
878
984
|
// 计算缩放比例,使图片宽度完全适应容器宽度
|
|
879
985
|
const calculatedScale = containerWidth / imageWidth;
|
|
986
|
+
const finalScale = Math.max(
|
|
987
|
+
props.minScale,
|
|
988
|
+
Math.min(props.maxScale, calculatedScale)
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
console.log("[ImagePreview] calculateAutoFitScale 计算结果:", {
|
|
992
|
+
containerWidth,
|
|
993
|
+
imageWidth,
|
|
994
|
+
calculatedScale,
|
|
995
|
+
finalScale,
|
|
996
|
+
});
|
|
880
997
|
|
|
881
998
|
// 确保缩放比例在允许的范围内
|
|
882
|
-
return
|
|
999
|
+
return finalScale;
|
|
883
1000
|
};
|
|
884
1001
|
|
|
885
1002
|
// 图片加载完成处理
|
|
886
1003
|
const onImageLoad = (event: Event, pageNum: number) => {
|
|
887
1004
|
const img = event.target as HTMLImageElement;
|
|
888
|
-
|
|
1005
|
+
|
|
1006
|
+
console.log("[ImagePreview] 图片加载完成:", {
|
|
1007
|
+
pageNum,
|
|
1008
|
+
naturalWidth: img.naturalWidth,
|
|
1009
|
+
naturalHeight: img.naturalHeight,
|
|
1010
|
+
autoFitWidth: props.autoFitWidth,
|
|
1011
|
+
isImageReady: isImageReady.value,
|
|
1012
|
+
isCalculatingAutoFit: isCalculatingAutoFit.value,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
889
1015
|
// 存储该页的图片尺寸
|
|
890
1016
|
imageSizes.set(pageNum, {
|
|
891
1017
|
width: img.naturalWidth,
|
|
@@ -894,24 +1020,84 @@ const onImageLoad = (event: Event, pageNum: number) => {
|
|
|
894
1020
|
|
|
895
1021
|
// 如果是第一页且启用自适应宽度,计算并设置初始缩放比例
|
|
896
1022
|
if (pageNum === 1 && props.autoFitWidth) {
|
|
1023
|
+
console.log("[ImagePreview] 第一页加载完成,开始计算自适应宽度");
|
|
897
1024
|
// 重置用户缩放标记
|
|
898
1025
|
isUserZooming.value = false;
|
|
899
1026
|
|
|
1027
|
+
// 确保图片是隐藏的(watch 已经设置了,这里再次确认)
|
|
1028
|
+
if (!isImageReady.value) {
|
|
1029
|
+
isCalculatingAutoFit.value = true;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 设置超时保护,防止一直显示 loading(最多等待 3 秒)
|
|
1033
|
+
const timeoutId = setTimeout(() => {
|
|
1034
|
+
console.warn("自适应宽度计算超时,强制显示图片");
|
|
1035
|
+
isCalculatingAutoFit.value = false;
|
|
1036
|
+
isImageReady.value = true;
|
|
1037
|
+
}, 3000);
|
|
1038
|
+
|
|
900
1039
|
// 使用双重 nextTick 确保容器尺寸已确定
|
|
901
1040
|
nextTick(() => {
|
|
902
1041
|
nextTick(() => {
|
|
903
1042
|
// 添加小延迟确保容器完全渲染
|
|
904
1043
|
setTimeout(() => {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1044
|
+
try {
|
|
1045
|
+
console.log("[ImagePreview] onImageLoad: 开始计算自适应宽度...");
|
|
1046
|
+
const autoScale = calculateAutoFitScale();
|
|
1047
|
+
console.log("[ImagePreview] onImageLoad: 自适应宽度计算结果:", {
|
|
1048
|
+
autoScale,
|
|
1049
|
+
containerRef: !!containerRef.value,
|
|
1050
|
+
containerWidth:
|
|
1051
|
+
containerRef.value?.getBoundingClientRect()?.width,
|
|
1052
|
+
firstPageSize: imageSizes.get(1),
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
if (autoScale > 0) {
|
|
1056
|
+
scale.value = autoScale;
|
|
1057
|
+
initialAutoFitScale.value = autoScale; // 记录初始自适应缩放比例
|
|
1058
|
+
// 记录当前容器宽度,用于后续 resize 检查
|
|
1059
|
+
if (containerRef.value) {
|
|
1060
|
+
lastContainerWidth =
|
|
1061
|
+
containerRef.value.getBoundingClientRect().width;
|
|
1062
|
+
}
|
|
1063
|
+
console.log(
|
|
1064
|
+
"[ImagePreview] onImageLoad: 缩放比例已设置:",
|
|
1065
|
+
autoScale
|
|
1066
|
+
);
|
|
1067
|
+
} else {
|
|
1068
|
+
console.warn(
|
|
1069
|
+
"[ImagePreview] onImageLoad: 计算出的缩放比例无效:",
|
|
1070
|
+
autoScale
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
console.error(
|
|
1075
|
+
"[ImagePreview] onImageLoad: 计算自适应宽度失败:",
|
|
1076
|
+
error
|
|
1077
|
+
);
|
|
1078
|
+
} finally {
|
|
1079
|
+
// 清除超时保护
|
|
1080
|
+
clearTimeout(timeoutId);
|
|
1081
|
+
console.log(
|
|
1082
|
+
"[ImagePreview] onImageLoad: 更新状态: isCalculatingAutoFit = false, isImageReady = true"
|
|
1083
|
+
);
|
|
1084
|
+
// 无论计算结果如何,都要更新状态,避免一直显示 loading
|
|
1085
|
+
isCalculatingAutoFit.value = false;
|
|
1086
|
+
// 使用 requestAnimationFrame 确保在下一帧显示,避免闪烁
|
|
1087
|
+
requestAnimationFrame(() => {
|
|
1088
|
+
isImageReady.value = true;
|
|
1089
|
+
console.log("[ImagePreview] onImageLoad: 图片已准备好显示");
|
|
1090
|
+
});
|
|
909
1091
|
}
|
|
910
1092
|
}, 100); // 增加延迟,确保所有图片都已加载
|
|
911
1093
|
});
|
|
912
1094
|
});
|
|
1095
|
+
} else if (!props.autoFitWidth) {
|
|
1096
|
+
// 如果没有启用自适应宽度,立即显示图片
|
|
1097
|
+
isImageReady.value = true;
|
|
1098
|
+
isCalculatingAutoFit.value = false;
|
|
913
1099
|
}
|
|
914
|
-
|
|
1100
|
+
|
|
915
1101
|
// 如果第一页已经加载完成,且当前页不是第一页,也应用自适应宽度
|
|
916
1102
|
if (pageNum > 1 && props.autoFitWidth && initialAutoFitScale.value !== null) {
|
|
917
1103
|
// 确保后续页面也使用相同的缩放比例
|
|
@@ -1121,9 +1307,13 @@ const renderTextLayer = (pageNum?: number) => {
|
|
|
1121
1307
|
hideTimer = null;
|
|
1122
1308
|
}
|
|
1123
1309
|
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1310
|
+
// 清除当前页面所有文本块的高亮样式(除了当前文本块)
|
|
1311
|
+
// 这样可以防止多个文本块同时高亮的竞态条件
|
|
1312
|
+
clearAllHighlights(blockDiv);
|
|
1313
|
+
|
|
1314
|
+
// 清除跳转高亮标志(如果之前是通过跳转高亮的)
|
|
1315
|
+
if (isHighlighted.value) {
|
|
1316
|
+
isHighlighted.value = false;
|
|
1127
1317
|
}
|
|
1128
1318
|
|
|
1129
1319
|
// 设置当前文本块为激活状态
|
|
@@ -1133,13 +1323,28 @@ const renderTextLayer = (pageNum?: number) => {
|
|
|
1133
1323
|
const existingAnnotation = getAnnotationForBlock(bbox, targetPage);
|
|
1134
1324
|
if (existingAnnotation) {
|
|
1135
1325
|
// 如果有批注,保持批注背景色,但添加蓝色边框表示 hover
|
|
1136
|
-
blockDiv.style.setProperty(
|
|
1137
|
-
|
|
1326
|
+
blockDiv.style.setProperty(
|
|
1327
|
+
"background-color",
|
|
1328
|
+
"rgba(255, 243, 205, 0.5)",
|
|
1329
|
+
"important"
|
|
1330
|
+
);
|
|
1331
|
+
blockDiv.style.setProperty(
|
|
1332
|
+
"border",
|
|
1333
|
+
"2px solid rgba(30, 144, 255, 0.8)",
|
|
1334
|
+
"important"
|
|
1335
|
+
);
|
|
1138
1336
|
blockDiv.style.setProperty("border-radius", "3px", "important");
|
|
1139
1337
|
blockDiv.style.setProperty("padding", "1px 3px", "important");
|
|
1140
|
-
blockDiv.style.setProperty(
|
|
1338
|
+
blockDiv.style.setProperty(
|
|
1339
|
+
"box-shadow",
|
|
1340
|
+
"0 0 0 2px rgba(30, 144, 255, 0.6), 0 1px 2px rgba(255, 193, 7, 0.25)",
|
|
1341
|
+
"important"
|
|
1342
|
+
);
|
|
1141
1343
|
} else {
|
|
1142
|
-
//
|
|
1344
|
+
// 如果没有批注,先清除可能残留的样式,再设置 hover 样式(与 PdfPreview 保持一致)
|
|
1345
|
+
blockDiv.style.removeProperty("background-color");
|
|
1346
|
+
blockDiv.style.removeProperty("border");
|
|
1347
|
+
blockDiv.style.removeProperty("box-shadow");
|
|
1143
1348
|
blockDiv.style.backgroundColor =
|
|
1144
1349
|
"var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
|
|
1145
1350
|
blockDiv.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
|
|
@@ -1207,7 +1412,7 @@ const showAnnotationButtonForBlock = (
|
|
|
1207
1412
|
|
|
1208
1413
|
try {
|
|
1209
1414
|
const bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1210
|
-
|
|
1415
|
+
|
|
1211
1416
|
// 从 blocksData 中查找对应的 content
|
|
1212
1417
|
const pageBlocksData = getPageBlocksData(currentPage.value);
|
|
1213
1418
|
const blockData = pageBlocksData.find((block) => {
|
|
@@ -1220,7 +1425,7 @@ const showAnnotationButtonForBlock = (
|
|
|
1220
1425
|
Math.abs(y2 - bbox[3]) < tolerance
|
|
1221
1426
|
);
|
|
1222
1427
|
});
|
|
1223
|
-
|
|
1428
|
+
|
|
1224
1429
|
if (!blockData) return;
|
|
1225
1430
|
|
|
1226
1431
|
const rect = blockDiv.getBoundingClientRect();
|
|
@@ -1303,6 +1508,90 @@ const showAnnotationButtonForBlock = (
|
|
|
1303
1508
|
}
|
|
1304
1509
|
};
|
|
1305
1510
|
|
|
1511
|
+
/**
|
|
1512
|
+
* 清除指定页面所有文本块的高亮样式(除了指定的文本块)
|
|
1513
|
+
* @param excludeBlockDiv 要排除的文本块(不清除它的样式)
|
|
1514
|
+
* @param pageNum 要清除的页码,如果不提供则从 excludeBlockDiv 中获取
|
|
1515
|
+
*/
|
|
1516
|
+
const clearAllHighlights = (
|
|
1517
|
+
excludeBlockDiv?: HTMLElement,
|
|
1518
|
+
pageNum?: number
|
|
1519
|
+
) => {
|
|
1520
|
+
// 确定要清除的页码
|
|
1521
|
+
let targetPage = pageNum;
|
|
1522
|
+
if (!targetPage && excludeBlockDiv) {
|
|
1523
|
+
const pageStr = excludeBlockDiv.dataset.page;
|
|
1524
|
+
targetPage = pageStr ? parseInt(pageStr, 10) : currentPage.value;
|
|
1525
|
+
}
|
|
1526
|
+
if (!targetPage) {
|
|
1527
|
+
targetPage = currentPage.value;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const textLayer = textLayerRefs.get(targetPage);
|
|
1531
|
+
if (!textLayer) return;
|
|
1532
|
+
|
|
1533
|
+
const blockDivs = textLayer.querySelectorAll(".text-block");
|
|
1534
|
+
blockDivs.forEach((div) => {
|
|
1535
|
+
const el = div as HTMLElement;
|
|
1536
|
+
// 如果是指定的文本块,跳过
|
|
1537
|
+
if (excludeBlockDiv && el === excludeBlockDiv) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// 检查是否有批注
|
|
1542
|
+
const bboxStr = el.dataset.bbox;
|
|
1543
|
+
if (!bboxStr) return;
|
|
1544
|
+
|
|
1545
|
+
try {
|
|
1546
|
+
const bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1547
|
+
const pageStr = el.dataset.page;
|
|
1548
|
+
const pageNum = pageStr ? parseInt(pageStr, 10) : undefined;
|
|
1549
|
+
const existingAnnotation = getAnnotationForBlock(bbox, pageNum);
|
|
1550
|
+
|
|
1551
|
+
if (existingAnnotation) {
|
|
1552
|
+
// 如果有批注,恢复批注样式(不使用 hover 样式)
|
|
1553
|
+
el.style.setProperty(
|
|
1554
|
+
"background-color",
|
|
1555
|
+
"rgba(255, 243, 205, 0.5)",
|
|
1556
|
+
"important"
|
|
1557
|
+
);
|
|
1558
|
+
el.style.setProperty(
|
|
1559
|
+
"border",
|
|
1560
|
+
"1px solid rgba(255, 193, 7, 0.7)",
|
|
1561
|
+
"important"
|
|
1562
|
+
);
|
|
1563
|
+
el.style.setProperty("border-radius", "3px", "important");
|
|
1564
|
+
el.style.setProperty("padding", "1px 3px", "important");
|
|
1565
|
+
el.style.setProperty(
|
|
1566
|
+
"box-shadow",
|
|
1567
|
+
"0 1px 2px rgba(255, 193, 7, 0.25)",
|
|
1568
|
+
"important"
|
|
1569
|
+
);
|
|
1570
|
+
} else {
|
|
1571
|
+
// 如果没有批注,清除所有高亮样式
|
|
1572
|
+
el.style.backgroundColor = "transparent";
|
|
1573
|
+
el.style.border = "none";
|
|
1574
|
+
el.style.borderRadius = "2px";
|
|
1575
|
+
el.style.padding = "0";
|
|
1576
|
+
el.style.boxShadow = "none";
|
|
1577
|
+
el.style.removeProperty("background-color");
|
|
1578
|
+
el.style.removeProperty("border");
|
|
1579
|
+
el.style.removeProperty("box-shadow");
|
|
1580
|
+
}
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
// 如果解析失败,清除所有高亮样式
|
|
1583
|
+
el.style.backgroundColor = "transparent";
|
|
1584
|
+
el.style.border = "none";
|
|
1585
|
+
el.style.borderRadius = "2px";
|
|
1586
|
+
el.style.padding = "0";
|
|
1587
|
+
el.style.boxShadow = "none";
|
|
1588
|
+
el.style.removeProperty("background-color");
|
|
1589
|
+
el.style.removeProperty("border");
|
|
1590
|
+
el.style.removeProperty("box-shadow");
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1306
1595
|
/**
|
|
1307
1596
|
* 恢复文本块样式(根据是否有批注)
|
|
1308
1597
|
*/
|
|
@@ -1341,36 +1630,58 @@ const restoreBlockStyle = (blockDiv: HTMLElement) => {
|
|
|
1341
1630
|
|
|
1342
1631
|
if (existingAnnotation) {
|
|
1343
1632
|
// 如果有批注,保持批注样式(使用 !important 确保优先级)
|
|
1344
|
-
blockDiv.style.setProperty(
|
|
1345
|
-
|
|
1633
|
+
blockDiv.style.setProperty(
|
|
1634
|
+
"background-color",
|
|
1635
|
+
"rgba(255, 243, 205, 0.5)",
|
|
1636
|
+
"important"
|
|
1637
|
+
);
|
|
1638
|
+
blockDiv.style.setProperty(
|
|
1639
|
+
"border",
|
|
1640
|
+
"1px solid rgba(255, 193, 7, 0.7)",
|
|
1641
|
+
"important"
|
|
1642
|
+
);
|
|
1346
1643
|
blockDiv.style.setProperty("border-radius", "3px", "important");
|
|
1347
1644
|
blockDiv.style.setProperty("padding", "1px 3px", "important");
|
|
1348
|
-
blockDiv.style.setProperty(
|
|
1349
|
-
|
|
1645
|
+
blockDiv.style.setProperty(
|
|
1646
|
+
"box-shadow",
|
|
1647
|
+
"0 1px 2px rgba(255, 193, 7, 0.25)",
|
|
1648
|
+
"important"
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1350
1651
|
// 确保元素可见
|
|
1351
1652
|
blockDiv.style.setProperty("display", "block", "important");
|
|
1352
1653
|
blockDiv.style.setProperty("visibility", "visible", "important");
|
|
1353
1654
|
blockDiv.style.setProperty("opacity", "1", "important");
|
|
1354
|
-
|
|
1655
|
+
|
|
1355
1656
|
console.log("restoreBlockStyle: 已设置批注样式", {
|
|
1356
1657
|
backgroundColor: blockDiv.style.backgroundColor,
|
|
1357
1658
|
border: blockDiv.style.border,
|
|
1358
1659
|
computedAfter: window.getComputedStyle(blockDiv).backgroundColor,
|
|
1359
1660
|
});
|
|
1360
1661
|
} else {
|
|
1361
|
-
//
|
|
1662
|
+
// 如果没有批注,恢复透明背景(清除所有高亮相关样式)
|
|
1362
1663
|
blockDiv.style.backgroundColor = "transparent";
|
|
1363
1664
|
blockDiv.style.border = "none";
|
|
1665
|
+
blockDiv.style.borderRadius = "2px"; // 恢复默认值
|
|
1364
1666
|
blockDiv.style.padding = "0";
|
|
1365
1667
|
blockDiv.style.boxShadow = "none";
|
|
1668
|
+
// 清除可能残留的样式属性
|
|
1669
|
+
blockDiv.style.removeProperty("background-color");
|
|
1670
|
+
blockDiv.style.removeProperty("border");
|
|
1671
|
+
blockDiv.style.removeProperty("box-shadow");
|
|
1366
1672
|
}
|
|
1367
1673
|
} catch (error) {
|
|
1368
1674
|
console.error("restoreBlockStyle 错误:", error);
|
|
1369
|
-
//
|
|
1675
|
+
// 如果解析失败,恢复透明背景(清除所有高亮相关样式)
|
|
1370
1676
|
blockDiv.style.backgroundColor = "transparent";
|
|
1371
1677
|
blockDiv.style.border = "none";
|
|
1678
|
+
blockDiv.style.borderRadius = "2px"; // 恢复默认值
|
|
1372
1679
|
blockDiv.style.padding = "0";
|
|
1373
1680
|
blockDiv.style.boxShadow = "none";
|
|
1681
|
+
// 清除可能残留的样式属性
|
|
1682
|
+
blockDiv.style.removeProperty("background-color");
|
|
1683
|
+
blockDiv.style.removeProperty("border");
|
|
1684
|
+
blockDiv.style.removeProperty("box-shadow");
|
|
1374
1685
|
}
|
|
1375
1686
|
};
|
|
1376
1687
|
|
|
@@ -1391,12 +1702,7 @@ const hideAnnotationButton = () => {
|
|
|
1391
1702
|
const bboxStr = activeBlockDiv.value.dataset.bbox;
|
|
1392
1703
|
if (bboxStr) {
|
|
1393
1704
|
try {
|
|
1394
|
-
const bbox = JSON.parse(bboxStr) as [
|
|
1395
|
-
number,
|
|
1396
|
-
number,
|
|
1397
|
-
number,
|
|
1398
|
-
number
|
|
1399
|
-
];
|
|
1705
|
+
const bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1400
1706
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1401
1707
|
if (existingAnnotation) {
|
|
1402
1708
|
// 如果有批注,恢复批注样式
|
|
@@ -1408,19 +1714,32 @@ const hideAnnotationButton = () => {
|
|
|
1408
1714
|
activeBlockDiv.value.style.boxShadow =
|
|
1409
1715
|
"0 1px 2px rgba(255, 193, 7, 0.25)";
|
|
1410
1716
|
} else {
|
|
1411
|
-
//
|
|
1717
|
+
// 如果没有批注,恢复透明背景(清除所有高亮相关样式)
|
|
1412
1718
|
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1413
1719
|
activeBlockDiv.value.style.border = "none";
|
|
1720
|
+
activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
|
|
1414
1721
|
activeBlockDiv.value.style.padding = "0";
|
|
1415
1722
|
activeBlockDiv.value.style.boxShadow = "none";
|
|
1723
|
+
// 清除可能残留的样式属性
|
|
1724
|
+
activeBlockDiv.value.style.removeProperty("background-color");
|
|
1725
|
+
activeBlockDiv.value.style.removeProperty("border");
|
|
1726
|
+
activeBlockDiv.value.style.removeProperty("box-shadow");
|
|
1416
1727
|
}
|
|
1417
1728
|
} catch (error) {
|
|
1729
|
+
// 如果解析失败,恢复透明背景(清除所有高亮相关样式)
|
|
1418
1730
|
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1419
1731
|
activeBlockDiv.value.style.border = "none";
|
|
1732
|
+
activeBlockDiv.value.style.borderRadius = "2px"; // 恢复默认值
|
|
1420
1733
|
activeBlockDiv.value.style.padding = "0";
|
|
1421
1734
|
activeBlockDiv.value.style.boxShadow = "none";
|
|
1735
|
+
// 清除可能残留的样式属性
|
|
1736
|
+
activeBlockDiv.value.style.removeProperty("background-color");
|
|
1737
|
+
activeBlockDiv.value.style.removeProperty("border");
|
|
1738
|
+
activeBlockDiv.value.style.removeProperty("box-shadow");
|
|
1422
1739
|
}
|
|
1423
1740
|
}
|
|
1741
|
+
// 清除高亮标志
|
|
1742
|
+
isHighlighted.value = false;
|
|
1424
1743
|
activeBlockDiv.value = null;
|
|
1425
1744
|
}
|
|
1426
1745
|
}, 300);
|
|
@@ -1526,6 +1845,7 @@ const openAnnotationInput = (e?: Event) => {
|
|
|
1526
1845
|
currentAnnotationBlock.value = {
|
|
1527
1846
|
bbox,
|
|
1528
1847
|
content: selectedText,
|
|
1848
|
+
annotationId: existingAnnotation?.id, // 保存已有批注的ID
|
|
1529
1849
|
};
|
|
1530
1850
|
|
|
1531
1851
|
// 确保弹窗显示
|
|
@@ -1635,7 +1955,7 @@ const closeAnnotationInput = () => {
|
|
|
1635
1955
|
currentAnnotationBlock.value = null;
|
|
1636
1956
|
annotationInput.value = "";
|
|
1637
1957
|
showAnnotationPopup.value = false;
|
|
1638
|
-
|
|
1958
|
+
|
|
1639
1959
|
// 关闭批注输入弹窗后,恢复文本块的样式
|
|
1640
1960
|
if (activeBlockDiv.value && !isHighlighted.value) {
|
|
1641
1961
|
restoreBlockStyle(activeBlockDiv.value);
|
|
@@ -1667,16 +1987,22 @@ const saveAnnotation = () => {
|
|
|
1667
1987
|
return;
|
|
1668
1988
|
}
|
|
1669
1989
|
|
|
1670
|
-
const { bbox, content } = currentAnnotationBlock.value;
|
|
1990
|
+
const { bbox, content, annotationId } = currentAnnotationBlock.value;
|
|
1671
1991
|
const annotationContent = annotationInput.value.trim();
|
|
1672
1992
|
|
|
1673
|
-
//
|
|
1674
|
-
|
|
1993
|
+
// 优先使用保存的ID查找批注,如果没有ID则使用bbox查找(向后兼容)
|
|
1994
|
+
let existingAnnotation: AnnotationInfo | null = null;
|
|
1995
|
+
if (annotationId && props.annotations) {
|
|
1996
|
+
existingAnnotation =
|
|
1997
|
+
props.annotations.find((ann) => ann.id === annotationId) || null;
|
|
1998
|
+
}
|
|
1999
|
+
// 如果没有通过ID找到,则使用bbox查找(向后兼容)
|
|
2000
|
+
if (!existingAnnotation) {
|
|
2001
|
+
existingAnnotation = getAnnotationForBlock(bbox);
|
|
2002
|
+
}
|
|
1675
2003
|
|
|
1676
2004
|
const annotation: AnnotationInfo = {
|
|
1677
|
-
id:
|
|
1678
|
-
existingAnnotation?.id ||
|
|
1679
|
-
`annotation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
2005
|
+
id: existingAnnotation?.id || "",
|
|
1680
2006
|
blockBbox: bbox,
|
|
1681
2007
|
blockContent: content,
|
|
1682
2008
|
blockPage: currentPage.value,
|
|
@@ -1707,6 +2033,33 @@ const saveAnnotation = () => {
|
|
|
1707
2033
|
* 处理滚动事件(隐藏批注按钮和文本块高亮,以及滚动翻页)
|
|
1708
2034
|
*/
|
|
1709
2035
|
const handleScroll = (e: Event) => {
|
|
2036
|
+
const container = e.target as HTMLElement;
|
|
2037
|
+
|
|
2038
|
+
// 检查是否是同步滚动触发的
|
|
2039
|
+
const isSyncing = container?.dataset?.syncingScroll === "true";
|
|
2040
|
+
if (isSyncing) {
|
|
2041
|
+
// 即使是被同步滚动触发的,也应该立即更新页码(但不触发翻页动画)
|
|
2042
|
+
// 使用 requestAnimationFrame 确保在浏览器渲染后立即更新
|
|
2043
|
+
if (
|
|
2044
|
+
props.enableScrollPaging &&
|
|
2045
|
+
totalPages.value > 1 &&
|
|
2046
|
+
!isScrollPaging.value
|
|
2047
|
+
) {
|
|
2048
|
+
// 立即更新页码,不等待防抖
|
|
2049
|
+
requestAnimationFrame(() => {
|
|
2050
|
+
updateCurrentPageFromScroll();
|
|
2051
|
+
// 清除标记(在页码更新后)
|
|
2052
|
+
delete container.dataset.syncingScroll;
|
|
2053
|
+
});
|
|
2054
|
+
} else {
|
|
2055
|
+
// 如果没有启用滚动翻页,立即清除标记
|
|
2056
|
+
delete container.dataset.syncingScroll;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// 同步滚动时,不执行其他逻辑(如隐藏批注按钮等)
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1710
2063
|
if (hideTimer) {
|
|
1711
2064
|
clearTimeout(hideTimer);
|
|
1712
2065
|
}
|
|
@@ -1728,12 +2081,17 @@ const handleScroll = (e: Event) => {
|
|
|
1728
2081
|
scale.value = initialAutoFitScale.value;
|
|
1729
2082
|
}
|
|
1730
2083
|
|
|
1731
|
-
//
|
|
2084
|
+
// 滚动翻页功能(用户主动滚动时)
|
|
1732
2085
|
if (
|
|
1733
2086
|
props.enableScrollPaging &&
|
|
1734
2087
|
totalPages.value > 1 &&
|
|
1735
2088
|
!isScrollPaging.value
|
|
1736
2089
|
) {
|
|
2090
|
+
// 立即更新页码(使用 requestAnimationFrame 确保在渲染后更新)
|
|
2091
|
+
requestAnimationFrame(() => {
|
|
2092
|
+
updateCurrentPageFromScroll();
|
|
2093
|
+
});
|
|
2094
|
+
// 同时使用防抖处理其他逻辑(如事件触发)
|
|
1737
2095
|
handleScrollPaging(e);
|
|
1738
2096
|
}
|
|
1739
2097
|
};
|
|
@@ -1741,10 +2099,55 @@ const handleScroll = (e: Event) => {
|
|
|
1741
2099
|
// 记录上次滚动位置,用于判断滚动方向
|
|
1742
2100
|
let lastScrollTop = 0;
|
|
1743
2101
|
|
|
2102
|
+
/**
|
|
2103
|
+
* 根据滚动位置更新当前页码
|
|
2104
|
+
* 通过找到距离视口中心最近的页面来确定当前页
|
|
2105
|
+
* 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
|
|
2106
|
+
*/
|
|
2107
|
+
const updateCurrentPageFromScroll = () => {
|
|
2108
|
+
if (!containerRef.value) return;
|
|
2109
|
+
|
|
2110
|
+
const container = containerRef.value;
|
|
2111
|
+
const containerRect = container.getBoundingClientRect();
|
|
2112
|
+
const containerTop = containerRect.top;
|
|
2113
|
+
const containerHeight = container.clientHeight;
|
|
2114
|
+
const containerCenter = containerTop + containerHeight / 2;
|
|
2115
|
+
|
|
2116
|
+
// 遍历所有页面,找到距离视口中心最近的页面
|
|
2117
|
+
let closestPage = 1;
|
|
2118
|
+
let closestDistance = Infinity;
|
|
2119
|
+
|
|
2120
|
+
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
2121
|
+
const pageElement = container.querySelector(
|
|
2122
|
+
`[data-page-number="${pageNum}"]`
|
|
2123
|
+
) as HTMLElement;
|
|
2124
|
+
|
|
2125
|
+
if (pageElement) {
|
|
2126
|
+
// 使用 getBoundingClientRect() 获取实际渲染位置(考虑了 transform: scale)
|
|
2127
|
+
// 而不是使用 offsetTop(不考虑 transform)
|
|
2128
|
+
const pageRect = pageElement.getBoundingClientRect();
|
|
2129
|
+
const pageTop = pageRect.top;
|
|
2130
|
+
const pageHeight = pageRect.height;
|
|
2131
|
+
const pageCenter = pageTop + pageHeight / 2;
|
|
2132
|
+
const distance = Math.abs(pageCenter - containerCenter);
|
|
2133
|
+
|
|
2134
|
+
if (distance < closestDistance) {
|
|
2135
|
+
closestDistance = distance;
|
|
2136
|
+
closestPage = pageNum;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// 只有当页码真正改变时才更新
|
|
2142
|
+
if (closestPage !== currentPage.value) {
|
|
2143
|
+
currentPage.value = closestPage;
|
|
2144
|
+
emit("page-change", closestPage, totalPages.value);
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
|
|
1744
2148
|
/**
|
|
1745
2149
|
* 处理滚动翻页(通过滚动位置判断当前页)
|
|
1746
|
-
*
|
|
1747
|
-
* 向上滑动:当视口底部到达上一页的底部时,切换到上一页
|
|
2150
|
+
* 使用视口中心最近的页面来确定当前页,更准确
|
|
1748
2151
|
*/
|
|
1749
2152
|
const handleScrollPaging = (e: Event) => {
|
|
1750
2153
|
const container = e.target as HTMLElement;
|
|
@@ -1761,80 +2164,16 @@ const handleScrollPaging = (e: Event) => {
|
|
|
1761
2164
|
}
|
|
1762
2165
|
|
|
1763
2166
|
// 使用防抖,避免频繁触发
|
|
2167
|
+
// 减少延迟到50ms,提高响应速度
|
|
1764
2168
|
scrollPagingTimer = setTimeout(() => {
|
|
1765
2169
|
// 再次检查是否正在翻页
|
|
1766
2170
|
if (isScrollPaging.value) {
|
|
1767
2171
|
return;
|
|
1768
2172
|
}
|
|
1769
2173
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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
|
-
}
|
|
1837
|
-
}, 100);
|
|
2174
|
+
// 更新当前页码(基于视口中心最近的页面)
|
|
2175
|
+
updateCurrentPageFromScroll();
|
|
2176
|
+
}, 50);
|
|
1838
2177
|
};
|
|
1839
2178
|
|
|
1840
2179
|
/**
|
|
@@ -1962,7 +2301,15 @@ const highlightPosition = (
|
|
|
1962
2301
|
if (shouldScroll && containerRef.value) {
|
|
1963
2302
|
const isVisible = isElementVisible(elementRef, containerRef.value);
|
|
1964
2303
|
if (!isVisible) {
|
|
2304
|
+
// 标记这是定位滚动,不应该被同步滚动干扰
|
|
2305
|
+
containerRef.value.dataset.pageScrolling = "true";
|
|
1965
2306
|
elementRef.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
2307
|
+
// 延迟清除标记,确保滚动完成
|
|
2308
|
+
setTimeout(() => {
|
|
2309
|
+
if (containerRef.value) {
|
|
2310
|
+
delete containerRef.value.dataset.pageScrolling;
|
|
2311
|
+
}
|
|
2312
|
+
}, 500); // scrollIntoView 的 smooth 动画通常需要 300-500ms
|
|
1966
2313
|
}
|
|
1967
2314
|
}
|
|
1968
2315
|
|
|
@@ -1998,7 +2345,12 @@ const jumpToPosition = (
|
|
|
1998
2345
|
return;
|
|
1999
2346
|
}
|
|
2000
2347
|
|
|
2001
|
-
//
|
|
2348
|
+
// 先跳转到对应页面(如果不在当前页)
|
|
2349
|
+
if (pageNum !== currentPage.value) {
|
|
2350
|
+
switchToPage(pageNum);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// 等待页面跳转完成后再高亮
|
|
2002
2354
|
nextTick(() => {
|
|
2003
2355
|
let retryCount = 0;
|
|
2004
2356
|
const maxRetries = 5;
|
|
@@ -2015,11 +2367,14 @@ const jumpToPosition = (
|
|
|
2015
2367
|
retryCount++;
|
|
2016
2368
|
setTimeout(tryHighlight, retryDelay);
|
|
2017
2369
|
} else {
|
|
2018
|
-
console.warn(
|
|
2370
|
+
console.warn(
|
|
2371
|
+
`无法找到并高亮指定位置: 页码 ${pageNum}, bbox: [${bbox.join(", ")}]`
|
|
2372
|
+
);
|
|
2019
2373
|
}
|
|
2020
2374
|
};
|
|
2021
2375
|
|
|
2022
|
-
|
|
2376
|
+
// 增加延迟,确保页面跳转完成(特别是同步滚动场景)
|
|
2377
|
+
setTimeout(tryHighlight, 500);
|
|
2023
2378
|
});
|
|
2024
2379
|
};
|
|
2025
2380
|
|
|
@@ -2094,6 +2449,98 @@ watch(
|
|
|
2094
2449
|
{ deep: true }
|
|
2095
2450
|
);
|
|
2096
2451
|
|
|
2452
|
+
/**
|
|
2453
|
+
* 处理容器尺寸变化,重新计算自适应缩放比例
|
|
2454
|
+
*/
|
|
2455
|
+
const handleContainerResize = () => {
|
|
2456
|
+
// 如果禁用了自适应宽度,或者用户主动缩放过,不自动调整
|
|
2457
|
+
if (!props.autoFitWidth || isUserZooming.value) {
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// 如果第一页图片还没有加载完成,不处理
|
|
2462
|
+
const firstPageSize = imageSizes.get(1);
|
|
2463
|
+
if (!firstPageSize || firstPageSize.width === 0) {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// 如果正在计算中或正在处理 resize,跳过(避免重复计算)
|
|
2468
|
+
if (isCalculatingAutoFit.value || isResizing) {
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// 检查容器尺寸是否真的变化了(避免无意义的重复计算)
|
|
2473
|
+
if (containerRef.value) {
|
|
2474
|
+
const currentWidth = containerRef.value.getBoundingClientRect().width;
|
|
2475
|
+
// 如果宽度变化小于 5px,认为是渲染抖动,不处理
|
|
2476
|
+
if (Math.abs(currentWidth - lastContainerWidth) < 5) {
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
lastContainerWidth = currentWidth;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
console.log("[ImagePreview] handleContainerResize 被调用:", {
|
|
2483
|
+
autoFitWidth: props.autoFitWidth,
|
|
2484
|
+
isUserZooming: isUserZooming.value,
|
|
2485
|
+
isImageReady: isImageReady.value,
|
|
2486
|
+
isCalculatingAutoFit: isCalculatingAutoFit.value,
|
|
2487
|
+
hasFirstPageSize: !!imageSizes.get(1),
|
|
2488
|
+
containerWidth: containerRef.value?.getBoundingClientRect()?.width,
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
// 标记正在处理 resize
|
|
2492
|
+
isResizing = true;
|
|
2493
|
+
|
|
2494
|
+
// 清除之前的定时器
|
|
2495
|
+
if (resizeTimer) {
|
|
2496
|
+
clearTimeout(resizeTimer);
|
|
2497
|
+
resizeTimer = null;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// 宽度变化时不显示 loading,只更新缩放比例(避免看起来像重新加载)
|
|
2501
|
+
console.log("[ImagePreview] handleContainerResize: 开始重新计算");
|
|
2502
|
+
|
|
2503
|
+
// 立即计算并应用新的缩放比例,避免过渡期间露出底色
|
|
2504
|
+
// 使用 requestAnimationFrame 确保在浏览器重绘前更新
|
|
2505
|
+
requestAnimationFrame(() => {
|
|
2506
|
+
try {
|
|
2507
|
+
console.log(
|
|
2508
|
+
"[ImagePreview] handleContainerResize: 开始计算自适应宽度..."
|
|
2509
|
+
);
|
|
2510
|
+
const newScale = calculateAutoFitScale();
|
|
2511
|
+
console.log("[ImagePreview] handleContainerResize: 计算结果:", newScale);
|
|
2512
|
+
|
|
2513
|
+
if (newScale > 0) {
|
|
2514
|
+
// 即使变化很小也立即更新,确保过渡期间图片始终填满容器
|
|
2515
|
+
scale.value = newScale;
|
|
2516
|
+
initialAutoFitScale.value = newScale;
|
|
2517
|
+
}
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
console.error("[ImagePreview] handleContainerResize: 计算失败:", error);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// 在过渡动画完成后再次检查,确保最终状态正确(处理过渡动画期间的连续变化)
|
|
2523
|
+
resizeTimer = setTimeout(() => {
|
|
2524
|
+
try {
|
|
2525
|
+
const finalScale = calculateAutoFitScale();
|
|
2526
|
+
if (finalScale > 0 && Math.abs(finalScale - scale.value) > 0.01) {
|
|
2527
|
+
scale.value = finalScale;
|
|
2528
|
+
initialAutoFitScale.value = finalScale;
|
|
2529
|
+
}
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
console.error(
|
|
2532
|
+
"[ImagePreview] handleContainerResize: 最终计算失败:",
|
|
2533
|
+
error
|
|
2534
|
+
);
|
|
2535
|
+
} finally {
|
|
2536
|
+
// 计算完成,重置标记(不改变图片显示状态,因为宽度变化时不应该显示loading)
|
|
2537
|
+
console.log("[ImagePreview] handleContainerResize: 更新状态完成");
|
|
2538
|
+
isResizing = false; // 重置标记
|
|
2539
|
+
}
|
|
2540
|
+
}, 350); // 350ms 延迟,略大于过渡动画时间(300ms),确保过渡完成后稳定
|
|
2541
|
+
});
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2097
2544
|
/**
|
|
2098
2545
|
* 组件挂载时的初始化
|
|
2099
2546
|
*/
|
|
@@ -2103,19 +2550,68 @@ onMounted(() => {
|
|
|
2103
2550
|
if (firstPageImage && firstPageImage.complete && props.autoFitWidth) {
|
|
2104
2551
|
const firstPageSize = imageSizes.get(1);
|
|
2105
2552
|
if (firstPageSize && firstPageSize.width > 0) {
|
|
2553
|
+
// 隐藏图片,显示 loading
|
|
2554
|
+
isImageReady.value = false;
|
|
2555
|
+
isCalculatingAutoFit.value = true;
|
|
2556
|
+
|
|
2557
|
+
// 设置超时保护,防止一直显示 loading(最多等待 3 秒)
|
|
2558
|
+
const timeoutId = setTimeout(() => {
|
|
2559
|
+
console.warn("自适应宽度计算超时,强制显示图片");
|
|
2560
|
+
isCalculatingAutoFit.value = false;
|
|
2561
|
+
isImageReady.value = true;
|
|
2562
|
+
}, 3000);
|
|
2563
|
+
|
|
2106
2564
|
nextTick(() => {
|
|
2107
2565
|
nextTick(() => {
|
|
2108
2566
|
setTimeout(() => {
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2567
|
+
try {
|
|
2568
|
+
const autoScale = calculateAutoFitScale();
|
|
2569
|
+
if (autoScale > 0) {
|
|
2570
|
+
scale.value = autoScale;
|
|
2571
|
+
initialAutoFitScale.value = autoScale;
|
|
2572
|
+
}
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
console.warn("计算自适应宽度失败:", error);
|
|
2575
|
+
} finally {
|
|
2576
|
+
// 清除超时保护
|
|
2577
|
+
clearTimeout(timeoutId);
|
|
2578
|
+
// 无论计算结果如何,都要更新状态,避免一直显示 loading
|
|
2579
|
+
isCalculatingAutoFit.value = false;
|
|
2580
|
+
requestAnimationFrame(() => {
|
|
2581
|
+
isImageReady.value = true;
|
|
2582
|
+
});
|
|
2113
2583
|
}
|
|
2114
2584
|
}, 100);
|
|
2115
2585
|
});
|
|
2116
2586
|
});
|
|
2117
2587
|
}
|
|
2588
|
+
} else if (!props.autoFitWidth) {
|
|
2589
|
+
// 如果没有启用自适应宽度,立即显示图片
|
|
2590
|
+
isImageReady.value = true;
|
|
2118
2591
|
}
|
|
2592
|
+
|
|
2593
|
+
// 监听容器尺寸变化(用于响应外部收起/展开操作)
|
|
2594
|
+
nextTick(() => {
|
|
2595
|
+
if (containerRef.value && typeof ResizeObserver !== "undefined") {
|
|
2596
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
2597
|
+
// 使用防抖,避免频繁触发
|
|
2598
|
+
if (resizeDebounceTimer) {
|
|
2599
|
+
clearTimeout(resizeDebounceTimer);
|
|
2600
|
+
}
|
|
2601
|
+
resizeDebounceTimer = setTimeout(() => {
|
|
2602
|
+
// 使用 entries 参数立即获取新尺寸,避免延迟
|
|
2603
|
+
for (const entry of entries) {
|
|
2604
|
+
// 立即响应尺寸变化,避免过渡期间露出底色
|
|
2605
|
+
handleContainerResize();
|
|
2606
|
+
}
|
|
2607
|
+
}, 100); // 100ms 防抖,避免频繁触发
|
|
2608
|
+
});
|
|
2609
|
+
resizeObserver.observe(containerRef.value);
|
|
2610
|
+
} else {
|
|
2611
|
+
// 降级方案:监听窗口大小变化
|
|
2612
|
+
window.addEventListener("resize", handleContainerResize);
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2119
2615
|
});
|
|
2120
2616
|
|
|
2121
2617
|
/**
|
|
@@ -2130,6 +2626,20 @@ onBeforeUnmount(() => {
|
|
|
2130
2626
|
clearTimeout(scrollPagingTimer);
|
|
2131
2627
|
scrollPagingTimer = null;
|
|
2132
2628
|
}
|
|
2629
|
+
if (resizeTimer) {
|
|
2630
|
+
clearTimeout(resizeTimer);
|
|
2631
|
+
resizeTimer = null;
|
|
2632
|
+
}
|
|
2633
|
+
if (resizeDebounceTimer) {
|
|
2634
|
+
clearTimeout(resizeDebounceTimer);
|
|
2635
|
+
resizeDebounceTimer = null;
|
|
2636
|
+
}
|
|
2637
|
+
if (resizeObserver) {
|
|
2638
|
+
resizeObserver.disconnect();
|
|
2639
|
+
resizeObserver = null;
|
|
2640
|
+
}
|
|
2641
|
+
// 移除窗口 resize 监听器(降级方案)
|
|
2642
|
+
window.removeEventListener("resize", handleContainerResize);
|
|
2133
2643
|
});
|
|
2134
2644
|
|
|
2135
2645
|
defineExpose({
|
|
@@ -2138,6 +2648,7 @@ defineExpose({
|
|
|
2138
2648
|
goToPage: switchToPage, // 暴露翻页方法给父组件
|
|
2139
2649
|
getCurrentPage: () => currentPage.value, // 获取当前页码
|
|
2140
2650
|
getTotalPages: () => totalPages.value, // 获取总页数
|
|
2651
|
+
getContainer: () => containerRef.value, // 暴露容器引用,用于同步滚动
|
|
2141
2652
|
});
|
|
2142
2653
|
</script>
|
|
2143
2654
|
|
|
@@ -2156,6 +2667,47 @@ defineExpose({
|
|
|
2156
2667
|
overflow: auto;
|
|
2157
2668
|
}
|
|
2158
2669
|
|
|
2670
|
+
// 自适应宽度计算 Loading
|
|
2671
|
+
.auto-fit-loading {
|
|
2672
|
+
position: absolute;
|
|
2673
|
+
top: 50%;
|
|
2674
|
+
left: 50%;
|
|
2675
|
+
transform: translate(-50%, -50%);
|
|
2676
|
+
z-index: 1000;
|
|
2677
|
+
display: flex;
|
|
2678
|
+
flex-direction: column;
|
|
2679
|
+
align-items: center;
|
|
2680
|
+
gap: 12px;
|
|
2681
|
+
padding: 20px 30px;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
.loading-spinner {
|
|
2685
|
+
width: 32px;
|
|
2686
|
+
height: 32px;
|
|
2687
|
+
border: 3px solid #e5e7eb;
|
|
2688
|
+
border-top-color: #1890ff;
|
|
2689
|
+
border-radius: 50%;
|
|
2690
|
+
animation: spin 0.8s linear infinite;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
@keyframes spin {
|
|
2694
|
+
to {
|
|
2695
|
+
transform: rotate(360deg);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
.loading-text {
|
|
2700
|
+
font-size: 14px;
|
|
2701
|
+
color: #666;
|
|
2702
|
+
white-space: nowrap;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// 图片隐藏状态(自适应宽度计算完成前)
|
|
2706
|
+
// 使用 display: none 确保图片完全不可见,不会在加载过程中显示
|
|
2707
|
+
.image-wrapper-container.image-hidden {
|
|
2708
|
+
display: none !important;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2159
2711
|
// 页码信息样式
|
|
2160
2712
|
.page-info {
|
|
2161
2713
|
display: inline-flex;
|
|
@@ -2208,6 +2760,8 @@ defineExpose({
|
|
|
2208
2760
|
margin: 0;
|
|
2209
2761
|
padding: 0;
|
|
2210
2762
|
line-height: 0;
|
|
2763
|
+
// 添加平滑过渡,避免切换时露出底色
|
|
2764
|
+
transition: transform 0.3s ease;
|
|
2211
2765
|
|
|
2212
2766
|
img {
|
|
2213
2767
|
display: block; // 移除图片底部默认间隙
|