@koi-br/ocr-web-sdk 1.0.27 → 1.0.29
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/dist/{index-j1qGOVkF.js → index-DG14_3TA.js} +80 -80
- package/dist/{index-5LNwsgpW.mjs → index-QOLKOKLO.mjs} +8576 -8474
- package/dist/index.cjs.js +2 -2
- package/dist/index.esm.js +2 -2
- package/dist/{tiff.min-KfzRTYak.js → tiff.min-Com-G-pb.js} +1 -1
- package/dist/{tiff.min-vkppgIK-.mjs → tiff.min-D0JMbw7k.mjs} +1 -1
- package/package.json +1 -1
- package/preview/ImagePreview.vue +785 -472
- package/preview/index.vue +4 -4
package/preview/ImagePreview.vue
CHANGED
|
@@ -10,30 +10,20 @@
|
|
|
10
10
|
<slot name="left-top"></slot>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="toolbar-group">
|
|
13
|
-
<span class="scale-text">
|
|
14
|
-
{{ Math.round(scale * 100) }}%
|
|
15
|
-
</span>
|
|
13
|
+
<span class="scale-text"> {{ Math.round(scale * 100) }}% </span>
|
|
16
14
|
<div class="toolbar-divider"></div>
|
|
17
|
-
<ATooltip mini position="bottom" content="重置">
|
|
18
|
-
<AButton
|
|
19
|
-
size="small"
|
|
20
|
-
type="outline"
|
|
21
|
-
@click="reset"
|
|
22
|
-
>
|
|
23
|
-
<RefreshCcw :size="16" />
|
|
24
|
-
</AButton>
|
|
25
|
-
</ATooltip>
|
|
26
15
|
<ATooltip
|
|
27
|
-
v-if="
|
|
16
|
+
v-if="showResetButton"
|
|
28
17
|
mini
|
|
29
18
|
position="bottom"
|
|
30
|
-
content="
|
|
19
|
+
content="重置"
|
|
31
20
|
>
|
|
32
|
-
<AButton
|
|
33
|
-
size="
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
<AButton size="small" type="outline" @click="reset">
|
|
22
|
+
<RefreshCcw :size="16" />
|
|
23
|
+
</AButton>
|
|
24
|
+
</ATooltip>
|
|
25
|
+
<ATooltip v-if="originalId" mini position="bottom" content="查看原图">
|
|
26
|
+
<AButton size="small" type="outline" @click="original">
|
|
37
27
|
<Maximize2 :size="16" />
|
|
38
28
|
</AButton>
|
|
39
29
|
</ATooltip>
|
|
@@ -58,16 +48,12 @@
|
|
|
58
48
|
</AButton>
|
|
59
49
|
</ATooltip>
|
|
60
50
|
<ATooltip
|
|
61
|
-
v-if="
|
|
51
|
+
v-if="showDownloadButton"
|
|
62
52
|
mini
|
|
63
53
|
position="bottom"
|
|
64
54
|
content="下载"
|
|
65
55
|
>
|
|
66
|
-
<AButton
|
|
67
|
-
size="small"
|
|
68
|
-
type="outline"
|
|
69
|
-
@click="emit('download')"
|
|
70
|
-
>
|
|
56
|
+
<AButton size="small" type="outline" @click="emit('download')">
|
|
71
57
|
<Download :size="16" />
|
|
72
58
|
</AButton>
|
|
73
59
|
</ATooltip>
|
|
@@ -78,11 +64,7 @@
|
|
|
78
64
|
position="bottom"
|
|
79
65
|
content="向左旋转"
|
|
80
66
|
>
|
|
81
|
-
<AButton
|
|
82
|
-
size="small"
|
|
83
|
-
type="outline"
|
|
84
|
-
@click="rotateImage('left')"
|
|
85
|
-
>
|
|
67
|
+
<AButton size="small" type="outline" @click="rotateImage('left')">
|
|
86
68
|
<RotateCw :size="16" />
|
|
87
69
|
</AButton>
|
|
88
70
|
</ATooltip>
|
|
@@ -92,11 +74,7 @@
|
|
|
92
74
|
position="bottom"
|
|
93
75
|
content="向右旋转"
|
|
94
76
|
>
|
|
95
|
-
<AButton
|
|
96
|
-
size="small"
|
|
97
|
-
type="outline"
|
|
98
|
-
@click="rotateImage('right')"
|
|
99
|
-
>
|
|
77
|
+
<AButton size="small" type="outline" @click="rotateImage('right')">
|
|
100
78
|
<RotateCcw :size="16" />
|
|
101
79
|
</AButton>
|
|
102
80
|
</ATooltip>
|
|
@@ -113,13 +91,12 @@
|
|
|
113
91
|
<ChevronLeft :size="16" />
|
|
114
92
|
</AButton>
|
|
115
93
|
</ATooltip>
|
|
116
|
-
<span class="page-info">
|
|
117
|
-
{{ currentPage }} / {{ totalPages }}
|
|
118
|
-
</span>
|
|
94
|
+
<span class="page-info"> {{ currentPage }} / {{ totalPages }} </span>
|
|
119
95
|
<ATooltip mini position="bottom" content="下一页">
|
|
120
96
|
<AButton
|
|
121
97
|
size="small"
|
|
122
98
|
type="outline"
|
|
99
|
+
style="padding-right: 0px;"
|
|
123
100
|
:disabled="currentPage >= totalPages"
|
|
124
101
|
@click="goToNextPage"
|
|
125
102
|
>
|
|
@@ -139,38 +116,46 @@
|
|
|
139
116
|
@mouseleave="stopPan"
|
|
140
117
|
@scroll="handleScroll"
|
|
141
118
|
>
|
|
142
|
-
<div
|
|
143
|
-
|
|
144
|
-
:style="containerStyle"
|
|
145
|
-
>
|
|
119
|
+
<div class="image-wrapper-container" :style="containerStyle">
|
|
120
|
+
<!-- 渲染所有图片页面 -->
|
|
146
121
|
<div
|
|
147
|
-
|
|
148
|
-
:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}"
|
|
122
|
+
v-for="(imageUrl, pageIndex) in imageUrls"
|
|
123
|
+
:key="pageIndex"
|
|
124
|
+
:data-page-number="pageIndex + 1"
|
|
125
|
+
class="image-page-container"
|
|
152
126
|
>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
127
|
+
<div
|
|
128
|
+
class="image-wrapper"
|
|
129
|
+
:style="{
|
|
130
|
+
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${scale})`,
|
|
131
|
+
transformOrigin: 'top center',
|
|
132
|
+
}"
|
|
133
|
+
>
|
|
134
|
+
<img
|
|
135
|
+
:ref="(el) => setImageRef(el, pageIndex + 1)"
|
|
136
|
+
:src="imageUrl"
|
|
137
|
+
:alt="`预览图片 ${pageIndex + 1}`"
|
|
138
|
+
:style="{
|
|
139
|
+
cursor: isPanning ? 'grabbing' : 'grab',
|
|
140
|
+
display: 'block',
|
|
141
|
+
pointerEvents:
|
|
142
|
+
getPageBlocksData(pageIndex + 1) &&
|
|
143
|
+
getPageBlocksData(pageIndex + 1).length > 0
|
|
144
|
+
? 'none'
|
|
145
|
+
: 'auto',
|
|
146
|
+
}"
|
|
147
|
+
@contextmenu.prevent
|
|
148
|
+
@mousedown="handleImageMouseDown"
|
|
149
|
+
@load="(e) => onImageLoad(e, pageIndex + 1)"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<!-- 文本图层(用于文本块选择和定位) -->
|
|
153
|
+
<div
|
|
154
|
+
v-if="getPageBlocksData(pageIndex + 1) && getPageBlocksData(pageIndex + 1).length > 0"
|
|
155
|
+
:ref="(el) => setTextLayerRef(el, pageIndex + 1)"
|
|
156
|
+
class="text-layer"
|
|
157
|
+
></div>
|
|
158
|
+
</div>
|
|
174
159
|
</div>
|
|
175
160
|
</div>
|
|
176
161
|
|
|
@@ -221,7 +206,12 @@
|
|
|
221
206
|
<AButton size="small" type="outline" @click="closeAnnotationInput">
|
|
222
207
|
取消
|
|
223
208
|
</AButton>
|
|
224
|
-
<AButton
|
|
209
|
+
<AButton
|
|
210
|
+
size="small"
|
|
211
|
+
type="primary"
|
|
212
|
+
@click="saveAnnotation"
|
|
213
|
+
:disabled="!annotationInput.trim()"
|
|
214
|
+
>
|
|
225
215
|
保存
|
|
226
216
|
</AButton>
|
|
227
217
|
</div>
|
|
@@ -231,9 +221,35 @@
|
|
|
231
221
|
</template>
|
|
232
222
|
|
|
233
223
|
<script setup lang="ts">
|
|
234
|
-
import {
|
|
235
|
-
|
|
236
|
-
|
|
224
|
+
import {
|
|
225
|
+
ref,
|
|
226
|
+
watch,
|
|
227
|
+
nextTick,
|
|
228
|
+
onMounted,
|
|
229
|
+
onBeforeUnmount,
|
|
230
|
+
computed,
|
|
231
|
+
} from "vue";
|
|
232
|
+
import {
|
|
233
|
+
ZoomIn,
|
|
234
|
+
ZoomOut,
|
|
235
|
+
RefreshCcw,
|
|
236
|
+
RotateCw,
|
|
237
|
+
RotateCcw,
|
|
238
|
+
Download,
|
|
239
|
+
Maximize2,
|
|
240
|
+
Copy,
|
|
241
|
+
ChevronLeft,
|
|
242
|
+
ChevronRight,
|
|
243
|
+
MessageSquare,
|
|
244
|
+
X,
|
|
245
|
+
} from "lucide-vue-next";
|
|
246
|
+
import {
|
|
247
|
+
Button as AButton,
|
|
248
|
+
Tooltip as ATooltip,
|
|
249
|
+
Message,
|
|
250
|
+
Input,
|
|
251
|
+
Textarea,
|
|
252
|
+
} from "@arco-design/web-vue";
|
|
237
253
|
|
|
238
254
|
/**
|
|
239
255
|
* 文本块数据结构
|
|
@@ -260,71 +276,86 @@ const props = defineProps({
|
|
|
260
276
|
// 支持单个URL(向后兼容)或URL数组
|
|
261
277
|
url: {
|
|
262
278
|
type: [String, Array] as () => string | string[],
|
|
263
|
-
required: true
|
|
279
|
+
required: true,
|
|
264
280
|
},
|
|
265
281
|
minScale: {
|
|
266
282
|
type: Number,
|
|
267
|
-
default: 0.1
|
|
283
|
+
default: 0.1,
|
|
268
284
|
},
|
|
269
285
|
maxScale: {
|
|
270
286
|
type: Number,
|
|
271
|
-
default: 5
|
|
287
|
+
default: 5,
|
|
272
288
|
},
|
|
273
289
|
clickStep: {
|
|
274
290
|
type: Number,
|
|
275
|
-
default: 0.25
|
|
291
|
+
default: 0.25,
|
|
276
292
|
},
|
|
277
293
|
wheelStep: {
|
|
278
294
|
type: Number,
|
|
279
|
-
default: 0.1
|
|
295
|
+
default: 0.1,
|
|
280
296
|
},
|
|
281
297
|
originalId: {
|
|
282
298
|
type: String,
|
|
283
|
-
default:
|
|
299
|
+
default: "",
|
|
284
300
|
},
|
|
285
301
|
isDownload: {
|
|
286
302
|
type: Boolean,
|
|
287
|
-
default: false
|
|
303
|
+
default: false,
|
|
288
304
|
},
|
|
289
305
|
blocksData: {
|
|
290
306
|
type: Array as () => BlockInfo[],
|
|
291
|
-
default: () => []
|
|
307
|
+
default: () => [],
|
|
292
308
|
},
|
|
293
309
|
enableWheelZoom: {
|
|
294
310
|
type: Boolean,
|
|
295
|
-
default: true // 默认启用滚轮缩放,保持向后兼容
|
|
311
|
+
default: true, // 默认启用滚轮缩放,保持向后兼容
|
|
296
312
|
},
|
|
297
313
|
// 批注数据(从外部传入,用于显示已有批注)
|
|
298
314
|
annotations: {
|
|
299
315
|
type: Array as () => AnnotationInfo[],
|
|
300
|
-
default: () => []
|
|
316
|
+
default: () => [],
|
|
301
317
|
},
|
|
302
318
|
// 是否在初始加载时自适应宽度
|
|
303
319
|
autoFitWidth: {
|
|
304
320
|
type: Boolean,
|
|
305
|
-
default: true
|
|
321
|
+
default: true,
|
|
306
322
|
},
|
|
307
323
|
// 是否启用滚动翻页
|
|
308
324
|
enableScrollPaging: {
|
|
309
325
|
type: Boolean,
|
|
310
|
-
default: true
|
|
326
|
+
default: true,
|
|
311
327
|
},
|
|
312
328
|
// 是否显示旋转按钮
|
|
313
329
|
showRotateButtons: {
|
|
314
330
|
type: Boolean,
|
|
315
|
-
default: true
|
|
316
|
-
}
|
|
331
|
+
default: true,
|
|
332
|
+
},
|
|
333
|
+
// 是否显示重置按钮
|
|
334
|
+
showResetButton: {
|
|
335
|
+
type: Boolean,
|
|
336
|
+
default: true,
|
|
337
|
+
},
|
|
338
|
+
// 是否显示下载按钮
|
|
339
|
+
showDownloadButton: {
|
|
340
|
+
type: Boolean,
|
|
341
|
+
default: true, // 默认不显示,保持向后兼容(isDownload 的默认值)
|
|
342
|
+
},
|
|
317
343
|
});
|
|
318
344
|
|
|
319
345
|
const emit = defineEmits<{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
346
|
+
original: [];
|
|
347
|
+
download: [];
|
|
348
|
+
load: [
|
|
349
|
+
data: { width: number; height: number; page?: number; totalPages?: number }
|
|
350
|
+
];
|
|
351
|
+
"position-jump": [
|
|
352
|
+
bbox: [number, number, number, number],
|
|
353
|
+
type: "block" | "seal"
|
|
354
|
+
];
|
|
355
|
+
"page-change": [page: number, totalPages: number];
|
|
356
|
+
"annotation-add": [annotation: AnnotationInfo]; // 添加批注事件
|
|
357
|
+
"annotation-update": [annotation: AnnotationInfo]; // 更新批注事件
|
|
358
|
+
"annotation-delete": [annotationId: string]; // 删除批注事件
|
|
328
359
|
}>();
|
|
329
360
|
|
|
330
361
|
// 图片URL数组和当前页码
|
|
@@ -340,7 +371,7 @@ const totalPages = computed(() => imageUrls.value.length);
|
|
|
340
371
|
|
|
341
372
|
// 当前显示的图片URL
|
|
342
373
|
const currentImageUrl = computed(() => {
|
|
343
|
-
return imageUrls.value[currentPage.value - 1] ||
|
|
374
|
+
return imageUrls.value[currentPage.value - 1] || "";
|
|
344
375
|
});
|
|
345
376
|
|
|
346
377
|
const rotation = ref(0);
|
|
@@ -357,20 +388,56 @@ const isScrollPaging = ref(false); // 标记是否正在进行滚动翻页
|
|
|
357
388
|
|
|
358
389
|
// 图片和容器引用
|
|
359
390
|
const containerRef = ref<HTMLElement>();
|
|
360
|
-
const
|
|
361
|
-
const
|
|
391
|
+
const imageRefs = new Map<number, HTMLImageElement>();
|
|
392
|
+
const textLayerRefs = new Map<number, HTMLElement>();
|
|
362
393
|
const annotationButtonRef = ref<HTMLElement>();
|
|
363
394
|
|
|
364
|
-
//
|
|
365
|
-
const
|
|
395
|
+
// 图片尺寸(存储每页的尺寸)
|
|
396
|
+
const imageSizes = new Map<number, { width: number; height: number }>();
|
|
397
|
+
|
|
398
|
+
// 设置图片引用
|
|
399
|
+
const setImageRef = (el: any, pageNum: number) => {
|
|
400
|
+
if (el) {
|
|
401
|
+
imageRefs.set(pageNum, el);
|
|
402
|
+
} else {
|
|
403
|
+
imageRefs.delete(pageNum);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// 设置文本图层引用
|
|
408
|
+
const setTextLayerRef = (el: any, pageNum: number) => {
|
|
409
|
+
if (el) {
|
|
410
|
+
textLayerRefs.set(pageNum, el);
|
|
411
|
+
} else {
|
|
412
|
+
textLayerRefs.delete(pageNum);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// 获取当前页的图片引用(用于兼容旧代码)
|
|
417
|
+
const imageRef = computed(() => imageRefs.get(currentPage.value));
|
|
418
|
+
const textLayerRef = computed(() => textLayerRefs.get(currentPage.value));
|
|
419
|
+
|
|
420
|
+
// 图片尺寸(用于兼容旧代码,返回当前页的尺寸)
|
|
421
|
+
const imageSize = computed({
|
|
422
|
+
get: () => imageSizes.get(currentPage.value) || { width: 0, height: 0 },
|
|
423
|
+
set: (value) => {
|
|
424
|
+
imageSizes.set(currentPage.value, value);
|
|
425
|
+
},
|
|
426
|
+
});
|
|
366
427
|
|
|
367
428
|
// 计算图片放大后的实际尺寸(考虑旋转)
|
|
429
|
+
// 在自适应宽度模式下,使用第一页的尺寸;否则使用当前页的尺寸
|
|
368
430
|
const scaledImageSize = computed(() => {
|
|
369
|
-
|
|
431
|
+
// 如果启用自适应宽度,使用第一页的尺寸作为基准
|
|
432
|
+
const baseSize = props.autoFitWidth
|
|
433
|
+
? (imageSizes.get(1) || { width: 0, height: 0 })
|
|
434
|
+
: imageSize.value;
|
|
435
|
+
|
|
436
|
+
if (baseSize.width === 0 || baseSize.height === 0) {
|
|
370
437
|
return { width: 0, height: 0 };
|
|
371
438
|
}
|
|
372
439
|
|
|
373
|
-
const { width, height } =
|
|
440
|
+
const { width, height } = baseSize;
|
|
374
441
|
const scaledWidth = width * scale.value;
|
|
375
442
|
const scaledHeight = height * scale.value;
|
|
376
443
|
|
|
@@ -380,7 +447,7 @@ const scaledImageSize = computed(() => {
|
|
|
380
447
|
|
|
381
448
|
return {
|
|
382
449
|
width: isRotated ? scaledHeight : scaledWidth,
|
|
383
|
-
height: isRotated ? scaledWidth : scaledHeight
|
|
450
|
+
height: isRotated ? scaledWidth : scaledHeight,
|
|
384
451
|
};
|
|
385
452
|
});
|
|
386
453
|
|
|
@@ -392,23 +459,26 @@ const containerStyle = computed(() => {
|
|
|
392
459
|
// 让容器自适应内容,避免横向滚动条
|
|
393
460
|
if (props.autoFitWidth) {
|
|
394
461
|
return {
|
|
395
|
-
minWidth:
|
|
396
|
-
minHeight: height > 0 ? `${height}px` :
|
|
462
|
+
minWidth: "100%", // 使用 100% 让容器自适应父容器宽度
|
|
463
|
+
minHeight: height > 0 ? `${height}px` : "100%",
|
|
397
464
|
};
|
|
398
465
|
}
|
|
399
466
|
|
|
400
467
|
// 未启用自适应宽度时,使用原始逻辑
|
|
401
468
|
return {
|
|
402
|
-
minWidth: width > 0 ? `${width}px` :
|
|
403
|
-
minHeight: height > 0 ? `${height}px` :
|
|
469
|
+
minWidth: width > 0 ? `${width}px` : "100%",
|
|
470
|
+
minHeight: height > 0 ? `${height}px` : "100%",
|
|
404
471
|
};
|
|
405
472
|
});
|
|
406
473
|
|
|
407
474
|
// 批注相关状态
|
|
408
475
|
const showAnnotationPopup = ref(false); // 是否显示批注弹窗
|
|
409
476
|
const annotationPopupStyle = ref<any>({}); // 批注弹窗样式
|
|
410
|
-
const annotationInput = ref(
|
|
411
|
-
const currentAnnotationBlock = ref<{
|
|
477
|
+
const annotationInput = ref(""); // 批注输入内容
|
|
478
|
+
const currentAnnotationBlock = ref<{
|
|
479
|
+
bbox: [number, number, number, number];
|
|
480
|
+
content: string;
|
|
481
|
+
} | null>(null); // 当前正在添加批注的文本块
|
|
412
482
|
const annotationPopupRef = ref<HTMLElement>(); // 批注弹窗引用
|
|
413
483
|
|
|
414
484
|
// 文本选择相关状态(保留用于兼容)
|
|
@@ -421,14 +491,22 @@ const currentPageBlocksData = computed(() => {
|
|
|
421
491
|
return [];
|
|
422
492
|
}
|
|
423
493
|
// 根据当前页码过滤
|
|
424
|
-
return props.blocksData.filter(block => block.pageNo === currentPage.value);
|
|
494
|
+
return props.blocksData.filter((block) => block.pageNo === currentPage.value);
|
|
425
495
|
});
|
|
426
496
|
|
|
497
|
+
// 获取指定页码的blocksData
|
|
498
|
+
const getPageBlocksData = (pageNo: number) => {
|
|
499
|
+
if (!props.blocksData || props.blocksData.length === 0) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
return props.blocksData.filter((block) => block.pageNo === pageNo);
|
|
503
|
+
};
|
|
504
|
+
|
|
427
505
|
// 隐藏定时器
|
|
428
506
|
let hideTimer: any = null;
|
|
429
507
|
|
|
430
|
-
const rotateImage = direction => {
|
|
431
|
-
if (direction ===
|
|
508
|
+
const rotateImage = (direction) => {
|
|
509
|
+
if (direction === "left") {
|
|
432
510
|
rotation.value = (rotation.value - 90) % 360;
|
|
433
511
|
} else {
|
|
434
512
|
rotation.value = (rotation.value + 90) % 360;
|
|
@@ -438,7 +516,7 @@ const rotateImage = direction => {
|
|
|
438
516
|
const zoom = (delta, isWheel = false) => {
|
|
439
517
|
// 用户主动缩放时,标记为手动缩放
|
|
440
518
|
isUserZooming.value = true;
|
|
441
|
-
|
|
519
|
+
|
|
442
520
|
const step = isWheel ? props.wheelStep : props.clickStep; // 滚轮缩放使用更小的步长
|
|
443
521
|
const newScale = scale.value + delta * step;
|
|
444
522
|
if (newScale <= props.minScale) {
|
|
@@ -464,7 +542,7 @@ const handleWheel = (e: WheelEvent) => {
|
|
|
464
542
|
const startPan = (e: MouseEvent) => {
|
|
465
543
|
// 只在左键点击时启用拖拽
|
|
466
544
|
if (e.button !== 0) return;
|
|
467
|
-
|
|
545
|
+
|
|
468
546
|
// 如果有文本图层数据,默认允许文本选择,不拖拽
|
|
469
547
|
// 只有在按住 Ctrl 或 Meta 键时才允许拖拽
|
|
470
548
|
if (currentPageBlocksData.value && currentPageBlocksData.value.length > 0) {
|
|
@@ -478,7 +556,7 @@ const startPan = (e: MouseEvent) => {
|
|
|
478
556
|
return;
|
|
479
557
|
}
|
|
480
558
|
}
|
|
481
|
-
|
|
559
|
+
|
|
482
560
|
isPanning.value = true;
|
|
483
561
|
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
484
562
|
e.preventDefault(); // 阻止默认行为,避免与滚动冲突
|
|
@@ -492,7 +570,7 @@ const pan = (e: MouseEvent) => {
|
|
|
492
570
|
|
|
493
571
|
position.value = {
|
|
494
572
|
x: position.value.x + deltaX,
|
|
495
|
-
y: position.value.y + deltaY
|
|
573
|
+
y: position.value.y + deltaY,
|
|
496
574
|
};
|
|
497
575
|
|
|
498
576
|
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
@@ -501,23 +579,35 @@ const pan = (e: MouseEvent) => {
|
|
|
501
579
|
|
|
502
580
|
const stopPan = () => {
|
|
503
581
|
isPanning.value = false;
|
|
504
|
-
|
|
582
|
+
|
|
505
583
|
// 在自适应宽度模式下,如果用户没有主动缩放,确保缩放比例保持初始值
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
584
|
+
if (
|
|
585
|
+
props.autoFitWidth &&
|
|
586
|
+
!isUserZooming.value &&
|
|
587
|
+
initialAutoFitScale.value !== null &&
|
|
588
|
+
Math.abs(scale.value - initialAutoFitScale.value) > 0.01
|
|
589
|
+
) {
|
|
510
590
|
// 拖拽操作不应该改变缩放比例,如果被改变了,恢复它
|
|
511
591
|
scale.value = initialAutoFitScale.value;
|
|
512
592
|
}
|
|
593
|
+
};
|
|
513
594
|
|
|
595
|
+
// 处理图片鼠标按下事件(允许文本选择)
|
|
596
|
+
const handleImageMouseDown = (e: MouseEvent) => {
|
|
597
|
+
// 如果点击的是文本块,不阻止默认行为,允许文本选择
|
|
598
|
+
const target = e.target as HTMLElement;
|
|
599
|
+
if (target.closest(".text-block")) {
|
|
600
|
+
return; // 允许文本选择
|
|
601
|
+
}
|
|
602
|
+
// 否则阻止默认行为,允许拖拽
|
|
603
|
+
e.preventDefault();
|
|
514
604
|
};
|
|
515
605
|
|
|
516
606
|
// 处理鼠标抬起事件(检测文本选择)
|
|
517
607
|
const handleMouseUp = (e: MouseEvent) => {
|
|
518
608
|
// 先执行停止拖拽的逻辑
|
|
519
609
|
stopPan();
|
|
520
|
-
|
|
610
|
+
|
|
521
611
|
// 延迟检测文本选择,确保选择已完成
|
|
522
612
|
setTimeout(() => {
|
|
523
613
|
checkTextSelection(e);
|
|
@@ -530,19 +620,25 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
530
620
|
if (currentAnnotationBlock.value) {
|
|
531
621
|
return;
|
|
532
622
|
}
|
|
533
|
-
|
|
623
|
+
|
|
534
624
|
const target = e.target as HTMLElement;
|
|
535
625
|
// 如果点击的是批注按钮或批注弹窗,不执行文本选择检测
|
|
536
|
-
if (
|
|
626
|
+
if (
|
|
627
|
+
target.closest(".annotation-button-popup") ||
|
|
628
|
+
target.closest(".annotation-input-popup")
|
|
629
|
+
) {
|
|
537
630
|
return;
|
|
538
631
|
}
|
|
539
|
-
|
|
632
|
+
|
|
540
633
|
const selection = window.getSelection();
|
|
541
|
-
|
|
634
|
+
|
|
542
635
|
// 如果没有选中文本,隐藏批注按钮(但不关闭批注输入弹窗)
|
|
543
636
|
if (!selection || selection.toString().trim().length === 0) {
|
|
544
637
|
// 如果点击的不是文本块,且不在批注弹窗内,隐藏批注按钮
|
|
545
|
-
if (
|
|
638
|
+
if (
|
|
639
|
+
!target.closest(".text-block") &&
|
|
640
|
+
!target.closest(".annotation-input-popup")
|
|
641
|
+
) {
|
|
546
642
|
// 只有在没有打开批注输入弹窗时才隐藏批注按钮
|
|
547
643
|
if (!currentAnnotationBlock.value) {
|
|
548
644
|
showAnnotationPopup.value = false;
|
|
@@ -551,92 +647,100 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
551
647
|
}
|
|
552
648
|
return;
|
|
553
649
|
}
|
|
554
|
-
|
|
650
|
+
|
|
555
651
|
// 获取选中的文本
|
|
556
652
|
const selectedText = selection.toString().trim();
|
|
557
653
|
if (!selectedText) {
|
|
558
654
|
return;
|
|
559
655
|
}
|
|
560
|
-
|
|
656
|
+
|
|
561
657
|
// 检查选中的文本是否在文本块内
|
|
562
658
|
const range = selection.getRangeAt(0);
|
|
563
659
|
const container = range.commonAncestorContainer;
|
|
564
|
-
|
|
660
|
+
|
|
565
661
|
// 向上查找最近的文本块元素
|
|
566
662
|
let textBlockElement: HTMLElement | null = null;
|
|
567
|
-
let node: Node | null =
|
|
568
|
-
|
|
663
|
+
let node: Node | null =
|
|
664
|
+
container.nodeType === Node.TEXT_NODE
|
|
665
|
+
? container.parentElement
|
|
666
|
+
: (container as HTMLElement);
|
|
667
|
+
|
|
569
668
|
while (node && node !== document.body) {
|
|
570
|
-
if (node instanceof HTMLElement && node.classList.contains(
|
|
669
|
+
if (node instanceof HTMLElement && node.classList.contains("text-block")) {
|
|
571
670
|
textBlockElement = node;
|
|
572
671
|
break;
|
|
573
672
|
}
|
|
574
673
|
node = node.parentElement;
|
|
575
674
|
}
|
|
576
|
-
|
|
675
|
+
|
|
577
676
|
if (!textBlockElement) {
|
|
578
677
|
return;
|
|
579
678
|
}
|
|
580
|
-
|
|
679
|
+
|
|
581
680
|
// 设置当前激活的文本块
|
|
582
681
|
activeBlockDiv.value = textBlockElement;
|
|
583
|
-
|
|
682
|
+
|
|
584
683
|
// 获取选中文本的位置
|
|
585
684
|
const rect = range.getBoundingClientRect();
|
|
586
685
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
587
|
-
|
|
686
|
+
|
|
588
687
|
if (!containerRect || !containerRef.value) return;
|
|
589
|
-
|
|
688
|
+
|
|
590
689
|
// 计算批注按钮的位置
|
|
591
690
|
const buttonWidth = 80;
|
|
592
691
|
const popupWidth = buttonWidth + 8;
|
|
593
692
|
const popupHeight = 38;
|
|
594
693
|
const spacing = 4; // 减少间距,让按钮更靠近文字
|
|
595
|
-
|
|
694
|
+
|
|
596
695
|
// 计算相对于容器的坐标(考虑滚动)
|
|
597
|
-
const relativeX =
|
|
696
|
+
const relativeX =
|
|
697
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
598
698
|
const relativeY = rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
599
|
-
|
|
699
|
+
|
|
600
700
|
// 容器可用空间
|
|
601
701
|
const containerWidth = containerRef.value.clientWidth;
|
|
602
|
-
|
|
702
|
+
|
|
603
703
|
// 计算可用空间
|
|
604
704
|
const rectBottom = rect.bottom;
|
|
605
705
|
const rectTop = rect.top;
|
|
606
706
|
const containerBottom = containerRect.bottom;
|
|
607
707
|
const containerTop = containerRect.top;
|
|
608
|
-
|
|
708
|
+
|
|
609
709
|
const bottomSpace = containerBottom - rectBottom;
|
|
610
710
|
const topSpace = rectTop - containerTop;
|
|
611
|
-
|
|
612
|
-
const showOnBottom =
|
|
613
|
-
|
|
711
|
+
|
|
712
|
+
const showOnBottom =
|
|
713
|
+
bottomSpace >= popupHeight + spacing ||
|
|
714
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
715
|
+
|
|
614
716
|
// 计算最终位置
|
|
615
717
|
let left: number;
|
|
616
718
|
let top: number;
|
|
617
|
-
|
|
719
|
+
|
|
618
720
|
const centerX = relativeX + rect.width / 2;
|
|
619
721
|
left = centerX - popupWidth / 2;
|
|
620
|
-
|
|
722
|
+
|
|
621
723
|
if (showOnBottom) {
|
|
622
724
|
top = relativeY + rect.height + spacing;
|
|
623
725
|
} else {
|
|
624
726
|
top = relativeY - popupHeight - spacing;
|
|
625
727
|
}
|
|
626
|
-
|
|
728
|
+
|
|
627
729
|
// 边界检查
|
|
628
|
-
const popupTopInViewport =
|
|
730
|
+
const popupTopInViewport =
|
|
731
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
629
732
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
630
|
-
|
|
733
|
+
|
|
631
734
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
632
735
|
top = relativeY - popupHeight - spacing;
|
|
633
736
|
}
|
|
634
|
-
|
|
635
|
-
const newPopupTopInViewport =
|
|
737
|
+
|
|
738
|
+
const newPopupTopInViewport =
|
|
739
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
636
740
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
637
741
|
top = relativeY + rect.height + spacing;
|
|
638
742
|
}
|
|
639
|
-
|
|
743
|
+
|
|
640
744
|
// 确保不超出容器边界
|
|
641
745
|
if (left + popupWidth > containerWidth - spacing) {
|
|
642
746
|
left = containerWidth - popupWidth - spacing;
|
|
@@ -644,12 +748,12 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
644
748
|
if (left < spacing) {
|
|
645
749
|
left = spacing;
|
|
646
750
|
}
|
|
647
|
-
|
|
751
|
+
|
|
648
752
|
annotationPopupStyle.value = {
|
|
649
753
|
left: `${left}px`,
|
|
650
|
-
top: `${top}px
|
|
754
|
+
top: `${top}px`,
|
|
651
755
|
};
|
|
652
|
-
|
|
756
|
+
|
|
653
757
|
// 显示批注按钮
|
|
654
758
|
showAnnotationPopup.value = true;
|
|
655
759
|
};
|
|
@@ -671,53 +775,38 @@ const switchToPage = (page: number) => {
|
|
|
671
775
|
if (page < 1 || page > totalPages.value) {
|
|
672
776
|
return;
|
|
673
777
|
}
|
|
674
|
-
|
|
675
|
-
// 切换页码前重置状态(但不重置缩放,等图片加载后再设置)
|
|
676
|
-
rotation.value = 0;
|
|
677
|
-
position.value = { x: 0, y: 0 };
|
|
678
|
-
isUserZooming.value = false;
|
|
679
|
-
|
|
778
|
+
|
|
680
779
|
// 重置文本选择状态和批注状态
|
|
681
780
|
showAnnotationPopup.value = false;
|
|
682
781
|
currentAnnotationBlock.value = null;
|
|
683
|
-
annotationInput.value =
|
|
782
|
+
annotationInput.value = "";
|
|
684
783
|
activeBlockDiv.value = null;
|
|
685
784
|
isHighlighted.value = false;
|
|
686
|
-
|
|
687
|
-
//
|
|
785
|
+
|
|
786
|
+
// 更新页码
|
|
688
787
|
currentPage.value = page;
|
|
689
|
-
|
|
788
|
+
|
|
690
789
|
// 触发页码变化事件
|
|
691
|
-
emit(
|
|
692
|
-
|
|
693
|
-
//
|
|
790
|
+
emit("page-change", page, totalPages.value);
|
|
791
|
+
|
|
792
|
+
// 滚动到对应页面
|
|
694
793
|
if (containerRef.value) {
|
|
695
|
-
containerRef.value.
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
setTimeout(() => {
|
|
705
|
-
const autoScale = calculateAutoFitScale();
|
|
706
|
-
if (autoScale !== 1 && autoScale > 0) {
|
|
707
|
-
scale.value = autoScale;
|
|
708
|
-
initialAutoFitScale.value = autoScale; // 更新初始自适应缩放比例
|
|
709
|
-
}
|
|
710
|
-
}, 50);
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
renderTextLayer();
|
|
794
|
+
const pageElement = containerRef.value.querySelector(
|
|
795
|
+
`[data-page-number="${page}"]`
|
|
796
|
+
) as HTMLElement;
|
|
797
|
+
if (pageElement) {
|
|
798
|
+
pageElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
799
|
+
// 更新 lastScrollTop,确保滚动方向判断准确
|
|
800
|
+
nextTick(() => {
|
|
801
|
+
lastScrollTop = containerRef.value?.scrollTop || 0;
|
|
802
|
+
});
|
|
714
803
|
}
|
|
715
|
-
}
|
|
804
|
+
}
|
|
716
805
|
};
|
|
717
806
|
|
|
718
807
|
const reset = () => {
|
|
719
808
|
rotation.value = 0;
|
|
720
|
-
|
|
809
|
+
|
|
721
810
|
// 如果启用自适应宽度且有初始缩放比例,恢复到初始缩放比例
|
|
722
811
|
if (props.autoFitWidth && initialAutoFitScale.value !== null) {
|
|
723
812
|
scale.value = initialAutoFitScale.value;
|
|
@@ -725,24 +814,30 @@ const reset = () => {
|
|
|
725
814
|
} else {
|
|
726
815
|
scale.value = 1;
|
|
727
816
|
}
|
|
728
|
-
|
|
817
|
+
|
|
729
818
|
position.value = { x: 0, y: 0 };
|
|
730
|
-
|
|
819
|
+
|
|
731
820
|
// 重置文本选择状态和批注状态
|
|
732
821
|
showAnnotationPopup.value = false;
|
|
733
822
|
currentAnnotationBlock.value = null;
|
|
734
|
-
annotationInput.value =
|
|
823
|
+
annotationInput.value = "";
|
|
735
824
|
activeBlockDiv.value = null;
|
|
736
825
|
isHighlighted.value = false;
|
|
737
826
|
};
|
|
738
827
|
|
|
739
828
|
const original = () => {
|
|
740
|
-
emit(
|
|
829
|
+
emit("original");
|
|
741
830
|
};
|
|
742
831
|
|
|
743
832
|
// 计算自适应宽度的缩放比例
|
|
744
833
|
const calculateAutoFitScale = () => {
|
|
745
|
-
if (!props.autoFitWidth || !containerRef.value
|
|
834
|
+
if (!props.autoFitWidth || !containerRef.value) {
|
|
835
|
+
return 1;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// 使用第一页的图片尺寸作为基准(所有页面使用相同的缩放比例)
|
|
839
|
+
const firstPageSize = imageSizes.get(1);
|
|
840
|
+
if (!firstPageSize || firstPageSize.width === 0) {
|
|
746
841
|
return 1;
|
|
747
842
|
}
|
|
748
843
|
|
|
@@ -750,7 +845,7 @@ const calculateAutoFitScale = () => {
|
|
|
750
845
|
// 使用容器的完整宽度,但预留足够空间避免因浏览器渲染误差导致横向滚动条
|
|
751
846
|
// 预留 4px 以确保不会因为任何渲染误差导致滚动条
|
|
752
847
|
const containerWidth = containerRect.width - 4;
|
|
753
|
-
|
|
848
|
+
|
|
754
849
|
if (containerWidth <= 0) {
|
|
755
850
|
return 1;
|
|
756
851
|
}
|
|
@@ -758,33 +853,35 @@ const calculateAutoFitScale = () => {
|
|
|
758
853
|
// 考虑旋转角度,如果旋转了90度或270度,需要交换宽高
|
|
759
854
|
const normalizedRotation = ((rotation.value % 360) + 360) % 360;
|
|
760
855
|
const isRotated = normalizedRotation === 90 || normalizedRotation === 270;
|
|
761
|
-
|
|
762
|
-
const imageWidth = isRotated ?
|
|
763
|
-
|
|
856
|
+
|
|
857
|
+
const imageWidth = isRotated ? firstPageSize.height : firstPageSize.width;
|
|
858
|
+
|
|
764
859
|
if (imageWidth <= 0) {
|
|
765
860
|
return 1;
|
|
766
861
|
}
|
|
767
|
-
|
|
862
|
+
|
|
768
863
|
// 计算缩放比例,使图片宽度完全适应容器宽度
|
|
769
864
|
const calculatedScale = containerWidth / imageWidth;
|
|
770
|
-
|
|
865
|
+
|
|
771
866
|
// 确保缩放比例在允许的范围内
|
|
772
867
|
return Math.max(props.minScale, Math.min(props.maxScale, calculatedScale));
|
|
773
868
|
};
|
|
774
869
|
|
|
775
870
|
// 图片加载完成处理
|
|
776
|
-
const onImageLoad = (event: Event) => {
|
|
871
|
+
const onImageLoad = (event: Event, pageNum: number) => {
|
|
777
872
|
const img = event.target as HTMLImageElement;
|
|
778
|
-
imageSize.value = {
|
|
779
|
-
width: img.naturalWidth,
|
|
780
|
-
height: img.naturalHeight
|
|
781
|
-
};
|
|
782
873
|
|
|
783
|
-
//
|
|
784
|
-
|
|
874
|
+
// 存储该页的图片尺寸
|
|
875
|
+
imageSizes.set(pageNum, {
|
|
876
|
+
width: img.naturalWidth,
|
|
877
|
+
height: img.naturalHeight,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// 如果是第一页且启用自适应宽度,计算并设置初始缩放比例
|
|
881
|
+
if (pageNum === 1 && props.autoFitWidth) {
|
|
785
882
|
// 重置用户缩放标记
|
|
786
883
|
isUserZooming.value = false;
|
|
787
|
-
|
|
884
|
+
|
|
788
885
|
// 使用双重 nextTick 确保容器尺寸已确定
|
|
789
886
|
nextTick(() => {
|
|
790
887
|
nextTick(() => {
|
|
@@ -795,21 +892,29 @@ const onImageLoad = (event: Event) => {
|
|
|
795
892
|
scale.value = autoScale;
|
|
796
893
|
initialAutoFitScale.value = autoScale; // 记录初始自适应缩放比例
|
|
797
894
|
}
|
|
798
|
-
},
|
|
895
|
+
}, 100); // 增加延迟,确保所有图片都已加载
|
|
799
896
|
});
|
|
800
897
|
});
|
|
801
898
|
}
|
|
802
899
|
|
|
803
|
-
|
|
900
|
+
// 如果第一页已经加载完成,且当前页不是第一页,也应用自适应宽度
|
|
901
|
+
if (pageNum > 1 && props.autoFitWidth && initialAutoFitScale.value !== null) {
|
|
902
|
+
// 确保后续页面也使用相同的缩放比例
|
|
903
|
+
if (Math.abs(scale.value - initialAutoFitScale.value) > 0.01) {
|
|
904
|
+
scale.value = initialAutoFitScale.value;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
emit("load", {
|
|
804
909
|
width: img.naturalWidth,
|
|
805
910
|
height: img.naturalHeight,
|
|
806
|
-
page:
|
|
807
|
-
totalPages: totalPages.value
|
|
911
|
+
page: pageNum,
|
|
912
|
+
totalPages: totalPages.value,
|
|
808
913
|
});
|
|
809
|
-
|
|
810
|
-
//
|
|
914
|
+
|
|
915
|
+
// 图片加载完成后,渲染该页的文本图层
|
|
811
916
|
nextTick(() => {
|
|
812
|
-
renderTextLayer();
|
|
917
|
+
renderTextLayer(pageNum);
|
|
813
918
|
});
|
|
814
919
|
};
|
|
815
920
|
|
|
@@ -825,7 +930,7 @@ const onImageLoad = (event: Event) => {
|
|
|
825
930
|
const calculateFontSize = (
|
|
826
931
|
text: string,
|
|
827
932
|
targetWidth: number,
|
|
828
|
-
targetHeight: number
|
|
933
|
+
targetHeight: number
|
|
829
934
|
): number => {
|
|
830
935
|
// 创建一个离屏 Canvas 用于测量文本
|
|
831
936
|
const canvas = document.createElement("canvas");
|
|
@@ -870,34 +975,50 @@ const calculateFontSize = (
|
|
|
870
975
|
};
|
|
871
976
|
|
|
872
977
|
/**
|
|
873
|
-
* 渲染文本图层(使用 blocksData
|
|
978
|
+
* 渲染文本图层(使用 blocksData 数据,支持增量渲染)
|
|
874
979
|
*/
|
|
875
|
-
const renderTextLayer = () => {
|
|
876
|
-
const
|
|
877
|
-
const
|
|
980
|
+
const renderTextLayer = (pageNum?: number) => {
|
|
981
|
+
const targetPage = pageNum || currentPage.value;
|
|
982
|
+
const textLayer = textLayerRefs.get(targetPage);
|
|
983
|
+
const image = imageRefs.get(targetPage);
|
|
878
984
|
|
|
879
985
|
if (!textLayer || !image) {
|
|
880
986
|
return;
|
|
881
987
|
}
|
|
882
988
|
|
|
883
|
-
|
|
989
|
+
// 如果图片还没加载完成,等待加载完成后再渲染
|
|
990
|
+
if (!image.complete || image.naturalWidth === 0) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const pageBlocksData = getPageBlocksData(targetPage);
|
|
995
|
+
console.log("renderTextLayer", targetPage, pageBlocksData);
|
|
884
996
|
|
|
885
|
-
//
|
|
886
|
-
|
|
887
|
-
|
|
997
|
+
// 设置文本图层的尺寸与图片尺寸一致
|
|
998
|
+
textLayer.style.width = `${image.naturalWidth}px`;
|
|
999
|
+
textLayer.style.height = `${image.naturalHeight}px`;
|
|
1000
|
+
|
|
1001
|
+
// 如果没有提供分块数据,清空文本图层
|
|
1002
|
+
if (!pageBlocksData || pageBlocksData.length === 0) {
|
|
1003
|
+
textLayer.innerHTML = "";
|
|
888
1004
|
return;
|
|
889
1005
|
}
|
|
890
1006
|
|
|
891
1007
|
try {
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
textLayer.
|
|
1008
|
+
// 增量渲染:获取现有的文本块
|
|
1009
|
+
const existingBlocks = new Map<string, HTMLElement>();
|
|
1010
|
+
textLayer.querySelectorAll(".text-block").forEach((el) => {
|
|
1011
|
+
const bboxStr = (el as HTMLElement).dataset.bbox;
|
|
1012
|
+
if (bboxStr) {
|
|
1013
|
+
existingBlocks.set(bboxStr, el as HTMLElement);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
895
1016
|
|
|
896
|
-
//
|
|
897
|
-
|
|
1017
|
+
// 创建新的文本块 Map,用于跟踪需要保留的块
|
|
1018
|
+
const newBlocksMap = new Map<string, boolean>();
|
|
898
1019
|
|
|
899
|
-
//
|
|
900
|
-
|
|
1020
|
+
// 使用指定页码的 blocksData 创建或更新可交互的块
|
|
1021
|
+
pageBlocksData.forEach((block, index) => {
|
|
901
1022
|
const { content, bbox } = block;
|
|
902
1023
|
|
|
903
1024
|
// bbox 格式: [x1, y1, x2, y2]
|
|
@@ -909,40 +1030,40 @@ const renderTextLayer = () => {
|
|
|
909
1030
|
const calculatedFontSize = calculateFontSize(content, width, height);
|
|
910
1031
|
|
|
911
1032
|
// 创建文本块
|
|
912
|
-
const blockDiv = document.createElement(
|
|
913
|
-
blockDiv.className =
|
|
1033
|
+
const blockDiv = document.createElement("div");
|
|
1034
|
+
blockDiv.className = "text-block";
|
|
914
1035
|
blockDiv.dataset.text = content;
|
|
915
1036
|
blockDiv.dataset.bbox = JSON.stringify(bbox);
|
|
916
1037
|
|
|
917
1038
|
// 设置基础样式
|
|
918
|
-
blockDiv.style.position =
|
|
1039
|
+
blockDiv.style.position = "absolute";
|
|
919
1040
|
blockDiv.style.left = `${x1}px`;
|
|
920
1041
|
blockDiv.style.top = `${y1}px`;
|
|
921
1042
|
blockDiv.style.width = `${width}px`;
|
|
922
1043
|
blockDiv.style.height = `${height}px`;
|
|
923
|
-
blockDiv.style.zIndex =
|
|
924
|
-
blockDiv.style.cursor =
|
|
1044
|
+
blockDiv.style.zIndex = "20"; // 确保文本块在图片上方
|
|
1045
|
+
blockDiv.style.cursor = "text"; // 改为文本选择光标
|
|
925
1046
|
// 设置文本内容(使用 textContent 而不是 innerHTML,避免 XSS 风险)
|
|
926
1047
|
blockDiv.textContent = content;
|
|
927
1048
|
// 设置文本样式,确保可以选择和显示
|
|
928
|
-
blockDiv.style.color =
|
|
1049
|
+
blockDiv.style.color = "red"; // 红色文字
|
|
929
1050
|
blockDiv.style.fontSize = `${calculatedFontSize}px`; // 使用计算出的字体大小
|
|
930
|
-
blockDiv.style.fontFamily =
|
|
931
|
-
blockDiv.style.lineHeight =
|
|
932
|
-
blockDiv.style.whiteSpace =
|
|
933
|
-
blockDiv.style.overflow =
|
|
934
|
-
blockDiv.style.display =
|
|
935
|
-
blockDiv.style.visibility =
|
|
1051
|
+
blockDiv.style.fontFamily = "Arial, sans-serif"; // 设置明确的字体
|
|
1052
|
+
blockDiv.style.lineHeight = "1.2"; // 设置合适的行高
|
|
1053
|
+
blockDiv.style.whiteSpace = "pre-wrap"; // 保留换行和空格
|
|
1054
|
+
blockDiv.style.overflow = "visible"; // 确保文字不被裁剪
|
|
1055
|
+
blockDiv.style.display = "block"; // 确保是块级元素
|
|
1056
|
+
blockDiv.style.visibility = "visible"; // 确保可见
|
|
936
1057
|
// 允许文本选择
|
|
937
|
-
blockDiv.style.userSelect =
|
|
938
|
-
blockDiv.style.webkitUserSelect =
|
|
939
|
-
blockDiv.style.mozUserSelect =
|
|
940
|
-
blockDiv.style.msUserSelect =
|
|
1058
|
+
blockDiv.style.userSelect = "text";
|
|
1059
|
+
blockDiv.style.webkitUserSelect = "text";
|
|
1060
|
+
blockDiv.style.mozUserSelect = "text";
|
|
1061
|
+
blockDiv.style.msUserSelect = "text";
|
|
941
1062
|
// 允许指针事件,但不阻止文本选择
|
|
942
|
-
blockDiv.style.pointerEvents =
|
|
1063
|
+
blockDiv.style.pointerEvents = "auto";
|
|
943
1064
|
|
|
944
1065
|
// 右键菜单:显示批注按钮
|
|
945
|
-
blockDiv.addEventListener(
|
|
1066
|
+
blockDiv.addEventListener("contextmenu", (e) => {
|
|
946
1067
|
e.preventDefault();
|
|
947
1068
|
// 设置当前文本块为激活状态
|
|
948
1069
|
activeBlockDiv.value = blockDiv;
|
|
@@ -954,78 +1075,155 @@ const renderTextLayer = () => {
|
|
|
954
1075
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
955
1076
|
if (existingAnnotation) {
|
|
956
1077
|
// 添加批注标记样式类
|
|
957
|
-
blockDiv.classList.add(
|
|
1078
|
+
blockDiv.classList.add("has-annotation");
|
|
958
1079
|
blockDiv.title = `已有批注: ${existingAnnotation.content}`;
|
|
959
1080
|
// 直接设置样式,确保生效(内联样式优先级高于CSS类)
|
|
960
|
-
blockDiv.style.backgroundColor =
|
|
961
|
-
blockDiv.style.border =
|
|
962
|
-
blockDiv.style.borderRadius =
|
|
963
|
-
blockDiv.style.padding =
|
|
964
|
-
blockDiv.style.boxShadow =
|
|
965
|
-
|
|
1081
|
+
blockDiv.style.backgroundColor = "rgba(255, 243, 205, 0.5)";
|
|
1082
|
+
blockDiv.style.border = "1px solid rgba(255, 193, 7, 0.7)";
|
|
1083
|
+
blockDiv.style.borderRadius = "3px";
|
|
1084
|
+
blockDiv.style.padding = "1px 3px";
|
|
1085
|
+
blockDiv.style.boxShadow = "0 1px 2px rgba(255, 193, 7, 0.25)";
|
|
1086
|
+
|
|
966
1087
|
// 创建批注图标标记
|
|
967
|
-
const annotationMarker = document.createElement(
|
|
968
|
-
annotationMarker.className =
|
|
969
|
-
annotationMarker.textContent =
|
|
970
|
-
annotationMarker.style.position =
|
|
971
|
-
annotationMarker.style.top =
|
|
972
|
-
annotationMarker.style.right =
|
|
973
|
-
annotationMarker.style.fontSize =
|
|
974
|
-
annotationMarker.style.backgroundColor =
|
|
975
|
-
annotationMarker.style.borderRadius =
|
|
976
|
-
annotationMarker.style.width =
|
|
977
|
-
annotationMarker.style.height =
|
|
978
|
-
annotationMarker.style.display =
|
|
979
|
-
annotationMarker.style.alignItems =
|
|
980
|
-
annotationMarker.style.justifyContent =
|
|
981
|
-
annotationMarker.style.zIndex =
|
|
982
|
-
annotationMarker.style.boxShadow =
|
|
983
|
-
annotationMarker.style.lineHeight =
|
|
1088
|
+
const annotationMarker = document.createElement("span");
|
|
1089
|
+
annotationMarker.className = "annotation-marker";
|
|
1090
|
+
annotationMarker.textContent = "📝";
|
|
1091
|
+
annotationMarker.style.position = "absolute";
|
|
1092
|
+
annotationMarker.style.top = "-6px";
|
|
1093
|
+
annotationMarker.style.right = "-6px";
|
|
1094
|
+
annotationMarker.style.fontSize = "11px";
|
|
1095
|
+
annotationMarker.style.backgroundColor = "rgba(255, 193, 7, 0.95)";
|
|
1096
|
+
annotationMarker.style.borderRadius = "50%";
|
|
1097
|
+
annotationMarker.style.width = "16px";
|
|
1098
|
+
annotationMarker.style.height = "16px";
|
|
1099
|
+
annotationMarker.style.display = "flex";
|
|
1100
|
+
annotationMarker.style.alignItems = "center";
|
|
1101
|
+
annotationMarker.style.justifyContent = "center";
|
|
1102
|
+
annotationMarker.style.zIndex = "30";
|
|
1103
|
+
annotationMarker.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.25)";
|
|
1104
|
+
annotationMarker.style.lineHeight = "1";
|
|
984
1105
|
blockDiv.appendChild(annotationMarker);
|
|
985
1106
|
} else {
|
|
986
1107
|
// 没有批注时设置为透明背景
|
|
987
|
-
blockDiv.style.backgroundColor =
|
|
1108
|
+
blockDiv.style.backgroundColor = "transparent";
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 检查是否已存在相同的文本块(通过 bbox 匹配)
|
|
1112
|
+
const bboxKey = JSON.stringify(bbox);
|
|
1113
|
+
const existingBlock = existingBlocks.get(bboxKey);
|
|
1114
|
+
|
|
1115
|
+
if (existingBlock) {
|
|
1116
|
+
// 如果已存在,更新内容(增量更新,避免闪烁)
|
|
1117
|
+
existingBlock.textContent = content;
|
|
1118
|
+
existingBlock.dataset.text = content;
|
|
1119
|
+
// 更新批注状态
|
|
1120
|
+
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1121
|
+
if (existingAnnotation) {
|
|
1122
|
+
if (!existingBlock.classList.contains("has-annotation")) {
|
|
1123
|
+
existingBlock.classList.add("has-annotation");
|
|
1124
|
+
existingBlock.title = `已有批注: ${existingAnnotation.content}`;
|
|
1125
|
+
existingBlock.style.backgroundColor = "rgba(255, 243, 205, 0.5)";
|
|
1126
|
+
existingBlock.style.border = "1px solid rgba(255, 193, 7, 0.7)";
|
|
1127
|
+
existingBlock.style.borderRadius = "3px";
|
|
1128
|
+
existingBlock.style.padding = "1px 3px";
|
|
1129
|
+
existingBlock.style.boxShadow = "0 1px 2px rgba(255, 193, 7, 0.25)";
|
|
1130
|
+
|
|
1131
|
+
// 添加批注标记(如果还没有)
|
|
1132
|
+
if (!existingBlock.querySelector(".annotation-marker")) {
|
|
1133
|
+
const annotationMarker = document.createElement("span");
|
|
1134
|
+
annotationMarker.className = "annotation-marker";
|
|
1135
|
+
annotationMarker.textContent = "📝";
|
|
1136
|
+
annotationMarker.style.position = "absolute";
|
|
1137
|
+
annotationMarker.style.top = "-6px";
|
|
1138
|
+
annotationMarker.style.right = "-6px";
|
|
1139
|
+
annotationMarker.style.fontSize = "11px";
|
|
1140
|
+
annotationMarker.style.backgroundColor = "rgba(255, 193, 7, 0.95)";
|
|
1141
|
+
annotationMarker.style.borderRadius = "50%";
|
|
1142
|
+
annotationMarker.style.width = "16px";
|
|
1143
|
+
annotationMarker.style.height = "16px";
|
|
1144
|
+
annotationMarker.style.display = "flex";
|
|
1145
|
+
annotationMarker.style.alignItems = "center";
|
|
1146
|
+
annotationMarker.style.justifyContent = "center";
|
|
1147
|
+
annotationMarker.style.zIndex = "30";
|
|
1148
|
+
annotationMarker.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.25)";
|
|
1149
|
+
annotationMarker.style.lineHeight = "1";
|
|
1150
|
+
existingBlock.appendChild(annotationMarker);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
} else {
|
|
1154
|
+
// 移除批注标记
|
|
1155
|
+
if (existingBlock.classList.contains("has-annotation")) {
|
|
1156
|
+
existingBlock.classList.remove("has-annotation");
|
|
1157
|
+
existingBlock.title = "";
|
|
1158
|
+
existingBlock.style.backgroundColor = "transparent";
|
|
1159
|
+
existingBlock.style.border = "none";
|
|
1160
|
+
existingBlock.style.padding = "0";
|
|
1161
|
+
existingBlock.style.boxShadow = "none";
|
|
1162
|
+
const marker = existingBlock.querySelector(".annotation-marker");
|
|
1163
|
+
if (marker) {
|
|
1164
|
+
marker.remove();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
newBlocksMap.set(bboxKey, true);
|
|
1169
|
+
} else {
|
|
1170
|
+
// 如果不存在,创建新的文本块
|
|
1171
|
+
textLayer.appendChild(blockDiv);
|
|
1172
|
+
newBlocksMap.set(bboxKey, true);
|
|
988
1173
|
}
|
|
1174
|
+
});
|
|
989
1175
|
|
|
990
|
-
|
|
1176
|
+
// 删除不再存在的文本块(增量删除)
|
|
1177
|
+
existingBlocks.forEach((block, bboxKey) => {
|
|
1178
|
+
if (!newBlocksMap.has(bboxKey)) {
|
|
1179
|
+
block.remove();
|
|
1180
|
+
}
|
|
991
1181
|
});
|
|
992
1182
|
} catch (error) {
|
|
993
|
-
console.error(
|
|
1183
|
+
console.error("❌ 文本图层渲染失败:", error);
|
|
994
1184
|
}
|
|
995
1185
|
};
|
|
996
1186
|
|
|
997
1187
|
/**
|
|
998
1188
|
* 获取文本块对应的批注
|
|
999
1189
|
*/
|
|
1000
|
-
const getAnnotationForBlock = (
|
|
1190
|
+
const getAnnotationForBlock = (
|
|
1191
|
+
bbox: [number, number, number, number]
|
|
1192
|
+
): AnnotationInfo | null => {
|
|
1001
1193
|
if (!props.annotations || props.annotations.length === 0) {
|
|
1002
1194
|
return null;
|
|
1003
1195
|
}
|
|
1004
|
-
|
|
1196
|
+
|
|
1005
1197
|
const tolerance = 2; // 容差
|
|
1006
|
-
return
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1198
|
+
return (
|
|
1199
|
+
props.annotations.find((annotation) => {
|
|
1200
|
+
const [x1, y1, x2, y2] = annotation.blockBbox;
|
|
1201
|
+
return (
|
|
1202
|
+
Math.abs(x1 - bbox[0]) < tolerance &&
|
|
1203
|
+
Math.abs(y1 - bbox[1]) < tolerance &&
|
|
1204
|
+
Math.abs(x2 - bbox[2]) < tolerance &&
|
|
1205
|
+
Math.abs(y2 - bbox[3]) < tolerance &&
|
|
1206
|
+
(annotation.blockPage === undefined ||
|
|
1207
|
+
annotation.blockPage === currentPage.value)
|
|
1208
|
+
);
|
|
1209
|
+
}) || null
|
|
1210
|
+
);
|
|
1016
1211
|
};
|
|
1017
1212
|
|
|
1018
1213
|
/**
|
|
1019
1214
|
* 显示文本块的批注按钮
|
|
1020
1215
|
*/
|
|
1021
|
-
const showAnnotationButtonForBlock = (
|
|
1022
|
-
|
|
1023
|
-
|
|
1216
|
+
const showAnnotationButtonForBlock = (
|
|
1217
|
+
event: MouseEvent,
|
|
1218
|
+
blockDiv: HTMLElement
|
|
1219
|
+
) => {
|
|
1220
|
+
const text = blockDiv.dataset.text || "";
|
|
1221
|
+
const bboxStr = blockDiv.dataset.bbox || "";
|
|
1024
1222
|
if (!text || !bboxStr) return;
|
|
1025
1223
|
|
|
1026
1224
|
try {
|
|
1027
1225
|
const bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1028
|
-
|
|
1226
|
+
|
|
1029
1227
|
const rect = blockDiv.getBoundingClientRect();
|
|
1030
1228
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1031
1229
|
|
|
@@ -1038,8 +1236,10 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1038
1236
|
const spacing = 8;
|
|
1039
1237
|
|
|
1040
1238
|
// 计算相对于容器的坐标(考虑滚动)
|
|
1041
|
-
const relativeX =
|
|
1042
|
-
|
|
1239
|
+
const relativeX =
|
|
1240
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1241
|
+
const relativeY =
|
|
1242
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1043
1243
|
|
|
1044
1244
|
// 容器可用空间
|
|
1045
1245
|
const containerWidth = containerRef.value.clientWidth;
|
|
@@ -1053,7 +1253,9 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1053
1253
|
const bottomSpace = containerBottom - rectBottom;
|
|
1054
1254
|
const topSpace = rectTop - containerTop;
|
|
1055
1255
|
|
|
1056
|
-
const showOnBottom =
|
|
1256
|
+
const showOnBottom =
|
|
1257
|
+
bottomSpace >= popupHeight + spacing ||
|
|
1258
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
1057
1259
|
|
|
1058
1260
|
// 计算最终位置
|
|
1059
1261
|
let left: number;
|
|
@@ -1069,14 +1271,16 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1069
1271
|
}
|
|
1070
1272
|
|
|
1071
1273
|
// 边界检查
|
|
1072
|
-
const popupTopInViewport =
|
|
1274
|
+
const popupTopInViewport =
|
|
1275
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1073
1276
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
1074
1277
|
|
|
1075
1278
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
1076
1279
|
top = relativeY - popupHeight - spacing;
|
|
1077
1280
|
}
|
|
1078
1281
|
|
|
1079
|
-
const newPopupTopInViewport =
|
|
1282
|
+
const newPopupTopInViewport =
|
|
1283
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1080
1284
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
1081
1285
|
top = relativeY + rect.height + spacing;
|
|
1082
1286
|
}
|
|
@@ -1091,12 +1295,12 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1091
1295
|
|
|
1092
1296
|
annotationPopupStyle.value = {
|
|
1093
1297
|
left: `${left}px`,
|
|
1094
|
-
top: `${top}px
|
|
1298
|
+
top: `${top}px`,
|
|
1095
1299
|
};
|
|
1096
1300
|
|
|
1097
1301
|
showAnnotationPopup.value = true;
|
|
1098
1302
|
} catch (error) {
|
|
1099
|
-
console.error(
|
|
1303
|
+
console.error("解析bbox失败:", error);
|
|
1100
1304
|
}
|
|
1101
1305
|
};
|
|
1102
1306
|
|
|
@@ -1108,13 +1312,13 @@ const hideAnnotationButton = () => {
|
|
|
1108
1312
|
if (currentAnnotationBlock.value) {
|
|
1109
1313
|
return;
|
|
1110
1314
|
}
|
|
1111
|
-
|
|
1315
|
+
|
|
1112
1316
|
hideTimer = setTimeout(() => {
|
|
1113
1317
|
showAnnotationPopup.value = false;
|
|
1114
1318
|
|
|
1115
1319
|
if (activeBlockDiv.value && !isHighlighted.value) {
|
|
1116
|
-
activeBlockDiv.value.style.backgroundColor =
|
|
1117
|
-
activeBlockDiv.value.style.boxShadow =
|
|
1320
|
+
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1321
|
+
activeBlockDiv.value.style.boxShadow = "none";
|
|
1118
1322
|
activeBlockDiv.value = null;
|
|
1119
1323
|
}
|
|
1120
1324
|
}, 300);
|
|
@@ -1138,69 +1342,71 @@ const openAnnotationInput = (e?: Event) => {
|
|
|
1138
1342
|
if (e) {
|
|
1139
1343
|
e.stopPropagation();
|
|
1140
1344
|
}
|
|
1141
|
-
|
|
1345
|
+
|
|
1142
1346
|
// 获取选中的文本
|
|
1143
1347
|
const selection = window.getSelection();
|
|
1144
|
-
let selectedText =
|
|
1145
|
-
|
|
1348
|
+
let selectedText = "";
|
|
1349
|
+
|
|
1146
1350
|
if (selection && selection.toString().trim().length > 0) {
|
|
1147
1351
|
selectedText = selection.toString().trim();
|
|
1148
1352
|
} else if (activeBlockDiv.value) {
|
|
1149
|
-
selectedText = activeBlockDiv.value.dataset.text ||
|
|
1353
|
+
selectedText = activeBlockDiv.value.dataset.text || "";
|
|
1150
1354
|
}
|
|
1151
|
-
|
|
1355
|
+
|
|
1152
1356
|
if (!selectedText && !activeBlockDiv.value) return;
|
|
1153
|
-
|
|
1357
|
+
|
|
1154
1358
|
// 如果有激活的文本块,使用文本块的 bbox
|
|
1155
1359
|
let bbox: [number, number, number, number] | null = null;
|
|
1156
1360
|
if (activeBlockDiv.value) {
|
|
1157
|
-
const bboxStr = activeBlockDiv.value.dataset.bbox ||
|
|
1361
|
+
const bboxStr = activeBlockDiv.value.dataset.bbox || "";
|
|
1158
1362
|
try {
|
|
1159
1363
|
bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1160
1364
|
} catch (error) {
|
|
1161
|
-
console.error(
|
|
1365
|
+
console.error("解析 bbox 失败:", error);
|
|
1162
1366
|
}
|
|
1163
1367
|
}
|
|
1164
|
-
|
|
1368
|
+
|
|
1165
1369
|
// 如果没有 bbox,尝试从选中文本的位置计算
|
|
1166
1370
|
if (!bbox && selection && selection.rangeCount > 0) {
|
|
1167
1371
|
const range = selection.getRangeAt(0);
|
|
1168
1372
|
const rect = range.getBoundingClientRect();
|
|
1169
1373
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1170
|
-
|
|
1374
|
+
|
|
1171
1375
|
if (containerRect && containerRef.value) {
|
|
1172
|
-
const relativeX =
|
|
1173
|
-
|
|
1376
|
+
const relativeX =
|
|
1377
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1378
|
+
const relativeY =
|
|
1379
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1174
1380
|
bbox = [
|
|
1175
1381
|
relativeX,
|
|
1176
1382
|
relativeY,
|
|
1177
1383
|
relativeX + rect.width,
|
|
1178
|
-
relativeY + rect.height
|
|
1384
|
+
relativeY + rect.height,
|
|
1179
1385
|
];
|
|
1180
1386
|
}
|
|
1181
1387
|
}
|
|
1182
|
-
|
|
1388
|
+
|
|
1183
1389
|
if (!bbox) {
|
|
1184
|
-
Message.warning(
|
|
1390
|
+
Message.warning("无法确定文本位置");
|
|
1185
1391
|
return;
|
|
1186
1392
|
}
|
|
1187
|
-
|
|
1393
|
+
|
|
1188
1394
|
// 检查是否已有批注
|
|
1189
1395
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1190
1396
|
if (existingAnnotation) {
|
|
1191
1397
|
annotationInput.value = existingAnnotation.content;
|
|
1192
1398
|
} else {
|
|
1193
|
-
annotationInput.value =
|
|
1399
|
+
annotationInput.value = "";
|
|
1194
1400
|
}
|
|
1195
|
-
|
|
1401
|
+
|
|
1196
1402
|
currentAnnotationBlock.value = {
|
|
1197
1403
|
bbox,
|
|
1198
|
-
content: selectedText
|
|
1404
|
+
content: selectedText,
|
|
1199
1405
|
};
|
|
1200
|
-
|
|
1406
|
+
|
|
1201
1407
|
// 确保弹窗显示
|
|
1202
1408
|
showAnnotationPopup.value = true;
|
|
1203
|
-
|
|
1409
|
+
|
|
1204
1410
|
// 重新计算弹窗位置(输入框更大)
|
|
1205
1411
|
nextTick(() => {
|
|
1206
1412
|
adjustAnnotationPopupPosition();
|
|
@@ -1213,56 +1419,62 @@ const openAnnotationInput = (e?: Event) => {
|
|
|
1213
1419
|
const adjustAnnotationPopupPosition = () => {
|
|
1214
1420
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1215
1421
|
const popup = annotationPopupRef.value;
|
|
1216
|
-
|
|
1422
|
+
|
|
1217
1423
|
if (!containerRect || !containerRef.value) {
|
|
1218
1424
|
return;
|
|
1219
1425
|
}
|
|
1220
|
-
|
|
1426
|
+
|
|
1221
1427
|
const popupWidth = popup?.getBoundingClientRect().width || 320; // 默认宽度
|
|
1222
1428
|
const popupHeight = popup?.getBoundingClientRect().height || 200; // 默认高度
|
|
1223
1429
|
const spacing = 4; // 减少间距,让弹窗更靠近文字
|
|
1224
|
-
|
|
1430
|
+
|
|
1225
1431
|
// 优先使用文本块位置(和批注按钮位置计算逻辑一致)
|
|
1226
1432
|
let left: number;
|
|
1227
1433
|
let top: number;
|
|
1228
|
-
|
|
1434
|
+
|
|
1229
1435
|
if (activeBlockDiv.value) {
|
|
1230
1436
|
// 使用文本块位置,和批注按钮使用相同的计算逻辑
|
|
1231
1437
|
const rect = activeBlockDiv.value.getBoundingClientRect();
|
|
1232
|
-
const relativeX =
|
|
1233
|
-
|
|
1234
|
-
|
|
1438
|
+
const relativeX =
|
|
1439
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1440
|
+
const relativeY =
|
|
1441
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1442
|
+
|
|
1235
1443
|
// 计算可用空间
|
|
1236
1444
|
const rectBottom = rect.bottom;
|
|
1237
1445
|
const rectTop = rect.top;
|
|
1238
1446
|
const containerBottom = containerRect.bottom;
|
|
1239
1447
|
const containerTop = containerRect.top;
|
|
1240
|
-
|
|
1448
|
+
|
|
1241
1449
|
const bottomSpace = containerBottom - rectBottom;
|
|
1242
1450
|
const topSpace = rectTop - containerTop;
|
|
1243
|
-
|
|
1451
|
+
|
|
1244
1452
|
// 判断弹窗显示在文字下方还是上方
|
|
1245
|
-
const showOnBottom =
|
|
1246
|
-
|
|
1453
|
+
const showOnBottom =
|
|
1454
|
+
bottomSpace >= popupHeight + spacing ||
|
|
1455
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
1456
|
+
|
|
1247
1457
|
// 计算位置(和批注按钮位置计算一致)
|
|
1248
1458
|
const centerX = relativeX + rect.width / 2;
|
|
1249
1459
|
left = centerX - popupWidth / 2;
|
|
1250
|
-
|
|
1460
|
+
|
|
1251
1461
|
if (showOnBottom) {
|
|
1252
1462
|
top = relativeY + rect.height + spacing;
|
|
1253
1463
|
} else {
|
|
1254
1464
|
top = relativeY - popupHeight - spacing;
|
|
1255
1465
|
}
|
|
1256
|
-
|
|
1466
|
+
|
|
1257
1467
|
// 边界检查
|
|
1258
|
-
const popupTopInViewport =
|
|
1468
|
+
const popupTopInViewport =
|
|
1469
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1259
1470
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
1260
|
-
|
|
1471
|
+
|
|
1261
1472
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
1262
1473
|
top = relativeY - popupHeight - spacing;
|
|
1263
1474
|
}
|
|
1264
|
-
|
|
1265
|
-
const newPopupTopInViewport =
|
|
1475
|
+
|
|
1476
|
+
const newPopupTopInViewport =
|
|
1477
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1266
1478
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
1267
1479
|
top = relativeY + rect.height + spacing;
|
|
1268
1480
|
}
|
|
@@ -1270,13 +1482,13 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1270
1482
|
// 如果没有文本块,使用当前按钮位置(作为后备方案)
|
|
1271
1483
|
const currentStyle = annotationPopupStyle.value;
|
|
1272
1484
|
if (currentStyle && currentStyle.left && currentStyle.top) {
|
|
1273
|
-
left = parseFloat(currentStyle.left.toString().replace(
|
|
1274
|
-
top = parseFloat(currentStyle.top.toString().replace(
|
|
1485
|
+
left = parseFloat(currentStyle.left.toString().replace("px", ""));
|
|
1486
|
+
top = parseFloat(currentStyle.top.toString().replace("px", ""));
|
|
1275
1487
|
} else {
|
|
1276
1488
|
return;
|
|
1277
1489
|
}
|
|
1278
1490
|
}
|
|
1279
|
-
|
|
1491
|
+
|
|
1280
1492
|
// 确保不超出容器边界
|
|
1281
1493
|
const containerWidth = containerRef.value.clientWidth;
|
|
1282
1494
|
if (left + popupWidth > containerWidth - spacing) {
|
|
@@ -1285,10 +1497,10 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1285
1497
|
if (left < spacing) {
|
|
1286
1498
|
left = spacing;
|
|
1287
1499
|
}
|
|
1288
|
-
|
|
1500
|
+
|
|
1289
1501
|
annotationPopupStyle.value = {
|
|
1290
1502
|
left: `${left}px`,
|
|
1291
|
-
top: `${top}px
|
|
1503
|
+
top: `${top}px`,
|
|
1292
1504
|
};
|
|
1293
1505
|
};
|
|
1294
1506
|
|
|
@@ -1297,7 +1509,7 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1297
1509
|
*/
|
|
1298
1510
|
const closeAnnotationInput = () => {
|
|
1299
1511
|
currentAnnotationBlock.value = null;
|
|
1300
|
-
annotationInput.value =
|
|
1512
|
+
annotationInput.value = "";
|
|
1301
1513
|
showAnnotationPopup.value = false;
|
|
1302
1514
|
};
|
|
1303
1515
|
|
|
@@ -1310,7 +1522,7 @@ const handleAnnotationPopupLeave = (e: MouseEvent) => {
|
|
|
1310
1522
|
if (relatedTarget && annotationPopupRef.value?.contains(relatedTarget)) {
|
|
1311
1523
|
return;
|
|
1312
1524
|
}
|
|
1313
|
-
|
|
1525
|
+
|
|
1314
1526
|
// 延迟关闭,给用户时间操作
|
|
1315
1527
|
hideTimer = setTimeout(() => {
|
|
1316
1528
|
closeAnnotationInput();
|
|
@@ -1324,35 +1536,37 @@ const saveAnnotation = () => {
|
|
|
1324
1536
|
if (!currentAnnotationBlock.value || !annotationInput.value.trim()) {
|
|
1325
1537
|
return;
|
|
1326
1538
|
}
|
|
1327
|
-
|
|
1539
|
+
|
|
1328
1540
|
const { bbox, content } = currentAnnotationBlock.value;
|
|
1329
1541
|
const annotationContent = annotationInput.value.trim();
|
|
1330
|
-
|
|
1542
|
+
|
|
1331
1543
|
// 检查是否已有批注
|
|
1332
1544
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1333
|
-
|
|
1545
|
+
|
|
1334
1546
|
const annotation: AnnotationInfo = {
|
|
1335
|
-
id:
|
|
1547
|
+
id:
|
|
1548
|
+
existingAnnotation?.id ||
|
|
1549
|
+
`annotation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1336
1550
|
blockBbox: bbox,
|
|
1337
1551
|
blockContent: content,
|
|
1338
1552
|
blockPage: currentPage.value,
|
|
1339
1553
|
content: annotationContent,
|
|
1340
|
-
createTime: existingAnnotation?.createTime || Date.now()
|
|
1554
|
+
createTime: existingAnnotation?.createTime || Date.now(),
|
|
1341
1555
|
};
|
|
1342
|
-
|
|
1556
|
+
|
|
1343
1557
|
if (existingAnnotation) {
|
|
1344
1558
|
// 更新已有批注
|
|
1345
|
-
emit(
|
|
1346
|
-
Message.success(
|
|
1559
|
+
emit("annotation-update", annotation);
|
|
1560
|
+
Message.success("批注已更新");
|
|
1347
1561
|
} else {
|
|
1348
1562
|
// 添加新批注
|
|
1349
|
-
emit(
|
|
1350
|
-
Message.success(
|
|
1563
|
+
emit("annotation-add", annotation);
|
|
1564
|
+
Message.success("批注已添加");
|
|
1351
1565
|
}
|
|
1352
|
-
|
|
1566
|
+
|
|
1353
1567
|
// 关闭输入框
|
|
1354
1568
|
closeAnnotationInput();
|
|
1355
|
-
|
|
1569
|
+
|
|
1356
1570
|
// 重新渲染文本图层以显示批注标记
|
|
1357
1571
|
nextTick(() => {
|
|
1358
1572
|
renderTextLayer();
|
|
@@ -1372,85 +1586,134 @@ const handleScroll = (e: Event) => {
|
|
|
1372
1586
|
showAnnotationPopup.value = false;
|
|
1373
1587
|
activeBlockDiv.value = null;
|
|
1374
1588
|
}
|
|
1375
|
-
|
|
1589
|
+
|
|
1376
1590
|
// 在自适应宽度模式下,滚动操作不应该改变缩放比例
|
|
1377
1591
|
// 如果缩放比例被意外改变,恢复它
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1592
|
+
if (
|
|
1593
|
+
props.autoFitWidth &&
|
|
1594
|
+
!isUserZooming.value &&
|
|
1595
|
+
initialAutoFitScale.value !== null &&
|
|
1596
|
+
Math.abs(scale.value - initialAutoFitScale.value) > 0.01
|
|
1597
|
+
) {
|
|
1382
1598
|
scale.value = initialAutoFitScale.value;
|
|
1383
1599
|
}
|
|
1384
|
-
|
|
1600
|
+
|
|
1385
1601
|
// 滚动翻页功能
|
|
1386
|
-
if (
|
|
1602
|
+
if (
|
|
1603
|
+
props.enableScrollPaging &&
|
|
1604
|
+
totalPages.value > 1 &&
|
|
1605
|
+
!isScrollPaging.value
|
|
1606
|
+
) {
|
|
1387
1607
|
handleScrollPaging(e);
|
|
1388
1608
|
}
|
|
1389
1609
|
};
|
|
1390
1610
|
|
|
1611
|
+
// 记录上次滚动位置,用于判断滚动方向
|
|
1612
|
+
let lastScrollTop = 0;
|
|
1613
|
+
|
|
1391
1614
|
/**
|
|
1392
|
-
*
|
|
1615
|
+
* 处理滚动翻页(通过滚动位置判断当前页)
|
|
1616
|
+
* 向下滑动:当视口顶部到达下一页的顶部时,切换到下一页
|
|
1617
|
+
* 向上滑动:当视口底部到达上一页的底部时,切换到上一页
|
|
1393
1618
|
*/
|
|
1394
1619
|
const handleScrollPaging = (e: Event) => {
|
|
1395
1620
|
const container = e.target as HTMLElement;
|
|
1396
|
-
if (!container) return;
|
|
1397
|
-
|
|
1621
|
+
if (!container || !containerRef.value) return;
|
|
1622
|
+
|
|
1398
1623
|
// 如果正在翻页中,直接返回
|
|
1399
1624
|
if (isScrollPaging.value) {
|
|
1400
1625
|
return;
|
|
1401
1626
|
}
|
|
1402
|
-
|
|
1627
|
+
|
|
1403
1628
|
// 清除之前的定时器
|
|
1404
1629
|
if (scrollPagingTimer) {
|
|
1405
1630
|
clearTimeout(scrollPagingTimer);
|
|
1406
1631
|
}
|
|
1407
|
-
|
|
1408
|
-
//
|
|
1632
|
+
|
|
1633
|
+
// 使用防抖,避免频繁触发
|
|
1409
1634
|
scrollPagingTimer = setTimeout(() => {
|
|
1410
1635
|
// 再次检查是否正在翻页
|
|
1411
1636
|
if (isScrollPaging.value) {
|
|
1412
1637
|
return;
|
|
1413
1638
|
}
|
|
1414
|
-
|
|
1639
|
+
|
|
1415
1640
|
const scrollTop = container.scrollTop;
|
|
1416
|
-
const scrollHeight = container.scrollHeight;
|
|
1417
1641
|
const clientHeight = container.clientHeight;
|
|
1642
|
+
const scrollBottom = scrollTop + clientHeight;
|
|
1418
1643
|
|
|
1419
|
-
//
|
|
1420
|
-
|
|
1644
|
+
// 判断滚动方向
|
|
1645
|
+
const isScrollingDown = scrollTop > lastScrollTop;
|
|
1646
|
+
lastScrollTop = scrollTop;
|
|
1647
|
+
|
|
1648
|
+
// 获取当前页的元素
|
|
1649
|
+
const currentPageElement = container.querySelector(
|
|
1650
|
+
`[data-page-number="${currentPage.value}"]`
|
|
1651
|
+
) as HTMLElement;
|
|
1652
|
+
|
|
1653
|
+
if (!currentPageElement) return;
|
|
1654
|
+
|
|
1655
|
+
const currentPageTop = currentPageElement.offsetTop;
|
|
1656
|
+
const currentPageHeight = currentPageElement.offsetHeight;
|
|
1657
|
+
const currentPageBottom = currentPageTop + currentPageHeight;
|
|
1658
|
+
|
|
1659
|
+
let newPage = currentPage.value;
|
|
1660
|
+
|
|
1661
|
+
if (isScrollingDown) {
|
|
1662
|
+
// 向下滑动:当视口顶部到达或超过下一页的顶部时,切换到下一页
|
|
1421
1663
|
if (currentPage.value < totalPages.value) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1664
|
+
const nextPageElement = container.querySelector(
|
|
1665
|
+
`[data-page-number="${currentPage.value + 1}"]`
|
|
1666
|
+
) as HTMLElement;
|
|
1667
|
+
|
|
1668
|
+
if (nextPageElement) {
|
|
1669
|
+
const nextPageTop = nextPageElement.offsetTop;
|
|
1670
|
+
// 当视口顶部到达或超过下一页的顶部时,切换到下一页
|
|
1671
|
+
if (scrollTop >= nextPageTop) {
|
|
1672
|
+
newPage = currentPage.value + 1;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1431
1675
|
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
else if (scrollTop <= 100) {
|
|
1676
|
+
} else {
|
|
1677
|
+
// 向上滑动:当视口底部到达或超过上一页的底部时,切换到上一页
|
|
1435
1678
|
if (currentPage.value > 1) {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1679
|
+
const prevPageElement = container.querySelector(
|
|
1680
|
+
`[data-page-number="${currentPage.value - 1}"]`
|
|
1681
|
+
) as HTMLElement;
|
|
1682
|
+
|
|
1683
|
+
if (prevPageElement) {
|
|
1684
|
+
const prevPageTop = prevPageElement.offsetTop;
|
|
1685
|
+
const prevPageHeight = prevPageElement.offsetHeight;
|
|
1686
|
+
const prevPageBottom = prevPageTop + prevPageHeight;
|
|
1687
|
+
|
|
1688
|
+
// 当视口底部到达或超过上一页的底部时,切换到上一页
|
|
1689
|
+
if (scrollBottom <= prevPageBottom) {
|
|
1690
|
+
newPage = currentPage.value - 1;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1445
1693
|
}
|
|
1446
1694
|
}
|
|
1447
|
-
|
|
1695
|
+
|
|
1696
|
+
// 如果页码发生变化,更新当前页
|
|
1697
|
+
if (newPage !== currentPage.value) {
|
|
1698
|
+
isScrollPaging.value = true;
|
|
1699
|
+
currentPage.value = newPage;
|
|
1700
|
+
emit("page-change", newPage, totalPages.value);
|
|
1701
|
+
|
|
1702
|
+
// 延迟解锁,确保页面切换完成
|
|
1703
|
+
setTimeout(() => {
|
|
1704
|
+
isScrollPaging.value = false;
|
|
1705
|
+
}, 100);
|
|
1706
|
+
}
|
|
1707
|
+
}, 100);
|
|
1448
1708
|
};
|
|
1449
1709
|
|
|
1450
1710
|
/**
|
|
1451
1711
|
* 检查元素是否在视口内可见
|
|
1452
1712
|
*/
|
|
1453
|
-
const isElementVisible = (
|
|
1713
|
+
const isElementVisible = (
|
|
1714
|
+
element: HTMLElement,
|
|
1715
|
+
container: HTMLElement
|
|
1716
|
+
): boolean => {
|
|
1454
1717
|
const elementRect = element.getBoundingClientRect();
|
|
1455
1718
|
const containerRect = container.getBoundingClientRect();
|
|
1456
1719
|
|
|
@@ -1464,38 +1727,51 @@ const isElementVisible = (element: HTMLElement, container: HTMLElement): boolean
|
|
|
1464
1727
|
|
|
1465
1728
|
/**
|
|
1466
1729
|
* 高亮指定位置
|
|
1467
|
-
* @param
|
|
1730
|
+
* @param pageNum 页码
|
|
1731
|
+
* @param bbox 块的边界框 [x1, y1, x2, y2](相对于该页面的坐标)
|
|
1468
1732
|
* @param shouldScroll 是否应该滚动到元素位置
|
|
1469
1733
|
* @returns 是否成功找到并高亮了元素
|
|
1470
1734
|
*/
|
|
1471
1735
|
const highlightPosition = (
|
|
1736
|
+
pageNum: number,
|
|
1472
1737
|
bbox: [number, number, number, number],
|
|
1473
1738
|
shouldScroll: boolean = true
|
|
1474
1739
|
): boolean => {
|
|
1475
1740
|
// 清除之前的高亮
|
|
1476
1741
|
if (activeBlockDiv.value) {
|
|
1477
|
-
activeBlockDiv.value.style.backgroundColor =
|
|
1478
|
-
activeBlockDiv.value.style.boxShadow =
|
|
1742
|
+
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1743
|
+
activeBlockDiv.value.style.boxShadow = "none";
|
|
1479
1744
|
activeBlockDiv.value = null;
|
|
1480
1745
|
}
|
|
1481
1746
|
isHighlighted.value = false;
|
|
1482
1747
|
|
|
1483
|
-
|
|
1748
|
+
// 如果页码不在有效范围内,返回 false
|
|
1749
|
+
if (pageNum < 1 || pageNum > totalPages.value) {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// 在指定页面的文本图层中查找
|
|
1754
|
+
const textLayer = textLayerRefs.get(pageNum);
|
|
1484
1755
|
if (!textLayer) return false;
|
|
1485
1756
|
|
|
1486
|
-
// 查找对应的块元素,使用存储的bbox数据匹配(使用容差)
|
|
1487
|
-
const blockDivs = textLayer.querySelectorAll('.text-block');
|
|
1488
1757
|
const tolerance = 2;
|
|
1489
|
-
|
|
1490
1758
|
let matchedElement: HTMLElement | null = null;
|
|
1491
1759
|
|
|
1760
|
+
// 查找对应的块元素,使用存储的bbox数据匹配(使用容差)
|
|
1761
|
+
const blockDivs = textLayer.querySelectorAll(".text-block");
|
|
1762
|
+
|
|
1492
1763
|
blockDivs.forEach((div) => {
|
|
1493
1764
|
const el = div as HTMLElement;
|
|
1494
1765
|
const storedBbox = el.dataset.bbox;
|
|
1495
1766
|
if (!storedBbox) return;
|
|
1496
1767
|
|
|
1497
1768
|
try {
|
|
1498
|
-
const parsedBbox = JSON.parse(storedBbox) as [
|
|
1769
|
+
const parsedBbox = JSON.parse(storedBbox) as [
|
|
1770
|
+
number,
|
|
1771
|
+
number,
|
|
1772
|
+
number,
|
|
1773
|
+
number
|
|
1774
|
+
];
|
|
1499
1775
|
|
|
1500
1776
|
const isMatch =
|
|
1501
1777
|
Math.abs(parsedBbox[0] - bbox[0]) < tolerance &&
|
|
@@ -1507,35 +1783,48 @@ const highlightPosition = (
|
|
|
1507
1783
|
matchedElement = el;
|
|
1508
1784
|
}
|
|
1509
1785
|
} catch (error) {
|
|
1510
|
-
console.warn(
|
|
1786
|
+
console.warn("解析bbox失败:", error);
|
|
1511
1787
|
}
|
|
1512
1788
|
});
|
|
1513
1789
|
|
|
1514
1790
|
if (!matchedElement) return false;
|
|
1515
1791
|
|
|
1792
|
+
// 如果找到的元素不在当前页,切换到对应页面
|
|
1793
|
+
if (pageNum !== currentPage.value) {
|
|
1794
|
+
switchToPage(pageNum);
|
|
1795
|
+
// 等待页面切换完成后再高亮
|
|
1796
|
+
nextTick(() => {
|
|
1797
|
+
setTimeout(() => {
|
|
1798
|
+
highlightPosition(pageNum, bbox, shouldScroll);
|
|
1799
|
+
}, 300);
|
|
1800
|
+
});
|
|
1801
|
+
return true;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1516
1804
|
// 保存引用
|
|
1517
|
-
const elementRef = matchedElement;
|
|
1805
|
+
const elementRef = matchedElement as HTMLElement;
|
|
1518
1806
|
|
|
1519
1807
|
activeBlockDiv.value = elementRef;
|
|
1520
1808
|
isHighlighted.value = true;
|
|
1521
1809
|
|
|
1522
1810
|
// 使用一致的高亮样式
|
|
1523
|
-
elementRef.style.backgroundColor =
|
|
1524
|
-
|
|
1811
|
+
elementRef.style.backgroundColor =
|
|
1812
|
+
"var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
|
|
1813
|
+
elementRef.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
|
|
1525
1814
|
|
|
1526
1815
|
// 只有在需要滚动且元素不在视口内时才滚动
|
|
1527
1816
|
if (shouldScroll && containerRef.value) {
|
|
1528
1817
|
const isVisible = isElementVisible(elementRef, containerRef.value);
|
|
1529
1818
|
if (!isVisible) {
|
|
1530
|
-
elementRef.scrollIntoView({ behavior:
|
|
1819
|
+
elementRef.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1531
1820
|
}
|
|
1532
1821
|
}
|
|
1533
1822
|
|
|
1534
1823
|
// 5秒后自动取消高亮
|
|
1535
1824
|
setTimeout(() => {
|
|
1536
1825
|
if (activeBlockDiv.value === elementRef && isHighlighted.value) {
|
|
1537
|
-
elementRef.style.backgroundColor =
|
|
1538
|
-
elementRef.style.boxShadow =
|
|
1826
|
+
elementRef.style.backgroundColor = "transparent";
|
|
1827
|
+
elementRef.style.boxShadow = "none";
|
|
1539
1828
|
activeBlockDiv.value = null;
|
|
1540
1829
|
isHighlighted.value = false;
|
|
1541
1830
|
}
|
|
@@ -1546,13 +1835,21 @@ const highlightPosition = (
|
|
|
1546
1835
|
|
|
1547
1836
|
/**
|
|
1548
1837
|
* 跳转到指定位置并高亮(外部调用接口)
|
|
1549
|
-
* @param
|
|
1838
|
+
* @param pageNum 页码(必须)
|
|
1839
|
+
* @param bbox 位置的边界框 [x1, y1, x2, y2](相对于该页面的坐标)
|
|
1550
1840
|
* @param emitEvent 是否触发跳转事件,默认为 true
|
|
1551
1841
|
*/
|
|
1552
1842
|
const jumpToPosition = (
|
|
1843
|
+
pageNum: number,
|
|
1553
1844
|
bbox: [number, number, number, number],
|
|
1554
1845
|
emitEvent: boolean = true
|
|
1555
1846
|
) => {
|
|
1847
|
+
// 如果页码不在有效范围内,直接返回
|
|
1848
|
+
if (pageNum < 1 || pageNum > totalPages.value) {
|
|
1849
|
+
console.warn(`页码 ${pageNum} 不在有效范围内 (1-${totalPages.value})`);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1556
1853
|
// 等待DOM更新后再高亮
|
|
1557
1854
|
nextTick(() => {
|
|
1558
1855
|
let retryCount = 0;
|
|
@@ -1560,17 +1857,17 @@ const jumpToPosition = (
|
|
|
1560
1857
|
const retryDelay = 200;
|
|
1561
1858
|
|
|
1562
1859
|
const tryHighlight = () => {
|
|
1563
|
-
const success = highlightPosition(bbox, true);
|
|
1860
|
+
const success = highlightPosition(pageNum, bbox, true);
|
|
1564
1861
|
if (success) {
|
|
1565
1862
|
// 高亮成功,触发事件
|
|
1566
1863
|
if (emitEvent) {
|
|
1567
|
-
emit(
|
|
1864
|
+
emit("position-jump", bbox, "block");
|
|
1568
1865
|
}
|
|
1569
1866
|
} else if (retryCount < maxRetries) {
|
|
1570
1867
|
retryCount++;
|
|
1571
1868
|
setTimeout(tryHighlight, retryDelay);
|
|
1572
1869
|
} else {
|
|
1573
|
-
console.warn(`无法找到并高亮指定位置: bbox: [${bbox.join(
|
|
1870
|
+
console.warn(`无法找到并高亮指定位置: 页码 ${pageNum}, bbox: [${bbox.join(", ")}]`);
|
|
1574
1871
|
}
|
|
1575
1872
|
};
|
|
1576
1873
|
|
|
@@ -1579,65 +1876,58 @@ const jumpToPosition = (
|
|
|
1579
1876
|
};
|
|
1580
1877
|
|
|
1581
1878
|
/**
|
|
1582
|
-
* 监听 blocksData
|
|
1879
|
+
* 监听 blocksData 变化,增量渲染所有页面的文本图层(避免闪烁)
|
|
1583
1880
|
*/
|
|
1584
1881
|
watch(
|
|
1585
1882
|
() => props.blocksData,
|
|
1586
1883
|
() => {
|
|
1587
1884
|
nextTick(() => {
|
|
1588
|
-
|
|
1885
|
+
// 渲染所有页面的文本图层
|
|
1886
|
+
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
1887
|
+
renderTextLayer(pageNum);
|
|
1888
|
+
}
|
|
1589
1889
|
});
|
|
1590
1890
|
},
|
|
1591
|
-
{ deep: true, immediate:
|
|
1891
|
+
{ deep: true, immediate: true }
|
|
1592
1892
|
);
|
|
1593
1893
|
|
|
1594
1894
|
/**
|
|
1595
|
-
* 监听 annotations
|
|
1895
|
+
* 监听 annotations 变化,增量更新所有页面的文本图层以显示批注标记
|
|
1596
1896
|
*/
|
|
1597
1897
|
watch(
|
|
1598
1898
|
() => props.annotations,
|
|
1599
1899
|
() => {
|
|
1600
1900
|
nextTick(() => {
|
|
1601
|
-
|
|
1901
|
+
// 更新所有页面的文本图层
|
|
1902
|
+
for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) {
|
|
1903
|
+
renderTextLayer(pageNum);
|
|
1904
|
+
}
|
|
1602
1905
|
});
|
|
1603
1906
|
},
|
|
1604
1907
|
{ deep: true }
|
|
1605
1908
|
);
|
|
1606
1909
|
|
|
1607
1910
|
/**
|
|
1608
|
-
*
|
|
1911
|
+
* 监听当前页码变化,确保当前页的文本图层已渲染
|
|
1609
1912
|
*/
|
|
1610
1913
|
watch(
|
|
1611
1914
|
() => currentPage.value,
|
|
1612
1915
|
() => {
|
|
1613
1916
|
nextTick(() => {
|
|
1614
|
-
renderTextLayer();
|
|
1917
|
+
renderTextLayer(currentPage.value);
|
|
1615
1918
|
});
|
|
1616
1919
|
}
|
|
1617
1920
|
);
|
|
1618
1921
|
|
|
1619
1922
|
/**
|
|
1620
|
-
*
|
|
1621
|
-
*/
|
|
1622
|
-
watch(
|
|
1623
|
-
() => currentPageBlocksData.value,
|
|
1624
|
-
() => {
|
|
1625
|
-
nextTick(() => {
|
|
1626
|
-
renderTextLayer();
|
|
1627
|
-
});
|
|
1628
|
-
},
|
|
1629
|
-
{ deep: true }
|
|
1630
|
-
);
|
|
1631
|
-
|
|
1632
|
-
/**
|
|
1633
|
-
* 监听图片尺寸变化,重新渲染文本图层
|
|
1923
|
+
* 监听图片尺寸变化,重新渲染当前页的文本图层
|
|
1634
1924
|
*/
|
|
1635
1925
|
watch(
|
|
1636
1926
|
() => imageSize.value,
|
|
1637
1927
|
() => {
|
|
1638
1928
|
if (imageSize.value.width > 0 && imageSize.value.height > 0) {
|
|
1639
1929
|
nextTick(() => {
|
|
1640
|
-
renderTextLayer();
|
|
1930
|
+
renderTextLayer(currentPage.value);
|
|
1641
1931
|
});
|
|
1642
1932
|
}
|
|
1643
1933
|
},
|
|
@@ -1647,27 +1937,36 @@ watch(
|
|
|
1647
1937
|
/**
|
|
1648
1938
|
* 监听缩放、旋转、位置变化,更新文本图层位置
|
|
1649
1939
|
*/
|
|
1650
|
-
watch(
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1940
|
+
watch(
|
|
1941
|
+
[scale, rotation, position],
|
|
1942
|
+
() => {
|
|
1943
|
+
// 文本图层会跟随图片的 transform,不需要手动更新位置
|
|
1944
|
+
// 但需要确保文本图层与图片同步变换
|
|
1945
|
+
},
|
|
1946
|
+
{ deep: true }
|
|
1947
|
+
);
|
|
1654
1948
|
|
|
1655
1949
|
/**
|
|
1656
1950
|
* 组件挂载时的初始化
|
|
1657
1951
|
*/
|
|
1658
1952
|
onMounted(() => {
|
|
1659
|
-
//
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1953
|
+
// 如果第一页图片已经加载完成,立即计算自适应宽度
|
|
1954
|
+
const firstPageImage = imageRefs.get(1);
|
|
1955
|
+
if (firstPageImage && firstPageImage.complete && props.autoFitWidth) {
|
|
1956
|
+
const firstPageSize = imageSizes.get(1);
|
|
1957
|
+
if (firstPageSize && firstPageSize.width > 0) {
|
|
1958
|
+
nextTick(() => {
|
|
1959
|
+
nextTick(() => {
|
|
1960
|
+
setTimeout(() => {
|
|
1961
|
+
const autoScale = calculateAutoFitScale();
|
|
1962
|
+
if (autoScale !== 1 && autoScale > 0) {
|
|
1963
|
+
scale.value = autoScale;
|
|
1964
|
+
initialAutoFitScale.value = autoScale;
|
|
1965
|
+
}
|
|
1966
|
+
}, 100);
|
|
1967
|
+
});
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1671
1970
|
}
|
|
1672
1971
|
});
|
|
1673
1972
|
|
|
@@ -1690,12 +1989,15 @@ defineExpose({
|
|
|
1690
1989
|
jumpToPosition, // 暴露定位方法给父组件
|
|
1691
1990
|
goToPage: switchToPage, // 暴露翻页方法给父组件
|
|
1692
1991
|
getCurrentPage: () => currentPage.value, // 获取当前页码
|
|
1693
|
-
getTotalPages: () => totalPages.value // 获取总页数
|
|
1992
|
+
getTotalPages: () => totalPages.value, // 获取总页数
|
|
1694
1993
|
});
|
|
1695
1994
|
</script>
|
|
1696
1995
|
|
|
1697
1996
|
<style lang="less" scoped>
|
|
1698
1997
|
// 样式已统一到公共样式文件,这里只保留组件特定样式
|
|
1998
|
+
.toolbar-group {
|
|
1999
|
+
gap: 0 !important;
|
|
2000
|
+
}
|
|
1699
2001
|
|
|
1700
2002
|
// 图片容器
|
|
1701
2003
|
.image-container {
|
|
@@ -1719,8 +2021,8 @@ defineExpose({
|
|
|
1719
2021
|
// 图片包装器容器(用于居中显示和提供滚动空间)
|
|
1720
2022
|
.image-wrapper-container {
|
|
1721
2023
|
display: flex;
|
|
1722
|
-
|
|
1723
|
-
|
|
2024
|
+
flex-direction: column; // 垂直排列所有页面
|
|
2025
|
+
align-items: center; // 水平居中
|
|
1724
2026
|
width: 100%;
|
|
1725
2027
|
min-height: 100%;
|
|
1726
2028
|
box-sizing: border-box;
|
|
@@ -1734,6 +2036,17 @@ defineExpose({
|
|
|
1734
2036
|
padding: 0;
|
|
1735
2037
|
}
|
|
1736
2038
|
|
|
2039
|
+
// 每个页面的容器
|
|
2040
|
+
.image-page-container {
|
|
2041
|
+
position: relative;
|
|
2042
|
+
display: flex;
|
|
2043
|
+
justify-content: center;
|
|
2044
|
+
width: 100%;
|
|
2045
|
+
flex-shrink: 0;
|
|
2046
|
+
margin: 0;
|
|
2047
|
+
padding: 0;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1737
2050
|
// 图片包装器(包含图片和文本图层)
|
|
1738
2051
|
.image-wrapper {
|
|
1739
2052
|
position: relative;
|
|
@@ -1747,7 +2060,7 @@ defineExpose({
|
|
|
1747
2060
|
margin: 0;
|
|
1748
2061
|
padding: 0;
|
|
1749
2062
|
line-height: 0;
|
|
1750
|
-
|
|
2063
|
+
|
|
1751
2064
|
img {
|
|
1752
2065
|
display: block; // 移除图片底部默认间隙
|
|
1753
2066
|
margin: 0;
|
|
@@ -1808,7 +2121,7 @@ defineExpose({
|
|
|
1808
2121
|
border-radius: 6px;
|
|
1809
2122
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
1810
2123
|
animation: fade-in 0.2s ease;
|
|
1811
|
-
|
|
2124
|
+
|
|
1812
2125
|
.annotation-button-action {
|
|
1813
2126
|
display: flex;
|
|
1814
2127
|
flex-direction: row;
|
|
@@ -1838,20 +2151,20 @@ defineExpose({
|
|
|
1838
2151
|
box-shadow: 0 4px 16px rgb(0 0 0 / 20%);
|
|
1839
2152
|
animation: fade-in 0.2s ease;
|
|
1840
2153
|
overflow: hidden;
|
|
1841
|
-
|
|
2154
|
+
|
|
1842
2155
|
.annotation-input-header {
|
|
1843
2156
|
display: flex;
|
|
1844
2157
|
align-items: center;
|
|
1845
2158
|
justify-content: space-between;
|
|
1846
2159
|
padding: 12px 16px;
|
|
1847
2160
|
border-bottom: 1px solid #e5e7eb;
|
|
1848
|
-
|
|
2161
|
+
|
|
1849
2162
|
.annotation-input-title {
|
|
1850
2163
|
font-size: 14px;
|
|
1851
2164
|
font-weight: 500;
|
|
1852
2165
|
color: #1d2129;
|
|
1853
2166
|
}
|
|
1854
|
-
|
|
2167
|
+
|
|
1855
2168
|
.annotation-close-btn {
|
|
1856
2169
|
display: flex;
|
|
1857
2170
|
align-items: center;
|
|
@@ -1865,22 +2178,22 @@ defineExpose({
|
|
|
1865
2178
|
border-radius: 4px;
|
|
1866
2179
|
color: #86909c;
|
|
1867
2180
|
transition: all 0.2s;
|
|
1868
|
-
|
|
2181
|
+
|
|
1869
2182
|
&:hover {
|
|
1870
2183
|
background-color: #f0f0f0;
|
|
1871
2184
|
color: #1d2129;
|
|
1872
2185
|
}
|
|
1873
2186
|
}
|
|
1874
2187
|
}
|
|
1875
|
-
|
|
2188
|
+
|
|
1876
2189
|
.annotation-input-content {
|
|
1877
2190
|
padding: 12px 16px;
|
|
1878
|
-
|
|
2191
|
+
|
|
1879
2192
|
.annotation-textarea {
|
|
1880
2193
|
width: 100%;
|
|
1881
2194
|
}
|
|
1882
2195
|
}
|
|
1883
|
-
|
|
2196
|
+
|
|
1884
2197
|
.annotation-input-footer {
|
|
1885
2198
|
display: flex;
|
|
1886
2199
|
align-items: center;
|
|
@@ -1903,17 +2216,17 @@ defineExpose({
|
|
|
1903
2216
|
padding: 1px 3px;
|
|
1904
2217
|
// 添加阴影效果
|
|
1905
2218
|
box-shadow: 0 1px 2px rgba(255, 193, 7, 0.25) !important;
|
|
1906
|
-
|
|
2219
|
+
|
|
1907
2220
|
// 鼠标悬停时加深效果
|
|
1908
2221
|
&:hover {
|
|
1909
2222
|
background-color: rgba(255, 243, 205, 0.7) !important;
|
|
1910
2223
|
border-color: rgba(255, 193, 7, 0.9) !important;
|
|
1911
2224
|
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.35) !important;
|
|
1912
2225
|
}
|
|
1913
|
-
|
|
2226
|
+
|
|
1914
2227
|
// 使用伪元素添加批注标记图标
|
|
1915
2228
|
&::before {
|
|
1916
|
-
content:
|
|
2229
|
+
content: "📝";
|
|
1917
2230
|
position: absolute;
|
|
1918
2231
|
top: -6px;
|
|
1919
2232
|
right: -6px;
|