@koi-br/ocr-web-sdk 1.0.26 → 1.0.28
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-CqSyQjJ2.mjs → index-BUW0_Hv2.mjs} +8891 -8805
- package/dist/{index-BHtQ-val.js → index-DxvQQd3Y.js} +81 -81
- package/dist/index.cjs.js +2 -2
- package/dist/index.esm.js +2 -2
- package/dist/{tiff.min-D0RfnjXu.js → tiff.min-BDrWBfNI.js} +1 -1
- package/dist/{tiff.min-q16GulNm.mjs → tiff.min-DFX5shSB.mjs} +1 -1
- package/package.json +1 -1
- package/preview/ImagePreview.vue +644 -428
- package/preview/index.vue +19 -5
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,12 +388,42 @@ 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
|
// 计算图片放大后的实际尺寸(考虑旋转)
|
|
368
429
|
const scaledImageSize = computed(() => {
|
|
@@ -380,7 +441,7 @@ const scaledImageSize = computed(() => {
|
|
|
380
441
|
|
|
381
442
|
return {
|
|
382
443
|
width: isRotated ? scaledHeight : scaledWidth,
|
|
383
|
-
height: isRotated ? scaledWidth : scaledHeight
|
|
444
|
+
height: isRotated ? scaledWidth : scaledHeight,
|
|
384
445
|
};
|
|
385
446
|
});
|
|
386
447
|
|
|
@@ -392,23 +453,26 @@ const containerStyle = computed(() => {
|
|
|
392
453
|
// 让容器自适应内容,避免横向滚动条
|
|
393
454
|
if (props.autoFitWidth) {
|
|
394
455
|
return {
|
|
395
|
-
minWidth:
|
|
396
|
-
minHeight: height > 0 ? `${height}px` :
|
|
456
|
+
minWidth: "100%", // 使用 100% 让容器自适应父容器宽度
|
|
457
|
+
minHeight: height > 0 ? `${height}px` : "100%",
|
|
397
458
|
};
|
|
398
459
|
}
|
|
399
460
|
|
|
400
461
|
// 未启用自适应宽度时,使用原始逻辑
|
|
401
462
|
return {
|
|
402
|
-
minWidth: width > 0 ? `${width}px` :
|
|
403
|
-
minHeight: height > 0 ? `${height}px` :
|
|
463
|
+
minWidth: width > 0 ? `${width}px` : "100%",
|
|
464
|
+
minHeight: height > 0 ? `${height}px` : "100%",
|
|
404
465
|
};
|
|
405
466
|
});
|
|
406
467
|
|
|
407
468
|
// 批注相关状态
|
|
408
469
|
const showAnnotationPopup = ref(false); // 是否显示批注弹窗
|
|
409
470
|
const annotationPopupStyle = ref<any>({}); // 批注弹窗样式
|
|
410
|
-
const annotationInput = ref(
|
|
411
|
-
const currentAnnotationBlock = ref<{
|
|
471
|
+
const annotationInput = ref(""); // 批注输入内容
|
|
472
|
+
const currentAnnotationBlock = ref<{
|
|
473
|
+
bbox: [number, number, number, number];
|
|
474
|
+
content: string;
|
|
475
|
+
} | null>(null); // 当前正在添加批注的文本块
|
|
412
476
|
const annotationPopupRef = ref<HTMLElement>(); // 批注弹窗引用
|
|
413
477
|
|
|
414
478
|
// 文本选择相关状态(保留用于兼容)
|
|
@@ -421,14 +485,22 @@ const currentPageBlocksData = computed(() => {
|
|
|
421
485
|
return [];
|
|
422
486
|
}
|
|
423
487
|
// 根据当前页码过滤
|
|
424
|
-
return props.blocksData.filter(block => block.pageNo === currentPage.value);
|
|
488
|
+
return props.blocksData.filter((block) => block.pageNo === currentPage.value);
|
|
425
489
|
});
|
|
426
490
|
|
|
491
|
+
// 获取指定页码的blocksData
|
|
492
|
+
const getPageBlocksData = (pageNo: number) => {
|
|
493
|
+
if (!props.blocksData || props.blocksData.length === 0) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
return props.blocksData.filter((block) => block.pageNo === pageNo);
|
|
497
|
+
};
|
|
498
|
+
|
|
427
499
|
// 隐藏定时器
|
|
428
500
|
let hideTimer: any = null;
|
|
429
501
|
|
|
430
|
-
const rotateImage = direction => {
|
|
431
|
-
if (direction ===
|
|
502
|
+
const rotateImage = (direction) => {
|
|
503
|
+
if (direction === "left") {
|
|
432
504
|
rotation.value = (rotation.value - 90) % 360;
|
|
433
505
|
} else {
|
|
434
506
|
rotation.value = (rotation.value + 90) % 360;
|
|
@@ -438,7 +510,7 @@ const rotateImage = direction => {
|
|
|
438
510
|
const zoom = (delta, isWheel = false) => {
|
|
439
511
|
// 用户主动缩放时,标记为手动缩放
|
|
440
512
|
isUserZooming.value = true;
|
|
441
|
-
|
|
513
|
+
|
|
442
514
|
const step = isWheel ? props.wheelStep : props.clickStep; // 滚轮缩放使用更小的步长
|
|
443
515
|
const newScale = scale.value + delta * step;
|
|
444
516
|
if (newScale <= props.minScale) {
|
|
@@ -464,7 +536,7 @@ const handleWheel = (e: WheelEvent) => {
|
|
|
464
536
|
const startPan = (e: MouseEvent) => {
|
|
465
537
|
// 只在左键点击时启用拖拽
|
|
466
538
|
if (e.button !== 0) return;
|
|
467
|
-
|
|
539
|
+
|
|
468
540
|
// 如果有文本图层数据,默认允许文本选择,不拖拽
|
|
469
541
|
// 只有在按住 Ctrl 或 Meta 键时才允许拖拽
|
|
470
542
|
if (currentPageBlocksData.value && currentPageBlocksData.value.length > 0) {
|
|
@@ -478,7 +550,7 @@ const startPan = (e: MouseEvent) => {
|
|
|
478
550
|
return;
|
|
479
551
|
}
|
|
480
552
|
}
|
|
481
|
-
|
|
553
|
+
|
|
482
554
|
isPanning.value = true;
|
|
483
555
|
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
484
556
|
e.preventDefault(); // 阻止默认行为,避免与滚动冲突
|
|
@@ -492,7 +564,7 @@ const pan = (e: MouseEvent) => {
|
|
|
492
564
|
|
|
493
565
|
position.value = {
|
|
494
566
|
x: position.value.x + deltaX,
|
|
495
|
-
y: position.value.y + deltaY
|
|
567
|
+
y: position.value.y + deltaY,
|
|
496
568
|
};
|
|
497
569
|
|
|
498
570
|
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
@@ -501,23 +573,35 @@ const pan = (e: MouseEvent) => {
|
|
|
501
573
|
|
|
502
574
|
const stopPan = () => {
|
|
503
575
|
isPanning.value = false;
|
|
504
|
-
|
|
576
|
+
|
|
505
577
|
// 在自适应宽度模式下,如果用户没有主动缩放,确保缩放比例保持初始值
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
578
|
+
if (
|
|
579
|
+
props.autoFitWidth &&
|
|
580
|
+
!isUserZooming.value &&
|
|
581
|
+
initialAutoFitScale.value !== null &&
|
|
582
|
+
Math.abs(scale.value - initialAutoFitScale.value) > 0.01
|
|
583
|
+
) {
|
|
510
584
|
// 拖拽操作不应该改变缩放比例,如果被改变了,恢复它
|
|
511
585
|
scale.value = initialAutoFitScale.value;
|
|
512
586
|
}
|
|
587
|
+
};
|
|
513
588
|
|
|
589
|
+
// 处理图片鼠标按下事件(允许文本选择)
|
|
590
|
+
const handleImageMouseDown = (e: MouseEvent) => {
|
|
591
|
+
// 如果点击的是文本块,不阻止默认行为,允许文本选择
|
|
592
|
+
const target = e.target as HTMLElement;
|
|
593
|
+
if (target.closest(".text-block")) {
|
|
594
|
+
return; // 允许文本选择
|
|
595
|
+
}
|
|
596
|
+
// 否则阻止默认行为,允许拖拽
|
|
597
|
+
e.preventDefault();
|
|
514
598
|
};
|
|
515
599
|
|
|
516
600
|
// 处理鼠标抬起事件(检测文本选择)
|
|
517
601
|
const handleMouseUp = (e: MouseEvent) => {
|
|
518
602
|
// 先执行停止拖拽的逻辑
|
|
519
603
|
stopPan();
|
|
520
|
-
|
|
604
|
+
|
|
521
605
|
// 延迟检测文本选择,确保选择已完成
|
|
522
606
|
setTimeout(() => {
|
|
523
607
|
checkTextSelection(e);
|
|
@@ -530,19 +614,25 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
530
614
|
if (currentAnnotationBlock.value) {
|
|
531
615
|
return;
|
|
532
616
|
}
|
|
533
|
-
|
|
617
|
+
|
|
534
618
|
const target = e.target as HTMLElement;
|
|
535
619
|
// 如果点击的是批注按钮或批注弹窗,不执行文本选择检测
|
|
536
|
-
if (
|
|
620
|
+
if (
|
|
621
|
+
target.closest(".annotation-button-popup") ||
|
|
622
|
+
target.closest(".annotation-input-popup")
|
|
623
|
+
) {
|
|
537
624
|
return;
|
|
538
625
|
}
|
|
539
|
-
|
|
626
|
+
|
|
540
627
|
const selection = window.getSelection();
|
|
541
|
-
|
|
628
|
+
|
|
542
629
|
// 如果没有选中文本,隐藏批注按钮(但不关闭批注输入弹窗)
|
|
543
630
|
if (!selection || selection.toString().trim().length === 0) {
|
|
544
631
|
// 如果点击的不是文本块,且不在批注弹窗内,隐藏批注按钮
|
|
545
|
-
if (
|
|
632
|
+
if (
|
|
633
|
+
!target.closest(".text-block") &&
|
|
634
|
+
!target.closest(".annotation-input-popup")
|
|
635
|
+
) {
|
|
546
636
|
// 只有在没有打开批注输入弹窗时才隐藏批注按钮
|
|
547
637
|
if (!currentAnnotationBlock.value) {
|
|
548
638
|
showAnnotationPopup.value = false;
|
|
@@ -551,92 +641,100 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
551
641
|
}
|
|
552
642
|
return;
|
|
553
643
|
}
|
|
554
|
-
|
|
644
|
+
|
|
555
645
|
// 获取选中的文本
|
|
556
646
|
const selectedText = selection.toString().trim();
|
|
557
647
|
if (!selectedText) {
|
|
558
648
|
return;
|
|
559
649
|
}
|
|
560
|
-
|
|
650
|
+
|
|
561
651
|
// 检查选中的文本是否在文本块内
|
|
562
652
|
const range = selection.getRangeAt(0);
|
|
563
653
|
const container = range.commonAncestorContainer;
|
|
564
|
-
|
|
654
|
+
|
|
565
655
|
// 向上查找最近的文本块元素
|
|
566
656
|
let textBlockElement: HTMLElement | null = null;
|
|
567
|
-
let node: Node | null =
|
|
568
|
-
|
|
657
|
+
let node: Node | null =
|
|
658
|
+
container.nodeType === Node.TEXT_NODE
|
|
659
|
+
? container.parentElement
|
|
660
|
+
: (container as HTMLElement);
|
|
661
|
+
|
|
569
662
|
while (node && node !== document.body) {
|
|
570
|
-
if (node instanceof HTMLElement && node.classList.contains(
|
|
663
|
+
if (node instanceof HTMLElement && node.classList.contains("text-block")) {
|
|
571
664
|
textBlockElement = node;
|
|
572
665
|
break;
|
|
573
666
|
}
|
|
574
667
|
node = node.parentElement;
|
|
575
668
|
}
|
|
576
|
-
|
|
669
|
+
|
|
577
670
|
if (!textBlockElement) {
|
|
578
671
|
return;
|
|
579
672
|
}
|
|
580
|
-
|
|
673
|
+
|
|
581
674
|
// 设置当前激活的文本块
|
|
582
675
|
activeBlockDiv.value = textBlockElement;
|
|
583
|
-
|
|
676
|
+
|
|
584
677
|
// 获取选中文本的位置
|
|
585
678
|
const rect = range.getBoundingClientRect();
|
|
586
679
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
587
|
-
|
|
680
|
+
|
|
588
681
|
if (!containerRect || !containerRef.value) return;
|
|
589
|
-
|
|
682
|
+
|
|
590
683
|
// 计算批注按钮的位置
|
|
591
684
|
const buttonWidth = 80;
|
|
592
685
|
const popupWidth = buttonWidth + 8;
|
|
593
686
|
const popupHeight = 38;
|
|
594
687
|
const spacing = 4; // 减少间距,让按钮更靠近文字
|
|
595
|
-
|
|
688
|
+
|
|
596
689
|
// 计算相对于容器的坐标(考虑滚动)
|
|
597
|
-
const relativeX =
|
|
690
|
+
const relativeX =
|
|
691
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
598
692
|
const relativeY = rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
599
|
-
|
|
693
|
+
|
|
600
694
|
// 容器可用空间
|
|
601
695
|
const containerWidth = containerRef.value.clientWidth;
|
|
602
|
-
|
|
696
|
+
|
|
603
697
|
// 计算可用空间
|
|
604
698
|
const rectBottom = rect.bottom;
|
|
605
699
|
const rectTop = rect.top;
|
|
606
700
|
const containerBottom = containerRect.bottom;
|
|
607
701
|
const containerTop = containerRect.top;
|
|
608
|
-
|
|
702
|
+
|
|
609
703
|
const bottomSpace = containerBottom - rectBottom;
|
|
610
704
|
const topSpace = rectTop - containerTop;
|
|
611
|
-
|
|
612
|
-
const showOnBottom =
|
|
613
|
-
|
|
705
|
+
|
|
706
|
+
const showOnBottom =
|
|
707
|
+
bottomSpace >= popupHeight + spacing ||
|
|
708
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
709
|
+
|
|
614
710
|
// 计算最终位置
|
|
615
711
|
let left: number;
|
|
616
712
|
let top: number;
|
|
617
|
-
|
|
713
|
+
|
|
618
714
|
const centerX = relativeX + rect.width / 2;
|
|
619
715
|
left = centerX - popupWidth / 2;
|
|
620
|
-
|
|
716
|
+
|
|
621
717
|
if (showOnBottom) {
|
|
622
718
|
top = relativeY + rect.height + spacing;
|
|
623
719
|
} else {
|
|
624
720
|
top = relativeY - popupHeight - spacing;
|
|
625
721
|
}
|
|
626
|
-
|
|
722
|
+
|
|
627
723
|
// 边界检查
|
|
628
|
-
const popupTopInViewport =
|
|
724
|
+
const popupTopInViewport =
|
|
725
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
629
726
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
630
|
-
|
|
727
|
+
|
|
631
728
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
632
729
|
top = relativeY - popupHeight - spacing;
|
|
633
730
|
}
|
|
634
|
-
|
|
635
|
-
const newPopupTopInViewport =
|
|
731
|
+
|
|
732
|
+
const newPopupTopInViewport =
|
|
733
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
636
734
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
637
735
|
top = relativeY + rect.height + spacing;
|
|
638
736
|
}
|
|
639
|
-
|
|
737
|
+
|
|
640
738
|
// 确保不超出容器边界
|
|
641
739
|
if (left + popupWidth > containerWidth - spacing) {
|
|
642
740
|
left = containerWidth - popupWidth - spacing;
|
|
@@ -644,12 +742,12 @@ const checkTextSelection = (e: MouseEvent) => {
|
|
|
644
742
|
if (left < spacing) {
|
|
645
743
|
left = spacing;
|
|
646
744
|
}
|
|
647
|
-
|
|
745
|
+
|
|
648
746
|
annotationPopupStyle.value = {
|
|
649
747
|
left: `${left}px`,
|
|
650
|
-
top: `${top}px
|
|
748
|
+
top: `${top}px`,
|
|
651
749
|
};
|
|
652
|
-
|
|
750
|
+
|
|
653
751
|
// 显示批注按钮
|
|
654
752
|
showAnnotationPopup.value = true;
|
|
655
753
|
};
|
|
@@ -671,53 +769,38 @@ const switchToPage = (page: number) => {
|
|
|
671
769
|
if (page < 1 || page > totalPages.value) {
|
|
672
770
|
return;
|
|
673
771
|
}
|
|
674
|
-
|
|
675
|
-
// 切换页码前重置状态(但不重置缩放,等图片加载后再设置)
|
|
676
|
-
rotation.value = 0;
|
|
677
|
-
position.value = { x: 0, y: 0 };
|
|
678
|
-
isUserZooming.value = false;
|
|
679
|
-
|
|
772
|
+
|
|
680
773
|
// 重置文本选择状态和批注状态
|
|
681
774
|
showAnnotationPopup.value = false;
|
|
682
775
|
currentAnnotationBlock.value = null;
|
|
683
|
-
annotationInput.value =
|
|
776
|
+
annotationInput.value = "";
|
|
684
777
|
activeBlockDiv.value = null;
|
|
685
778
|
isHighlighted.value = false;
|
|
686
|
-
|
|
687
|
-
//
|
|
779
|
+
|
|
780
|
+
// 更新页码
|
|
688
781
|
currentPage.value = page;
|
|
689
|
-
|
|
782
|
+
|
|
690
783
|
// 触发页码变化事件
|
|
691
|
-
emit(
|
|
692
|
-
|
|
693
|
-
//
|
|
784
|
+
emit("page-change", page, totalPages.value);
|
|
785
|
+
|
|
786
|
+
// 滚动到对应页面
|
|
694
787
|
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();
|
|
788
|
+
const pageElement = containerRef.value.querySelector(
|
|
789
|
+
`[data-page-number="${page}"]`
|
|
790
|
+
) as HTMLElement;
|
|
791
|
+
if (pageElement) {
|
|
792
|
+
pageElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
793
|
+
// 更新 lastScrollTop,确保滚动方向判断准确
|
|
794
|
+
nextTick(() => {
|
|
795
|
+
lastScrollTop = containerRef.value?.scrollTop || 0;
|
|
796
|
+
});
|
|
714
797
|
}
|
|
715
|
-
}
|
|
798
|
+
}
|
|
716
799
|
};
|
|
717
800
|
|
|
718
801
|
const reset = () => {
|
|
719
802
|
rotation.value = 0;
|
|
720
|
-
|
|
803
|
+
|
|
721
804
|
// 如果启用自适应宽度且有初始缩放比例,恢复到初始缩放比例
|
|
722
805
|
if (props.autoFitWidth && initialAutoFitScale.value !== null) {
|
|
723
806
|
scale.value = initialAutoFitScale.value;
|
|
@@ -725,24 +808,28 @@ const reset = () => {
|
|
|
725
808
|
} else {
|
|
726
809
|
scale.value = 1;
|
|
727
810
|
}
|
|
728
|
-
|
|
811
|
+
|
|
729
812
|
position.value = { x: 0, y: 0 };
|
|
730
|
-
|
|
813
|
+
|
|
731
814
|
// 重置文本选择状态和批注状态
|
|
732
815
|
showAnnotationPopup.value = false;
|
|
733
816
|
currentAnnotationBlock.value = null;
|
|
734
|
-
annotationInput.value =
|
|
817
|
+
annotationInput.value = "";
|
|
735
818
|
activeBlockDiv.value = null;
|
|
736
819
|
isHighlighted.value = false;
|
|
737
820
|
};
|
|
738
821
|
|
|
739
822
|
const original = () => {
|
|
740
|
-
emit(
|
|
823
|
+
emit("original");
|
|
741
824
|
};
|
|
742
825
|
|
|
743
826
|
// 计算自适应宽度的缩放比例
|
|
744
827
|
const calculateAutoFitScale = () => {
|
|
745
|
-
if (
|
|
828
|
+
if (
|
|
829
|
+
!props.autoFitWidth ||
|
|
830
|
+
!containerRef.value ||
|
|
831
|
+
imageSize.value.width === 0
|
|
832
|
+
) {
|
|
746
833
|
return 1;
|
|
747
834
|
}
|
|
748
835
|
|
|
@@ -750,7 +837,7 @@ const calculateAutoFitScale = () => {
|
|
|
750
837
|
// 使用容器的完整宽度,但预留足够空间避免因浏览器渲染误差导致横向滚动条
|
|
751
838
|
// 预留 4px 以确保不会因为任何渲染误差导致滚动条
|
|
752
839
|
const containerWidth = containerRect.width - 4;
|
|
753
|
-
|
|
840
|
+
|
|
754
841
|
if (containerWidth <= 0) {
|
|
755
842
|
return 1;
|
|
756
843
|
}
|
|
@@ -758,33 +845,35 @@ const calculateAutoFitScale = () => {
|
|
|
758
845
|
// 考虑旋转角度,如果旋转了90度或270度,需要交换宽高
|
|
759
846
|
const normalizedRotation = ((rotation.value % 360) + 360) % 360;
|
|
760
847
|
const isRotated = normalizedRotation === 90 || normalizedRotation === 270;
|
|
761
|
-
|
|
848
|
+
|
|
762
849
|
const imageWidth = isRotated ? imageSize.value.height : imageSize.value.width;
|
|
763
|
-
|
|
850
|
+
|
|
764
851
|
if (imageWidth <= 0) {
|
|
765
852
|
return 1;
|
|
766
853
|
}
|
|
767
|
-
|
|
854
|
+
|
|
768
855
|
// 计算缩放比例,使图片宽度完全适应容器宽度
|
|
769
856
|
const calculatedScale = containerWidth / imageWidth;
|
|
770
|
-
|
|
857
|
+
|
|
771
858
|
// 确保缩放比例在允许的范围内
|
|
772
859
|
return Math.max(props.minScale, Math.min(props.maxScale, calculatedScale));
|
|
773
860
|
};
|
|
774
861
|
|
|
775
862
|
// 图片加载完成处理
|
|
776
|
-
const onImageLoad = (event: Event) => {
|
|
863
|
+
const onImageLoad = (event: Event, pageNum: number) => {
|
|
777
864
|
const img = event.target as HTMLImageElement;
|
|
778
|
-
imageSize.value = {
|
|
779
|
-
width: img.naturalWidth,
|
|
780
|
-
height: img.naturalHeight
|
|
781
|
-
};
|
|
782
865
|
|
|
783
|
-
//
|
|
784
|
-
|
|
866
|
+
// 存储该页的图片尺寸
|
|
867
|
+
imageSizes.set(pageNum, {
|
|
868
|
+
width: img.naturalWidth,
|
|
869
|
+
height: img.naturalHeight,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// 如果是第一页且启用自适应宽度,计算并设置初始缩放比例
|
|
873
|
+
if (pageNum === 1 && props.autoFitWidth) {
|
|
785
874
|
// 重置用户缩放标记
|
|
786
875
|
isUserZooming.value = false;
|
|
787
|
-
|
|
876
|
+
|
|
788
877
|
// 使用双重 nextTick 确保容器尺寸已确定
|
|
789
878
|
nextTick(() => {
|
|
790
879
|
nextTick(() => {
|
|
@@ -799,17 +888,17 @@ const onImageLoad = (event: Event) => {
|
|
|
799
888
|
});
|
|
800
889
|
});
|
|
801
890
|
}
|
|
802
|
-
|
|
803
|
-
emit(
|
|
891
|
+
|
|
892
|
+
emit("load", {
|
|
804
893
|
width: img.naturalWidth,
|
|
805
894
|
height: img.naturalHeight,
|
|
806
|
-
page:
|
|
807
|
-
totalPages: totalPages.value
|
|
895
|
+
page: pageNum,
|
|
896
|
+
totalPages: totalPages.value,
|
|
808
897
|
});
|
|
809
|
-
|
|
810
|
-
//
|
|
898
|
+
|
|
899
|
+
// 图片加载完成后,渲染该页的文本图层
|
|
811
900
|
nextTick(() => {
|
|
812
|
-
renderTextLayer();
|
|
901
|
+
renderTextLayer(pageNum);
|
|
813
902
|
});
|
|
814
903
|
};
|
|
815
904
|
|
|
@@ -825,7 +914,7 @@ const onImageLoad = (event: Event) => {
|
|
|
825
914
|
const calculateFontSize = (
|
|
826
915
|
text: string,
|
|
827
916
|
targetWidth: number,
|
|
828
|
-
targetHeight: number
|
|
917
|
+
targetHeight: number
|
|
829
918
|
): number => {
|
|
830
919
|
// 创建一个离屏 Canvas 用于测量文本
|
|
831
920
|
const canvas = document.createElement("canvas");
|
|
@@ -872,19 +961,21 @@ const calculateFontSize = (
|
|
|
872
961
|
/**
|
|
873
962
|
* 渲染文本图层(使用 blocksData 数据)
|
|
874
963
|
*/
|
|
875
|
-
const renderTextLayer = () => {
|
|
876
|
-
const
|
|
877
|
-
const
|
|
964
|
+
const renderTextLayer = (pageNum?: number) => {
|
|
965
|
+
const targetPage = pageNum || currentPage.value;
|
|
966
|
+
const textLayer = textLayerRefs.get(targetPage);
|
|
967
|
+
const image = imageRefs.get(targetPage);
|
|
878
968
|
|
|
879
969
|
if (!textLayer || !image) {
|
|
880
970
|
return;
|
|
881
971
|
}
|
|
882
972
|
|
|
883
|
-
|
|
973
|
+
const pageBlocksData = getPageBlocksData(targetPage);
|
|
974
|
+
console.log("renderTextLayer", targetPage, pageBlocksData);
|
|
884
975
|
|
|
885
976
|
// 如果没有提供分块数据,跳过渲染
|
|
886
|
-
if (!
|
|
887
|
-
textLayer.innerHTML =
|
|
977
|
+
if (!pageBlocksData || pageBlocksData.length === 0) {
|
|
978
|
+
textLayer.innerHTML = "";
|
|
888
979
|
return;
|
|
889
980
|
}
|
|
890
981
|
|
|
@@ -894,10 +985,10 @@ const renderTextLayer = () => {
|
|
|
894
985
|
textLayer.style.height = `${image.naturalHeight}px`;
|
|
895
986
|
|
|
896
987
|
// 清空文本图层
|
|
897
|
-
textLayer.innerHTML =
|
|
988
|
+
textLayer.innerHTML = "";
|
|
898
989
|
|
|
899
|
-
//
|
|
900
|
-
|
|
990
|
+
// 使用指定页码的 blocksData 创建可交互的块
|
|
991
|
+
pageBlocksData.forEach((block, index) => {
|
|
901
992
|
const { content, bbox } = block;
|
|
902
993
|
|
|
903
994
|
// bbox 格式: [x1, y1, x2, y2]
|
|
@@ -909,40 +1000,40 @@ const renderTextLayer = () => {
|
|
|
909
1000
|
const calculatedFontSize = calculateFontSize(content, width, height);
|
|
910
1001
|
|
|
911
1002
|
// 创建文本块
|
|
912
|
-
const blockDiv = document.createElement(
|
|
913
|
-
blockDiv.className =
|
|
1003
|
+
const blockDiv = document.createElement("div");
|
|
1004
|
+
blockDiv.className = "text-block";
|
|
914
1005
|
blockDiv.dataset.text = content;
|
|
915
1006
|
blockDiv.dataset.bbox = JSON.stringify(bbox);
|
|
916
1007
|
|
|
917
1008
|
// 设置基础样式
|
|
918
|
-
blockDiv.style.position =
|
|
1009
|
+
blockDiv.style.position = "absolute";
|
|
919
1010
|
blockDiv.style.left = `${x1}px`;
|
|
920
1011
|
blockDiv.style.top = `${y1}px`;
|
|
921
1012
|
blockDiv.style.width = `${width}px`;
|
|
922
1013
|
blockDiv.style.height = `${height}px`;
|
|
923
|
-
blockDiv.style.zIndex =
|
|
924
|
-
blockDiv.style.cursor =
|
|
1014
|
+
blockDiv.style.zIndex = "20"; // 确保文本块在图片上方
|
|
1015
|
+
blockDiv.style.cursor = "text"; // 改为文本选择光标
|
|
925
1016
|
// 设置文本内容(使用 textContent 而不是 innerHTML,避免 XSS 风险)
|
|
926
1017
|
blockDiv.textContent = content;
|
|
927
1018
|
// 设置文本样式,确保可以选择和显示
|
|
928
|
-
blockDiv.style.color =
|
|
1019
|
+
blockDiv.style.color = "red"; // 红色文字
|
|
929
1020
|
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 =
|
|
1021
|
+
blockDiv.style.fontFamily = "Arial, sans-serif"; // 设置明确的字体
|
|
1022
|
+
blockDiv.style.lineHeight = "1.2"; // 设置合适的行高
|
|
1023
|
+
blockDiv.style.whiteSpace = "pre-wrap"; // 保留换行和空格
|
|
1024
|
+
blockDiv.style.overflow = "visible"; // 确保文字不被裁剪
|
|
1025
|
+
blockDiv.style.display = "block"; // 确保是块级元素
|
|
1026
|
+
blockDiv.style.visibility = "visible"; // 确保可见
|
|
936
1027
|
// 允许文本选择
|
|
937
|
-
blockDiv.style.userSelect =
|
|
938
|
-
blockDiv.style.webkitUserSelect =
|
|
939
|
-
blockDiv.style.mozUserSelect =
|
|
940
|
-
blockDiv.style.msUserSelect =
|
|
1028
|
+
blockDiv.style.userSelect = "text";
|
|
1029
|
+
blockDiv.style.webkitUserSelect = "text";
|
|
1030
|
+
blockDiv.style.mozUserSelect = "text";
|
|
1031
|
+
blockDiv.style.msUserSelect = "text";
|
|
941
1032
|
// 允许指针事件,但不阻止文本选择
|
|
942
|
-
blockDiv.style.pointerEvents =
|
|
1033
|
+
blockDiv.style.pointerEvents = "auto";
|
|
943
1034
|
|
|
944
1035
|
// 右键菜单:显示批注按钮
|
|
945
|
-
blockDiv.addEventListener(
|
|
1036
|
+
blockDiv.addEventListener("contextmenu", (e) => {
|
|
946
1037
|
e.preventDefault();
|
|
947
1038
|
// 设置当前文本块为激活状态
|
|
948
1039
|
activeBlockDiv.value = blockDiv;
|
|
@@ -954,78 +1045,86 @@ const renderTextLayer = () => {
|
|
|
954
1045
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
955
1046
|
if (existingAnnotation) {
|
|
956
1047
|
// 添加批注标记样式类
|
|
957
|
-
blockDiv.classList.add(
|
|
1048
|
+
blockDiv.classList.add("has-annotation");
|
|
958
1049
|
blockDiv.title = `已有批注: ${existingAnnotation.content}`;
|
|
959
1050
|
// 直接设置样式,确保生效(内联样式优先级高于CSS类)
|
|
960
|
-
blockDiv.style.backgroundColor =
|
|
961
|
-
blockDiv.style.border =
|
|
962
|
-
blockDiv.style.borderRadius =
|
|
963
|
-
blockDiv.style.padding =
|
|
964
|
-
blockDiv.style.boxShadow =
|
|
965
|
-
|
|
1051
|
+
blockDiv.style.backgroundColor = "rgba(255, 243, 205, 0.5)";
|
|
1052
|
+
blockDiv.style.border = "1px solid rgba(255, 193, 7, 0.7)";
|
|
1053
|
+
blockDiv.style.borderRadius = "3px";
|
|
1054
|
+
blockDiv.style.padding = "1px 3px";
|
|
1055
|
+
blockDiv.style.boxShadow = "0 1px 2px rgba(255, 193, 7, 0.25)";
|
|
1056
|
+
|
|
966
1057
|
// 创建批注图标标记
|
|
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 =
|
|
1058
|
+
const annotationMarker = document.createElement("span");
|
|
1059
|
+
annotationMarker.className = "annotation-marker";
|
|
1060
|
+
annotationMarker.textContent = "📝";
|
|
1061
|
+
annotationMarker.style.position = "absolute";
|
|
1062
|
+
annotationMarker.style.top = "-6px";
|
|
1063
|
+
annotationMarker.style.right = "-6px";
|
|
1064
|
+
annotationMarker.style.fontSize = "11px";
|
|
1065
|
+
annotationMarker.style.backgroundColor = "rgba(255, 193, 7, 0.95)";
|
|
1066
|
+
annotationMarker.style.borderRadius = "50%";
|
|
1067
|
+
annotationMarker.style.width = "16px";
|
|
1068
|
+
annotationMarker.style.height = "16px";
|
|
1069
|
+
annotationMarker.style.display = "flex";
|
|
1070
|
+
annotationMarker.style.alignItems = "center";
|
|
1071
|
+
annotationMarker.style.justifyContent = "center";
|
|
1072
|
+
annotationMarker.style.zIndex = "30";
|
|
1073
|
+
annotationMarker.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.25)";
|
|
1074
|
+
annotationMarker.style.lineHeight = "1";
|
|
984
1075
|
blockDiv.appendChild(annotationMarker);
|
|
985
1076
|
} else {
|
|
986
1077
|
// 没有批注时设置为透明背景
|
|
987
|
-
blockDiv.style.backgroundColor =
|
|
1078
|
+
blockDiv.style.backgroundColor = "transparent";
|
|
988
1079
|
}
|
|
989
1080
|
|
|
990
1081
|
textLayer.appendChild(blockDiv);
|
|
991
1082
|
});
|
|
992
1083
|
} catch (error) {
|
|
993
|
-
console.error(
|
|
1084
|
+
console.error("❌ 文本图层渲染失败:", error);
|
|
994
1085
|
}
|
|
995
1086
|
};
|
|
996
1087
|
|
|
997
1088
|
/**
|
|
998
1089
|
* 获取文本块对应的批注
|
|
999
1090
|
*/
|
|
1000
|
-
const getAnnotationForBlock = (
|
|
1091
|
+
const getAnnotationForBlock = (
|
|
1092
|
+
bbox: [number, number, number, number]
|
|
1093
|
+
): AnnotationInfo | null => {
|
|
1001
1094
|
if (!props.annotations || props.annotations.length === 0) {
|
|
1002
1095
|
return null;
|
|
1003
1096
|
}
|
|
1004
|
-
|
|
1097
|
+
|
|
1005
1098
|
const tolerance = 2; // 容差
|
|
1006
|
-
return
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1099
|
+
return (
|
|
1100
|
+
props.annotations.find((annotation) => {
|
|
1101
|
+
const [x1, y1, x2, y2] = annotation.blockBbox;
|
|
1102
|
+
return (
|
|
1103
|
+
Math.abs(x1 - bbox[0]) < tolerance &&
|
|
1104
|
+
Math.abs(y1 - bbox[1]) < tolerance &&
|
|
1105
|
+
Math.abs(x2 - bbox[2]) < tolerance &&
|
|
1106
|
+
Math.abs(y2 - bbox[3]) < tolerance &&
|
|
1107
|
+
(annotation.blockPage === undefined ||
|
|
1108
|
+
annotation.blockPage === currentPage.value)
|
|
1109
|
+
);
|
|
1110
|
+
}) || null
|
|
1111
|
+
);
|
|
1016
1112
|
};
|
|
1017
1113
|
|
|
1018
1114
|
/**
|
|
1019
1115
|
* 显示文本块的批注按钮
|
|
1020
1116
|
*/
|
|
1021
|
-
const showAnnotationButtonForBlock = (
|
|
1022
|
-
|
|
1023
|
-
|
|
1117
|
+
const showAnnotationButtonForBlock = (
|
|
1118
|
+
event: MouseEvent,
|
|
1119
|
+
blockDiv: HTMLElement
|
|
1120
|
+
) => {
|
|
1121
|
+
const text = blockDiv.dataset.text || "";
|
|
1122
|
+
const bboxStr = blockDiv.dataset.bbox || "";
|
|
1024
1123
|
if (!text || !bboxStr) return;
|
|
1025
1124
|
|
|
1026
1125
|
try {
|
|
1027
1126
|
const bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1028
|
-
|
|
1127
|
+
|
|
1029
1128
|
const rect = blockDiv.getBoundingClientRect();
|
|
1030
1129
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1031
1130
|
|
|
@@ -1038,8 +1137,10 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1038
1137
|
const spacing = 8;
|
|
1039
1138
|
|
|
1040
1139
|
// 计算相对于容器的坐标(考虑滚动)
|
|
1041
|
-
const relativeX =
|
|
1042
|
-
|
|
1140
|
+
const relativeX =
|
|
1141
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1142
|
+
const relativeY =
|
|
1143
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1043
1144
|
|
|
1044
1145
|
// 容器可用空间
|
|
1045
1146
|
const containerWidth = containerRef.value.clientWidth;
|
|
@@ -1053,7 +1154,9 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1053
1154
|
const bottomSpace = containerBottom - rectBottom;
|
|
1054
1155
|
const topSpace = rectTop - containerTop;
|
|
1055
1156
|
|
|
1056
|
-
const showOnBottom =
|
|
1157
|
+
const showOnBottom =
|
|
1158
|
+
bottomSpace >= popupHeight + spacing ||
|
|
1159
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
1057
1160
|
|
|
1058
1161
|
// 计算最终位置
|
|
1059
1162
|
let left: number;
|
|
@@ -1069,14 +1172,16 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1069
1172
|
}
|
|
1070
1173
|
|
|
1071
1174
|
// 边界检查
|
|
1072
|
-
const popupTopInViewport =
|
|
1175
|
+
const popupTopInViewport =
|
|
1176
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1073
1177
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
1074
1178
|
|
|
1075
1179
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
1076
1180
|
top = relativeY - popupHeight - spacing;
|
|
1077
1181
|
}
|
|
1078
1182
|
|
|
1079
|
-
const newPopupTopInViewport =
|
|
1183
|
+
const newPopupTopInViewport =
|
|
1184
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1080
1185
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
1081
1186
|
top = relativeY + rect.height + spacing;
|
|
1082
1187
|
}
|
|
@@ -1091,12 +1196,12 @@ const showAnnotationButtonForBlock = (event: MouseEvent, blockDiv: HTMLElement)
|
|
|
1091
1196
|
|
|
1092
1197
|
annotationPopupStyle.value = {
|
|
1093
1198
|
left: `${left}px`,
|
|
1094
|
-
top: `${top}px
|
|
1199
|
+
top: `${top}px`,
|
|
1095
1200
|
};
|
|
1096
1201
|
|
|
1097
1202
|
showAnnotationPopup.value = true;
|
|
1098
1203
|
} catch (error) {
|
|
1099
|
-
console.error(
|
|
1204
|
+
console.error("解析bbox失败:", error);
|
|
1100
1205
|
}
|
|
1101
1206
|
};
|
|
1102
1207
|
|
|
@@ -1108,13 +1213,13 @@ const hideAnnotationButton = () => {
|
|
|
1108
1213
|
if (currentAnnotationBlock.value) {
|
|
1109
1214
|
return;
|
|
1110
1215
|
}
|
|
1111
|
-
|
|
1216
|
+
|
|
1112
1217
|
hideTimer = setTimeout(() => {
|
|
1113
1218
|
showAnnotationPopup.value = false;
|
|
1114
1219
|
|
|
1115
1220
|
if (activeBlockDiv.value && !isHighlighted.value) {
|
|
1116
|
-
activeBlockDiv.value.style.backgroundColor =
|
|
1117
|
-
activeBlockDiv.value.style.boxShadow =
|
|
1221
|
+
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1222
|
+
activeBlockDiv.value.style.boxShadow = "none";
|
|
1118
1223
|
activeBlockDiv.value = null;
|
|
1119
1224
|
}
|
|
1120
1225
|
}, 300);
|
|
@@ -1138,69 +1243,71 @@ const openAnnotationInput = (e?: Event) => {
|
|
|
1138
1243
|
if (e) {
|
|
1139
1244
|
e.stopPropagation();
|
|
1140
1245
|
}
|
|
1141
|
-
|
|
1246
|
+
|
|
1142
1247
|
// 获取选中的文本
|
|
1143
1248
|
const selection = window.getSelection();
|
|
1144
|
-
let selectedText =
|
|
1145
|
-
|
|
1249
|
+
let selectedText = "";
|
|
1250
|
+
|
|
1146
1251
|
if (selection && selection.toString().trim().length > 0) {
|
|
1147
1252
|
selectedText = selection.toString().trim();
|
|
1148
1253
|
} else if (activeBlockDiv.value) {
|
|
1149
|
-
selectedText = activeBlockDiv.value.dataset.text ||
|
|
1254
|
+
selectedText = activeBlockDiv.value.dataset.text || "";
|
|
1150
1255
|
}
|
|
1151
|
-
|
|
1256
|
+
|
|
1152
1257
|
if (!selectedText && !activeBlockDiv.value) return;
|
|
1153
|
-
|
|
1258
|
+
|
|
1154
1259
|
// 如果有激活的文本块,使用文本块的 bbox
|
|
1155
1260
|
let bbox: [number, number, number, number] | null = null;
|
|
1156
1261
|
if (activeBlockDiv.value) {
|
|
1157
|
-
const bboxStr = activeBlockDiv.value.dataset.bbox ||
|
|
1262
|
+
const bboxStr = activeBlockDiv.value.dataset.bbox || "";
|
|
1158
1263
|
try {
|
|
1159
1264
|
bbox = JSON.parse(bboxStr) as [number, number, number, number];
|
|
1160
1265
|
} catch (error) {
|
|
1161
|
-
console.error(
|
|
1266
|
+
console.error("解析 bbox 失败:", error);
|
|
1162
1267
|
}
|
|
1163
1268
|
}
|
|
1164
|
-
|
|
1269
|
+
|
|
1165
1270
|
// 如果没有 bbox,尝试从选中文本的位置计算
|
|
1166
1271
|
if (!bbox && selection && selection.rangeCount > 0) {
|
|
1167
1272
|
const range = selection.getRangeAt(0);
|
|
1168
1273
|
const rect = range.getBoundingClientRect();
|
|
1169
1274
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1170
|
-
|
|
1275
|
+
|
|
1171
1276
|
if (containerRect && containerRef.value) {
|
|
1172
|
-
const relativeX =
|
|
1173
|
-
|
|
1277
|
+
const relativeX =
|
|
1278
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1279
|
+
const relativeY =
|
|
1280
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1174
1281
|
bbox = [
|
|
1175
1282
|
relativeX,
|
|
1176
1283
|
relativeY,
|
|
1177
1284
|
relativeX + rect.width,
|
|
1178
|
-
relativeY + rect.height
|
|
1285
|
+
relativeY + rect.height,
|
|
1179
1286
|
];
|
|
1180
1287
|
}
|
|
1181
1288
|
}
|
|
1182
|
-
|
|
1289
|
+
|
|
1183
1290
|
if (!bbox) {
|
|
1184
|
-
Message.warning(
|
|
1291
|
+
Message.warning("无法确定文本位置");
|
|
1185
1292
|
return;
|
|
1186
1293
|
}
|
|
1187
|
-
|
|
1294
|
+
|
|
1188
1295
|
// 检查是否已有批注
|
|
1189
1296
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1190
1297
|
if (existingAnnotation) {
|
|
1191
1298
|
annotationInput.value = existingAnnotation.content;
|
|
1192
1299
|
} else {
|
|
1193
|
-
annotationInput.value =
|
|
1300
|
+
annotationInput.value = "";
|
|
1194
1301
|
}
|
|
1195
|
-
|
|
1302
|
+
|
|
1196
1303
|
currentAnnotationBlock.value = {
|
|
1197
1304
|
bbox,
|
|
1198
|
-
content: selectedText
|
|
1305
|
+
content: selectedText,
|
|
1199
1306
|
};
|
|
1200
|
-
|
|
1307
|
+
|
|
1201
1308
|
// 确保弹窗显示
|
|
1202
1309
|
showAnnotationPopup.value = true;
|
|
1203
|
-
|
|
1310
|
+
|
|
1204
1311
|
// 重新计算弹窗位置(输入框更大)
|
|
1205
1312
|
nextTick(() => {
|
|
1206
1313
|
adjustAnnotationPopupPosition();
|
|
@@ -1213,56 +1320,62 @@ const openAnnotationInput = (e?: Event) => {
|
|
|
1213
1320
|
const adjustAnnotationPopupPosition = () => {
|
|
1214
1321
|
const containerRect = containerRef.value?.getBoundingClientRect();
|
|
1215
1322
|
const popup = annotationPopupRef.value;
|
|
1216
|
-
|
|
1323
|
+
|
|
1217
1324
|
if (!containerRect || !containerRef.value) {
|
|
1218
1325
|
return;
|
|
1219
1326
|
}
|
|
1220
|
-
|
|
1327
|
+
|
|
1221
1328
|
const popupWidth = popup?.getBoundingClientRect().width || 320; // 默认宽度
|
|
1222
1329
|
const popupHeight = popup?.getBoundingClientRect().height || 200; // 默认高度
|
|
1223
1330
|
const spacing = 4; // 减少间距,让弹窗更靠近文字
|
|
1224
|
-
|
|
1331
|
+
|
|
1225
1332
|
// 优先使用文本块位置(和批注按钮位置计算逻辑一致)
|
|
1226
1333
|
let left: number;
|
|
1227
1334
|
let top: number;
|
|
1228
|
-
|
|
1335
|
+
|
|
1229
1336
|
if (activeBlockDiv.value) {
|
|
1230
1337
|
// 使用文本块位置,和批注按钮使用相同的计算逻辑
|
|
1231
1338
|
const rect = activeBlockDiv.value.getBoundingClientRect();
|
|
1232
|
-
const relativeX =
|
|
1233
|
-
|
|
1234
|
-
|
|
1339
|
+
const relativeX =
|
|
1340
|
+
rect.left - containerRect.left + containerRef.value.scrollLeft;
|
|
1341
|
+
const relativeY =
|
|
1342
|
+
rect.top - containerRect.top + containerRef.value.scrollTop;
|
|
1343
|
+
|
|
1235
1344
|
// 计算可用空间
|
|
1236
1345
|
const rectBottom = rect.bottom;
|
|
1237
1346
|
const rectTop = rect.top;
|
|
1238
1347
|
const containerBottom = containerRect.bottom;
|
|
1239
1348
|
const containerTop = containerRect.top;
|
|
1240
|
-
|
|
1349
|
+
|
|
1241
1350
|
const bottomSpace = containerBottom - rectBottom;
|
|
1242
1351
|
const topSpace = rectTop - containerTop;
|
|
1243
|
-
|
|
1352
|
+
|
|
1244
1353
|
// 判断弹窗显示在文字下方还是上方
|
|
1245
|
-
const showOnBottom =
|
|
1246
|
-
|
|
1354
|
+
const showOnBottom =
|
|
1355
|
+
bottomSpace >= popupHeight + spacing ||
|
|
1356
|
+
(bottomSpace > 0 && bottomSpace >= topSpace);
|
|
1357
|
+
|
|
1247
1358
|
// 计算位置(和批注按钮位置计算一致)
|
|
1248
1359
|
const centerX = relativeX + rect.width / 2;
|
|
1249
1360
|
left = centerX - popupWidth / 2;
|
|
1250
|
-
|
|
1361
|
+
|
|
1251
1362
|
if (showOnBottom) {
|
|
1252
1363
|
top = relativeY + rect.height + spacing;
|
|
1253
1364
|
} else {
|
|
1254
1365
|
top = relativeY - popupHeight - spacing;
|
|
1255
1366
|
}
|
|
1256
|
-
|
|
1367
|
+
|
|
1257
1368
|
// 边界检查
|
|
1258
|
-
const popupTopInViewport =
|
|
1369
|
+
const popupTopInViewport =
|
|
1370
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1259
1371
|
const popupBottomInViewport = popupTopInViewport + popupHeight;
|
|
1260
|
-
|
|
1372
|
+
|
|
1261
1373
|
if (popupBottomInViewport > containerBottom - spacing) {
|
|
1262
1374
|
top = relativeY - popupHeight - spacing;
|
|
1263
1375
|
}
|
|
1264
|
-
|
|
1265
|
-
const newPopupTopInViewport =
|
|
1376
|
+
|
|
1377
|
+
const newPopupTopInViewport =
|
|
1378
|
+
top - containerRef.value.scrollTop + containerRect.top;
|
|
1266
1379
|
if (newPopupTopInViewport < containerTop + spacing) {
|
|
1267
1380
|
top = relativeY + rect.height + spacing;
|
|
1268
1381
|
}
|
|
@@ -1270,13 +1383,13 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1270
1383
|
// 如果没有文本块,使用当前按钮位置(作为后备方案)
|
|
1271
1384
|
const currentStyle = annotationPopupStyle.value;
|
|
1272
1385
|
if (currentStyle && currentStyle.left && currentStyle.top) {
|
|
1273
|
-
left = parseFloat(currentStyle.left.toString().replace(
|
|
1274
|
-
top = parseFloat(currentStyle.top.toString().replace(
|
|
1386
|
+
left = parseFloat(currentStyle.left.toString().replace("px", ""));
|
|
1387
|
+
top = parseFloat(currentStyle.top.toString().replace("px", ""));
|
|
1275
1388
|
} else {
|
|
1276
1389
|
return;
|
|
1277
1390
|
}
|
|
1278
1391
|
}
|
|
1279
|
-
|
|
1392
|
+
|
|
1280
1393
|
// 确保不超出容器边界
|
|
1281
1394
|
const containerWidth = containerRef.value.clientWidth;
|
|
1282
1395
|
if (left + popupWidth > containerWidth - spacing) {
|
|
@@ -1285,10 +1398,10 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1285
1398
|
if (left < spacing) {
|
|
1286
1399
|
left = spacing;
|
|
1287
1400
|
}
|
|
1288
|
-
|
|
1401
|
+
|
|
1289
1402
|
annotationPopupStyle.value = {
|
|
1290
1403
|
left: `${left}px`,
|
|
1291
|
-
top: `${top}px
|
|
1404
|
+
top: `${top}px`,
|
|
1292
1405
|
};
|
|
1293
1406
|
};
|
|
1294
1407
|
|
|
@@ -1297,7 +1410,7 @@ const adjustAnnotationPopupPosition = () => {
|
|
|
1297
1410
|
*/
|
|
1298
1411
|
const closeAnnotationInput = () => {
|
|
1299
1412
|
currentAnnotationBlock.value = null;
|
|
1300
|
-
annotationInput.value =
|
|
1413
|
+
annotationInput.value = "";
|
|
1301
1414
|
showAnnotationPopup.value = false;
|
|
1302
1415
|
};
|
|
1303
1416
|
|
|
@@ -1310,7 +1423,7 @@ const handleAnnotationPopupLeave = (e: MouseEvent) => {
|
|
|
1310
1423
|
if (relatedTarget && annotationPopupRef.value?.contains(relatedTarget)) {
|
|
1311
1424
|
return;
|
|
1312
1425
|
}
|
|
1313
|
-
|
|
1426
|
+
|
|
1314
1427
|
// 延迟关闭,给用户时间操作
|
|
1315
1428
|
hideTimer = setTimeout(() => {
|
|
1316
1429
|
closeAnnotationInput();
|
|
@@ -1324,35 +1437,37 @@ const saveAnnotation = () => {
|
|
|
1324
1437
|
if (!currentAnnotationBlock.value || !annotationInput.value.trim()) {
|
|
1325
1438
|
return;
|
|
1326
1439
|
}
|
|
1327
|
-
|
|
1440
|
+
|
|
1328
1441
|
const { bbox, content } = currentAnnotationBlock.value;
|
|
1329
1442
|
const annotationContent = annotationInput.value.trim();
|
|
1330
|
-
|
|
1443
|
+
|
|
1331
1444
|
// 检查是否已有批注
|
|
1332
1445
|
const existingAnnotation = getAnnotationForBlock(bbox);
|
|
1333
|
-
|
|
1446
|
+
|
|
1334
1447
|
const annotation: AnnotationInfo = {
|
|
1335
|
-
id:
|
|
1448
|
+
id:
|
|
1449
|
+
existingAnnotation?.id ||
|
|
1450
|
+
`annotation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1336
1451
|
blockBbox: bbox,
|
|
1337
1452
|
blockContent: content,
|
|
1338
1453
|
blockPage: currentPage.value,
|
|
1339
1454
|
content: annotationContent,
|
|
1340
|
-
createTime: existingAnnotation?.createTime || Date.now()
|
|
1455
|
+
createTime: existingAnnotation?.createTime || Date.now(),
|
|
1341
1456
|
};
|
|
1342
|
-
|
|
1457
|
+
|
|
1343
1458
|
if (existingAnnotation) {
|
|
1344
1459
|
// 更新已有批注
|
|
1345
|
-
emit(
|
|
1346
|
-
Message.success(
|
|
1460
|
+
emit("annotation-update", annotation);
|
|
1461
|
+
Message.success("批注已更新");
|
|
1347
1462
|
} else {
|
|
1348
1463
|
// 添加新批注
|
|
1349
|
-
emit(
|
|
1350
|
-
Message.success(
|
|
1464
|
+
emit("annotation-add", annotation);
|
|
1465
|
+
Message.success("批注已添加");
|
|
1351
1466
|
}
|
|
1352
|
-
|
|
1467
|
+
|
|
1353
1468
|
// 关闭输入框
|
|
1354
1469
|
closeAnnotationInput();
|
|
1355
|
-
|
|
1470
|
+
|
|
1356
1471
|
// 重新渲染文本图层以显示批注标记
|
|
1357
1472
|
nextTick(() => {
|
|
1358
1473
|
renderTextLayer();
|
|
@@ -1372,85 +1487,134 @@ const handleScroll = (e: Event) => {
|
|
|
1372
1487
|
showAnnotationPopup.value = false;
|
|
1373
1488
|
activeBlockDiv.value = null;
|
|
1374
1489
|
}
|
|
1375
|
-
|
|
1490
|
+
|
|
1376
1491
|
// 在自适应宽度模式下,滚动操作不应该改变缩放比例
|
|
1377
1492
|
// 如果缩放比例被意外改变,恢复它
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1493
|
+
if (
|
|
1494
|
+
props.autoFitWidth &&
|
|
1495
|
+
!isUserZooming.value &&
|
|
1496
|
+
initialAutoFitScale.value !== null &&
|
|
1497
|
+
Math.abs(scale.value - initialAutoFitScale.value) > 0.01
|
|
1498
|
+
) {
|
|
1382
1499
|
scale.value = initialAutoFitScale.value;
|
|
1383
1500
|
}
|
|
1384
|
-
|
|
1501
|
+
|
|
1385
1502
|
// 滚动翻页功能
|
|
1386
|
-
if (
|
|
1503
|
+
if (
|
|
1504
|
+
props.enableScrollPaging &&
|
|
1505
|
+
totalPages.value > 1 &&
|
|
1506
|
+
!isScrollPaging.value
|
|
1507
|
+
) {
|
|
1387
1508
|
handleScrollPaging(e);
|
|
1388
1509
|
}
|
|
1389
1510
|
};
|
|
1390
1511
|
|
|
1512
|
+
// 记录上次滚动位置,用于判断滚动方向
|
|
1513
|
+
let lastScrollTop = 0;
|
|
1514
|
+
|
|
1391
1515
|
/**
|
|
1392
|
-
*
|
|
1516
|
+
* 处理滚动翻页(通过滚动位置判断当前页)
|
|
1517
|
+
* 向下滑动:当视口顶部到达下一页的顶部时,切换到下一页
|
|
1518
|
+
* 向上滑动:当视口底部到达上一页的底部时,切换到上一页
|
|
1393
1519
|
*/
|
|
1394
1520
|
const handleScrollPaging = (e: Event) => {
|
|
1395
1521
|
const container = e.target as HTMLElement;
|
|
1396
|
-
if (!container) return;
|
|
1397
|
-
|
|
1522
|
+
if (!container || !containerRef.value) return;
|
|
1523
|
+
|
|
1398
1524
|
// 如果正在翻页中,直接返回
|
|
1399
1525
|
if (isScrollPaging.value) {
|
|
1400
1526
|
return;
|
|
1401
1527
|
}
|
|
1402
|
-
|
|
1528
|
+
|
|
1403
1529
|
// 清除之前的定时器
|
|
1404
1530
|
if (scrollPagingTimer) {
|
|
1405
1531
|
clearTimeout(scrollPagingTimer);
|
|
1406
1532
|
}
|
|
1407
|
-
|
|
1408
|
-
//
|
|
1533
|
+
|
|
1534
|
+
// 使用防抖,避免频繁触发
|
|
1409
1535
|
scrollPagingTimer = setTimeout(() => {
|
|
1410
1536
|
// 再次检查是否正在翻页
|
|
1411
1537
|
if (isScrollPaging.value) {
|
|
1412
1538
|
return;
|
|
1413
1539
|
}
|
|
1414
|
-
|
|
1540
|
+
|
|
1415
1541
|
const scrollTop = container.scrollTop;
|
|
1416
|
-
const scrollHeight = container.scrollHeight;
|
|
1417
1542
|
const clientHeight = container.clientHeight;
|
|
1543
|
+
const scrollBottom = scrollTop + clientHeight;
|
|
1544
|
+
|
|
1545
|
+
// 判断滚动方向
|
|
1546
|
+
const isScrollingDown = scrollTop > lastScrollTop;
|
|
1547
|
+
lastScrollTop = scrollTop;
|
|
1548
|
+
|
|
1549
|
+
// 获取当前页的元素
|
|
1550
|
+
const currentPageElement = container.querySelector(
|
|
1551
|
+
`[data-page-number="${currentPage.value}"]`
|
|
1552
|
+
) as HTMLElement;
|
|
1418
1553
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1554
|
+
if (!currentPageElement) return;
|
|
1555
|
+
|
|
1556
|
+
const currentPageTop = currentPageElement.offsetTop;
|
|
1557
|
+
const currentPageHeight = currentPageElement.offsetHeight;
|
|
1558
|
+
const currentPageBottom = currentPageTop + currentPageHeight;
|
|
1559
|
+
|
|
1560
|
+
let newPage = currentPage.value;
|
|
1561
|
+
|
|
1562
|
+
if (isScrollingDown) {
|
|
1563
|
+
// 向下滑动:当视口顶部到达或超过下一页的顶部时,切换到下一页
|
|
1421
1564
|
if (currentPage.value < totalPages.value) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1565
|
+
const nextPageElement = container.querySelector(
|
|
1566
|
+
`[data-page-number="${currentPage.value + 1}"]`
|
|
1567
|
+
) as HTMLElement;
|
|
1568
|
+
|
|
1569
|
+
if (nextPageElement) {
|
|
1570
|
+
const nextPageTop = nextPageElement.offsetTop;
|
|
1571
|
+
// 当视口顶部到达或超过下一页的顶部时,切换到下一页
|
|
1572
|
+
if (scrollTop >= nextPageTop) {
|
|
1573
|
+
newPage = currentPage.value + 1;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1431
1576
|
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
else if (scrollTop <= 100) {
|
|
1577
|
+
} else {
|
|
1578
|
+
// 向上滑动:当视口底部到达或超过上一页的底部时,切换到上一页
|
|
1435
1579
|
if (currentPage.value > 1) {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1580
|
+
const prevPageElement = container.querySelector(
|
|
1581
|
+
`[data-page-number="${currentPage.value - 1}"]`
|
|
1582
|
+
) as HTMLElement;
|
|
1583
|
+
|
|
1584
|
+
if (prevPageElement) {
|
|
1585
|
+
const prevPageTop = prevPageElement.offsetTop;
|
|
1586
|
+
const prevPageHeight = prevPageElement.offsetHeight;
|
|
1587
|
+
const prevPageBottom = prevPageTop + prevPageHeight;
|
|
1588
|
+
|
|
1589
|
+
// 当视口底部到达或超过上一页的底部时,切换到上一页
|
|
1590
|
+
if (scrollBottom <= prevPageBottom) {
|
|
1591
|
+
newPage = currentPage.value - 1;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1445
1594
|
}
|
|
1446
1595
|
}
|
|
1447
|
-
|
|
1596
|
+
|
|
1597
|
+
// 如果页码发生变化,更新当前页
|
|
1598
|
+
if (newPage !== currentPage.value) {
|
|
1599
|
+
isScrollPaging.value = true;
|
|
1600
|
+
currentPage.value = newPage;
|
|
1601
|
+
emit("page-change", newPage, totalPages.value);
|
|
1602
|
+
|
|
1603
|
+
// 延迟解锁,确保页面切换完成
|
|
1604
|
+
setTimeout(() => {
|
|
1605
|
+
isScrollPaging.value = false;
|
|
1606
|
+
}, 100);
|
|
1607
|
+
}
|
|
1608
|
+
}, 100);
|
|
1448
1609
|
};
|
|
1449
1610
|
|
|
1450
1611
|
/**
|
|
1451
1612
|
* 检查元素是否在视口内可见
|
|
1452
1613
|
*/
|
|
1453
|
-
const isElementVisible = (
|
|
1614
|
+
const isElementVisible = (
|
|
1615
|
+
element: HTMLElement,
|
|
1616
|
+
container: HTMLElement
|
|
1617
|
+
): boolean => {
|
|
1454
1618
|
const elementRect = element.getBoundingClientRect();
|
|
1455
1619
|
const containerRect = container.getBoundingClientRect();
|
|
1456
1620
|
|
|
@@ -1464,38 +1628,51 @@ const isElementVisible = (element: HTMLElement, container: HTMLElement): boolean
|
|
|
1464
1628
|
|
|
1465
1629
|
/**
|
|
1466
1630
|
* 高亮指定位置
|
|
1467
|
-
* @param
|
|
1631
|
+
* @param pageNum 页码
|
|
1632
|
+
* @param bbox 块的边界框 [x1, y1, x2, y2](相对于该页面的坐标)
|
|
1468
1633
|
* @param shouldScroll 是否应该滚动到元素位置
|
|
1469
1634
|
* @returns 是否成功找到并高亮了元素
|
|
1470
1635
|
*/
|
|
1471
1636
|
const highlightPosition = (
|
|
1637
|
+
pageNum: number,
|
|
1472
1638
|
bbox: [number, number, number, number],
|
|
1473
1639
|
shouldScroll: boolean = true
|
|
1474
1640
|
): boolean => {
|
|
1475
1641
|
// 清除之前的高亮
|
|
1476
1642
|
if (activeBlockDiv.value) {
|
|
1477
|
-
activeBlockDiv.value.style.backgroundColor =
|
|
1478
|
-
activeBlockDiv.value.style.boxShadow =
|
|
1643
|
+
activeBlockDiv.value.style.backgroundColor = "transparent";
|
|
1644
|
+
activeBlockDiv.value.style.boxShadow = "none";
|
|
1479
1645
|
activeBlockDiv.value = null;
|
|
1480
1646
|
}
|
|
1481
1647
|
isHighlighted.value = false;
|
|
1482
1648
|
|
|
1483
|
-
|
|
1649
|
+
// 如果页码不在有效范围内,返回 false
|
|
1650
|
+
if (pageNum < 1 || pageNum > totalPages.value) {
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// 在指定页面的文本图层中查找
|
|
1655
|
+
const textLayer = textLayerRefs.get(pageNum);
|
|
1484
1656
|
if (!textLayer) return false;
|
|
1485
1657
|
|
|
1486
|
-
// 查找对应的块元素,使用存储的bbox数据匹配(使用容差)
|
|
1487
|
-
const blockDivs = textLayer.querySelectorAll('.text-block');
|
|
1488
1658
|
const tolerance = 2;
|
|
1489
|
-
|
|
1490
1659
|
let matchedElement: HTMLElement | null = null;
|
|
1491
1660
|
|
|
1661
|
+
// 查找对应的块元素,使用存储的bbox数据匹配(使用容差)
|
|
1662
|
+
const blockDivs = textLayer.querySelectorAll(".text-block");
|
|
1663
|
+
|
|
1492
1664
|
blockDivs.forEach((div) => {
|
|
1493
1665
|
const el = div as HTMLElement;
|
|
1494
1666
|
const storedBbox = el.dataset.bbox;
|
|
1495
1667
|
if (!storedBbox) return;
|
|
1496
1668
|
|
|
1497
1669
|
try {
|
|
1498
|
-
const parsedBbox = JSON.parse(storedBbox) as [
|
|
1670
|
+
const parsedBbox = JSON.parse(storedBbox) as [
|
|
1671
|
+
number,
|
|
1672
|
+
number,
|
|
1673
|
+
number,
|
|
1674
|
+
number
|
|
1675
|
+
];
|
|
1499
1676
|
|
|
1500
1677
|
const isMatch =
|
|
1501
1678
|
Math.abs(parsedBbox[0] - bbox[0]) < tolerance &&
|
|
@@ -1507,35 +1684,48 @@ const highlightPosition = (
|
|
|
1507
1684
|
matchedElement = el;
|
|
1508
1685
|
}
|
|
1509
1686
|
} catch (error) {
|
|
1510
|
-
console.warn(
|
|
1687
|
+
console.warn("解析bbox失败:", error);
|
|
1511
1688
|
}
|
|
1512
1689
|
});
|
|
1513
1690
|
|
|
1514
1691
|
if (!matchedElement) return false;
|
|
1515
1692
|
|
|
1693
|
+
// 如果找到的元素不在当前页,切换到对应页面
|
|
1694
|
+
if (pageNum !== currentPage.value) {
|
|
1695
|
+
switchToPage(pageNum);
|
|
1696
|
+
// 等待页面切换完成后再高亮
|
|
1697
|
+
nextTick(() => {
|
|
1698
|
+
setTimeout(() => {
|
|
1699
|
+
highlightPosition(pageNum, bbox, shouldScroll);
|
|
1700
|
+
}, 300);
|
|
1701
|
+
});
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1516
1705
|
// 保存引用
|
|
1517
|
-
const elementRef = matchedElement;
|
|
1706
|
+
const elementRef = matchedElement as HTMLElement;
|
|
1518
1707
|
|
|
1519
1708
|
activeBlockDiv.value = elementRef;
|
|
1520
1709
|
isHighlighted.value = true;
|
|
1521
1710
|
|
|
1522
1711
|
// 使用一致的高亮样式
|
|
1523
|
-
elementRef.style.backgroundColor =
|
|
1524
|
-
|
|
1712
|
+
elementRef.style.backgroundColor =
|
|
1713
|
+
"var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))";
|
|
1714
|
+
elementRef.style.boxShadow = "0 0 0 2px rgba(30, 144, 255, 0.6)";
|
|
1525
1715
|
|
|
1526
1716
|
// 只有在需要滚动且元素不在视口内时才滚动
|
|
1527
1717
|
if (shouldScroll && containerRef.value) {
|
|
1528
1718
|
const isVisible = isElementVisible(elementRef, containerRef.value);
|
|
1529
1719
|
if (!isVisible) {
|
|
1530
|
-
elementRef.scrollIntoView({ behavior:
|
|
1720
|
+
elementRef.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1531
1721
|
}
|
|
1532
1722
|
}
|
|
1533
1723
|
|
|
1534
1724
|
// 5秒后自动取消高亮
|
|
1535
1725
|
setTimeout(() => {
|
|
1536
1726
|
if (activeBlockDiv.value === elementRef && isHighlighted.value) {
|
|
1537
|
-
elementRef.style.backgroundColor =
|
|
1538
|
-
elementRef.style.boxShadow =
|
|
1727
|
+
elementRef.style.backgroundColor = "transparent";
|
|
1728
|
+
elementRef.style.boxShadow = "none";
|
|
1539
1729
|
activeBlockDiv.value = null;
|
|
1540
1730
|
isHighlighted.value = false;
|
|
1541
1731
|
}
|
|
@@ -1546,13 +1736,21 @@ const highlightPosition = (
|
|
|
1546
1736
|
|
|
1547
1737
|
/**
|
|
1548
1738
|
* 跳转到指定位置并高亮(外部调用接口)
|
|
1549
|
-
* @param
|
|
1739
|
+
* @param pageNum 页码(必须)
|
|
1740
|
+
* @param bbox 位置的边界框 [x1, y1, x2, y2](相对于该页面的坐标)
|
|
1550
1741
|
* @param emitEvent 是否触发跳转事件,默认为 true
|
|
1551
1742
|
*/
|
|
1552
1743
|
const jumpToPosition = (
|
|
1744
|
+
pageNum: number,
|
|
1553
1745
|
bbox: [number, number, number, number],
|
|
1554
1746
|
emitEvent: boolean = true
|
|
1555
1747
|
) => {
|
|
1748
|
+
// 如果页码不在有效范围内,直接返回
|
|
1749
|
+
if (pageNum < 1 || pageNum > totalPages.value) {
|
|
1750
|
+
console.warn(`页码 ${pageNum} 不在有效范围内 (1-${totalPages.value})`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1556
1754
|
// 等待DOM更新后再高亮
|
|
1557
1755
|
nextTick(() => {
|
|
1558
1756
|
let retryCount = 0;
|
|
@@ -1560,17 +1758,17 @@ const jumpToPosition = (
|
|
|
1560
1758
|
const retryDelay = 200;
|
|
1561
1759
|
|
|
1562
1760
|
const tryHighlight = () => {
|
|
1563
|
-
const success = highlightPosition(bbox, true);
|
|
1761
|
+
const success = highlightPosition(pageNum, bbox, true);
|
|
1564
1762
|
if (success) {
|
|
1565
1763
|
// 高亮成功,触发事件
|
|
1566
1764
|
if (emitEvent) {
|
|
1567
|
-
emit(
|
|
1765
|
+
emit("position-jump", bbox, "block");
|
|
1568
1766
|
}
|
|
1569
1767
|
} else if (retryCount < maxRetries) {
|
|
1570
1768
|
retryCount++;
|
|
1571
1769
|
setTimeout(tryHighlight, retryDelay);
|
|
1572
1770
|
} else {
|
|
1573
|
-
console.warn(`无法找到并高亮指定位置: bbox: [${bbox.join(
|
|
1771
|
+
console.warn(`无法找到并高亮指定位置: 页码 ${pageNum}, bbox: [${bbox.join(", ")}]`);
|
|
1574
1772
|
}
|
|
1575
1773
|
};
|
|
1576
1774
|
|
|
@@ -1647,10 +1845,14 @@ watch(
|
|
|
1647
1845
|
/**
|
|
1648
1846
|
* 监听缩放、旋转、位置变化,更新文本图层位置
|
|
1649
1847
|
*/
|
|
1650
|
-
watch(
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1848
|
+
watch(
|
|
1849
|
+
[scale, rotation, position],
|
|
1850
|
+
() => {
|
|
1851
|
+
// 文本图层会跟随图片的 transform,不需要手动更新位置
|
|
1852
|
+
// 但需要确保文本图层与图片同步变换
|
|
1853
|
+
},
|
|
1854
|
+
{ deep: true }
|
|
1855
|
+
);
|
|
1654
1856
|
|
|
1655
1857
|
/**
|
|
1656
1858
|
* 组件挂载时的初始化
|
|
@@ -1690,12 +1892,15 @@ defineExpose({
|
|
|
1690
1892
|
jumpToPosition, // 暴露定位方法给父组件
|
|
1691
1893
|
goToPage: switchToPage, // 暴露翻页方法给父组件
|
|
1692
1894
|
getCurrentPage: () => currentPage.value, // 获取当前页码
|
|
1693
|
-
getTotalPages: () => totalPages.value // 获取总页数
|
|
1895
|
+
getTotalPages: () => totalPages.value, // 获取总页数
|
|
1694
1896
|
});
|
|
1695
1897
|
</script>
|
|
1696
1898
|
|
|
1697
1899
|
<style lang="less" scoped>
|
|
1698
1900
|
// 样式已统一到公共样式文件,这里只保留组件特定样式
|
|
1901
|
+
.toolbar-group {
|
|
1902
|
+
gap: 0 !important;
|
|
1903
|
+
}
|
|
1699
1904
|
|
|
1700
1905
|
// 图片容器
|
|
1701
1906
|
.image-container {
|
|
@@ -1719,8 +1924,8 @@ defineExpose({
|
|
|
1719
1924
|
// 图片包装器容器(用于居中显示和提供滚动空间)
|
|
1720
1925
|
.image-wrapper-container {
|
|
1721
1926
|
display: flex;
|
|
1722
|
-
|
|
1723
|
-
|
|
1927
|
+
flex-direction: column; // 垂直排列所有页面
|
|
1928
|
+
align-items: center; // 水平居中
|
|
1724
1929
|
width: 100%;
|
|
1725
1930
|
min-height: 100%;
|
|
1726
1931
|
box-sizing: border-box;
|
|
@@ -1734,6 +1939,17 @@ defineExpose({
|
|
|
1734
1939
|
padding: 0;
|
|
1735
1940
|
}
|
|
1736
1941
|
|
|
1942
|
+
// 每个页面的容器
|
|
1943
|
+
.image-page-container {
|
|
1944
|
+
position: relative;
|
|
1945
|
+
display: flex;
|
|
1946
|
+
justify-content: center;
|
|
1947
|
+
width: 100%;
|
|
1948
|
+
flex-shrink: 0;
|
|
1949
|
+
margin: 0;
|
|
1950
|
+
padding: 0;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1737
1953
|
// 图片包装器(包含图片和文本图层)
|
|
1738
1954
|
.image-wrapper {
|
|
1739
1955
|
position: relative;
|
|
@@ -1747,7 +1963,7 @@ defineExpose({
|
|
|
1747
1963
|
margin: 0;
|
|
1748
1964
|
padding: 0;
|
|
1749
1965
|
line-height: 0;
|
|
1750
|
-
|
|
1966
|
+
|
|
1751
1967
|
img {
|
|
1752
1968
|
display: block; // 移除图片底部默认间隙
|
|
1753
1969
|
margin: 0;
|
|
@@ -1808,7 +2024,7 @@ defineExpose({
|
|
|
1808
2024
|
border-radius: 6px;
|
|
1809
2025
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
1810
2026
|
animation: fade-in 0.2s ease;
|
|
1811
|
-
|
|
2027
|
+
|
|
1812
2028
|
.annotation-button-action {
|
|
1813
2029
|
display: flex;
|
|
1814
2030
|
flex-direction: row;
|
|
@@ -1838,20 +2054,20 @@ defineExpose({
|
|
|
1838
2054
|
box-shadow: 0 4px 16px rgb(0 0 0 / 20%);
|
|
1839
2055
|
animation: fade-in 0.2s ease;
|
|
1840
2056
|
overflow: hidden;
|
|
1841
|
-
|
|
2057
|
+
|
|
1842
2058
|
.annotation-input-header {
|
|
1843
2059
|
display: flex;
|
|
1844
2060
|
align-items: center;
|
|
1845
2061
|
justify-content: space-between;
|
|
1846
2062
|
padding: 12px 16px;
|
|
1847
2063
|
border-bottom: 1px solid #e5e7eb;
|
|
1848
|
-
|
|
2064
|
+
|
|
1849
2065
|
.annotation-input-title {
|
|
1850
2066
|
font-size: 14px;
|
|
1851
2067
|
font-weight: 500;
|
|
1852
2068
|
color: #1d2129;
|
|
1853
2069
|
}
|
|
1854
|
-
|
|
2070
|
+
|
|
1855
2071
|
.annotation-close-btn {
|
|
1856
2072
|
display: flex;
|
|
1857
2073
|
align-items: center;
|
|
@@ -1865,22 +2081,22 @@ defineExpose({
|
|
|
1865
2081
|
border-radius: 4px;
|
|
1866
2082
|
color: #86909c;
|
|
1867
2083
|
transition: all 0.2s;
|
|
1868
|
-
|
|
2084
|
+
|
|
1869
2085
|
&:hover {
|
|
1870
2086
|
background-color: #f0f0f0;
|
|
1871
2087
|
color: #1d2129;
|
|
1872
2088
|
}
|
|
1873
2089
|
}
|
|
1874
2090
|
}
|
|
1875
|
-
|
|
2091
|
+
|
|
1876
2092
|
.annotation-input-content {
|
|
1877
2093
|
padding: 12px 16px;
|
|
1878
|
-
|
|
2094
|
+
|
|
1879
2095
|
.annotation-textarea {
|
|
1880
2096
|
width: 100%;
|
|
1881
2097
|
}
|
|
1882
2098
|
}
|
|
1883
|
-
|
|
2099
|
+
|
|
1884
2100
|
.annotation-input-footer {
|
|
1885
2101
|
display: flex;
|
|
1886
2102
|
align-items: center;
|
|
@@ -1903,17 +2119,17 @@ defineExpose({
|
|
|
1903
2119
|
padding: 1px 3px;
|
|
1904
2120
|
// 添加阴影效果
|
|
1905
2121
|
box-shadow: 0 1px 2px rgba(255, 193, 7, 0.25) !important;
|
|
1906
|
-
|
|
2122
|
+
|
|
1907
2123
|
// 鼠标悬停时加深效果
|
|
1908
2124
|
&:hover {
|
|
1909
2125
|
background-color: rgba(255, 243, 205, 0.7) !important;
|
|
1910
2126
|
border-color: rgba(255, 193, 7, 0.9) !important;
|
|
1911
2127
|
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.35) !important;
|
|
1912
2128
|
}
|
|
1913
|
-
|
|
2129
|
+
|
|
1914
2130
|
// 使用伪元素添加批注标记图标
|
|
1915
2131
|
&::before {
|
|
1916
|
-
content:
|
|
2132
|
+
content: "📝";
|
|
1917
2133
|
position: absolute;
|
|
1918
2134
|
top: -6px;
|
|
1919
2135
|
right: -6px;
|