@koi-br/ocr-web-sdk 1.0.52 → 1.0.53

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.
@@ -12,12 +12,7 @@
12
12
  <div class="toolbar-group">
13
13
  <span class="scale-text"> {{ Math.round(scale * 100) }}% </span>
14
14
  <div class="toolbar-divider"></div>
15
- <ATooltip
16
- v-if="showResetButton"
17
- mini
18
- position="bottom"
19
- content="重置"
20
- >
15
+ <ATooltip v-if="showResetButton" mini position="bottom" content="重置">
21
16
  <AButton size="small" type="outline" @click="reset">
22
17
  <RefreshCcw :size="16" />
23
18
  </AButton>
@@ -96,7 +91,7 @@
96
91
  <AButton
97
92
  size="small"
98
93
  type="outline"
99
- style="padding-right: 0px;"
94
+ style="padding-right: 0px"
100
95
  :disabled="currentPage >= totalPages"
101
96
  @click="goToNextPage"
102
97
  >
@@ -121,9 +116,9 @@
121
116
  <div class="loading-spinner"></div>
122
117
  <div class="loading-text">加载中...</div>
123
118
  </div>
124
-
125
- <div
126
- class="image-wrapper-container"
119
+
120
+ <div
121
+ class="image-wrapper-container"
127
122
  :style="containerStyle"
128
123
  :class="{ 'image-hidden': !isImageReady && autoFitWidth }"
129
124
  >
@@ -162,7 +157,10 @@
162
157
 
163
158
  <!-- 文本图层(用于文本块选择和定位) -->
164
159
  <div
165
- v-if="getPageBlocksData(pageIndex + 1) && getPageBlocksData(pageIndex + 1).length > 0"
160
+ v-if="
161
+ getPageBlocksData(pageIndex + 1) &&
162
+ getPageBlocksData(pageIndex + 1).length > 0
163
+ "
166
164
  :ref="(el) => setTextLayerRef(el, pageIndex + 1)"
167
165
  class="text-layer"
168
166
  ></div>
@@ -194,15 +192,48 @@
194
192
  @mouseenter="cancelHideAnnotationButton"
195
193
  >
196
194
  <div class="annotation-input-header">
197
- <span class="annotation-input-title">添加批注</span>
195
+ <div class="annotation-input-header-title">
196
+ {{ currentAnnotationBlock?.content }}
197
+ </div>
198
198
  <button class="annotation-close-btn" @click="closeAnnotationInput">
199
199
  <X :size="14" />
200
200
  </button>
201
201
  </div>
202
202
  <div class="annotation-input-content">
203
+ <!-- 插槽:允许父组件自定义批注头部显示(包括用户名和头像) -->
204
+ <slot
205
+ name="annotation-header"
206
+ :annotation="currentEditingAnnotation"
207
+ :is-editing="!!currentEditingAnnotation"
208
+ >
209
+ <!-- 默认显示:如果没有使用插槽,且有用户信息,则显示用户名和头像 -->
210
+ <div
211
+ v-if="
212
+ currentEditingAnnotation?.username ||
213
+ currentEditingAnnotation?.avatar
214
+ "
215
+ class="annotation-header-user-info"
216
+ >
217
+ <img
218
+ v-if="currentEditingAnnotation?.avatar"
219
+ :src="currentEditingAnnotation.avatar"
220
+ :alt="currentEditingAnnotation.username || '用户'"
221
+ class="annotation-user-avatar"
222
+ />
223
+ <span
224
+ v-if="currentEditingAnnotation?.username"
225
+ class="annotation-username"
226
+ >
227
+ {{ currentEditingAnnotation.username }}
228
+ </span>
229
+ </div>
230
+ <span v-else class="annotation-input-title">
231
+ {{ currentEditingAnnotation ? "编辑批注" : "添加批注" }}
232
+ </span>
233
+ </slot>
203
234
  <Textarea
204
235
  v-model="annotationInput"
205
- :auto-size="{ minRows: 3, maxRows: 6 }"
236
+ :auto-size="{ minRows: 1, maxRows: 3}"
206
237
  placeholder="请输入批注内容..."
207
238
  class="annotation-textarea"
208
239
  @keydown.ctrl.enter="saveAnnotation"
@@ -213,17 +244,18 @@
213
244
  @mousedown.stop
214
245
  />
215
246
  </div>
216
- <div class="annotation-input-footer">
217
- <AButton size="small" type="outline" @click="closeAnnotationInput">
247
+ <div class="annotation-input-footer" v-if="annotationInput">
248
+ <AButton size="small" type="outline" style="border:1px solid rgb(208, 211, 214);border-radius: 4px;" @click="closeAnnotationInput">
218
249
  取消
219
250
  </AButton>
220
251
  <AButton
221
252
  size="small"
222
253
  type="primary"
254
+ style="border-radius: 4px;"
223
255
  @click="saveAnnotation"
224
256
  :disabled="!annotationInput.trim()"
225
257
  >
226
- 保存
258
+ 发送
227
259
  </AButton>
228
260
  </div>
229
261
  </div>
@@ -281,6 +313,10 @@ interface AnnotationInfo {
281
313
  blockPage?: number; // 所属页码
282
314
  content: string; // 批注内容
283
315
  createTime: number; // 创建时间戳
316
+ // 可选的用户信息(业务层可以自行决定是否传递)
317
+ username?: string; // 用户名
318
+ avatar?: string; // 头像URL
319
+ userId?: string | number; // 用户ID
284
320
  }
285
321
 
286
322
  /**
@@ -380,7 +416,8 @@ const props = defineProps({
380
416
  hoverStyle: {
381
417
  type: Object as () => HoverStyle,
382
418
  default: () => ({
383
- backgroundColor: "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))",
419
+ backgroundColor:
420
+ "var(--s-color-brand-primary-transparent-3, rgba(0, 102, 255, .15))",
384
421
  boxShadow: "0 0 0 2px rgba(30, 144, 255, 0.6)",
385
422
  borderRadius: "2px",
386
423
  padding: "1px 3px",
@@ -395,7 +432,8 @@ const props = defineProps({
395
432
  border: "2px solid rgba(30, 144, 255, 0.8)",
396
433
  borderRadius: "3px",
397
434
  padding: "1px 3px",
398
- boxShadow: "0 0 0 2px rgba(30, 144, 255, 0.6), 0 1px 2px rgba(255, 193, 7, 0.25)",
435
+ boxShadow:
436
+ "0 0 0 2px rgba(30, 144, 255, 0.6), 0 1px 2px rgba(255, 193, 7, 0.25)",
399
437
  }),
400
438
  },
401
439
  // hover 时批注按钮的隐藏延迟(毫秒)
@@ -456,22 +494,22 @@ const isImageReady = ref(false); // 标记图片是否已准备好显示(自
456
494
  watch(
457
495
  () => imageUrls.value,
458
496
  (newUrls, oldUrls) => {
459
- console.log('[ImagePreview] imageUrls changed:', {
497
+ console.log("[ImagePreview] imageUrls changed:", {
460
498
  newUrls: newUrls?.length,
461
499
  oldUrls: oldUrls?.length,
462
500
  autoFitWidth: props.autoFitWidth,
463
501
  isImageReady: isImageReady.value,
464
502
  isCalculatingAutoFit: isCalculatingAutoFit.value,
465
503
  });
466
-
504
+
467
505
  // 如果有新的图片URL,且启用自适应宽度,立即隐藏图片
468
506
  if (newUrls && newUrls.length > 0 && props.autoFitWidth) {
469
- console.log('[ImagePreview] 设置图片隐藏,等待自适应宽度计算');
507
+ console.log("[ImagePreview] 设置图片隐藏,等待自适应宽度计算");
470
508
  isImageReady.value = false;
471
509
  isCalculatingAutoFit.value = true;
472
510
  } else if (!props.autoFitWidth) {
473
511
  // 如果没有启用自适应宽度,立即显示
474
- console.log('[ImagePreview] 未启用自适应宽度,立即显示图片');
512
+ console.log("[ImagePreview] 未启用自适应宽度,立即显示图片");
475
513
  isImageReady.value = true;
476
514
  isCalculatingAutoFit.value = false;
477
515
  }
@@ -537,10 +575,10 @@ const imageSize = computed({
537
575
  // 在自适应宽度模式下,使用第一页的尺寸;否则使用当前页的尺寸
538
576
  const scaledImageSize = computed(() => {
539
577
  // 如果启用自适应宽度,使用第一页的尺寸作为基准
540
- const baseSize = props.autoFitWidth
541
- ? (imageSizes.get(1) || { width: 0, height: 0 })
578
+ const baseSize = props.autoFitWidth
579
+ ? imageSizes.get(1) || { width: 0, height: 0 }
542
580
  : imageSize.value;
543
-
581
+
544
582
  if (baseSize.width === 0 || baseSize.height === 0) {
545
583
  return { width: 0, height: 0 };
546
584
  }
@@ -587,6 +625,7 @@ const currentAnnotationBlock = ref<{
587
625
  bbox: [number, number, number, number];
588
626
  content: string;
589
627
  } | null>(null); // 当前正在添加批注的文本块
628
+ const currentEditingAnnotation = ref<AnnotationInfo | null>(null); // 当前正在编辑的批注信息(如果有)
590
629
  const annotationPopupRef = ref<HTMLElement>(); // 批注弹窗引用
591
630
 
592
631
  // 文本选择相关状态(保留用于兼容)
@@ -635,7 +674,7 @@ const getPageScaledSize = (pageNo: number) => {
635
674
  const getPageContainerStyle = (pageNo: number) => {
636
675
  const scaledSize = getPageScaledSize(pageNo);
637
676
  return {
638
- height: scaledSize.height > 0 ? `${scaledSize.height}px` : 'auto',
677
+ height: scaledSize.height > 0 ? `${scaledSize.height}px` : "auto",
639
678
  };
640
679
  };
641
680
 
@@ -939,7 +978,7 @@ const switchToPage = (page: number) => {
939
978
  ) as HTMLElement;
940
979
  if (pageElement) {
941
980
  // 标记这是翻页滚动,不应该被同步滚动干扰
942
- containerRef.value.dataset.pageScrolling = 'true';
981
+ containerRef.value.dataset.pageScrolling = "true";
943
982
  pageElement.scrollIntoView({ behavior: "smooth", block: "start" });
944
983
  // 更新 lastScrollTop,确保滚动方向判断准确
945
984
  nextTick(() => {
@@ -975,12 +1014,12 @@ const reset = () => {
975
1014
  const hadHighlight = isHighlighted.value && activeBlockDiv.value !== null;
976
1015
  activeBlockDiv.value = null;
977
1016
  isHighlighted.value = false;
978
-
1017
+
979
1018
  // 如果之前有高亮,触发高亮清除事件
980
1019
  if (hadHighlight) {
981
1020
  emit("highlight-clear");
982
1021
  }
983
-
1022
+
984
1023
  // 清除已渲染页面集合
985
1024
  renderedPages.value.clear();
986
1025
  };
@@ -991,7 +1030,7 @@ const original = () => {
991
1030
 
992
1031
  // 计算自适应宽度的缩放比例
993
1032
  const calculateAutoFitScale = () => {
994
- console.log('[ImagePreview] calculateAutoFitScale 开始:', {
1033
+ console.log("[ImagePreview] calculateAutoFitScale 开始:", {
995
1034
  autoFitWidth: props.autoFitWidth,
996
1035
  hasContainerRef: !!containerRef.value,
997
1036
  containerRect: containerRef.value?.getBoundingClientRect(),
@@ -1000,16 +1039,19 @@ const calculateAutoFitScale = () => {
1000
1039
  minScale: props.minScale,
1001
1040
  maxScale: props.maxScale,
1002
1041
  });
1003
-
1042
+
1004
1043
  if (!props.autoFitWidth || !containerRef.value) {
1005
- console.log('[ImagePreview] calculateAutoFitScale 返回 1 (条件不满足)');
1044
+ console.log("[ImagePreview] calculateAutoFitScale 返回 1 (条件不满足)");
1006
1045
  return 1;
1007
1046
  }
1008
1047
 
1009
1048
  // 使用第一页的图片尺寸作为基准(所有页面使用相同的缩放比例)
1010
1049
  const firstPageSize = imageSizes.get(1);
1011
1050
  if (!firstPageSize || firstPageSize.width === 0) {
1012
- console.log('[ImagePreview] calculateAutoFitScale 返回 1 (第一页尺寸无效)', firstPageSize);
1051
+ console.log(
1052
+ "[ImagePreview] calculateAutoFitScale 返回 1 (第一页尺寸无效)",
1053
+ firstPageSize
1054
+ );
1013
1055
  return 1;
1014
1056
  }
1015
1057
 
@@ -1019,7 +1061,10 @@ const calculateAutoFitScale = () => {
1019
1061
  const containerWidth = containerRect.width - 4;
1020
1062
 
1021
1063
  if (containerWidth <= 0) {
1022
- console.log('[ImagePreview] calculateAutoFitScale 返回 1 (容器宽度无效)', containerWidth);
1064
+ console.log(
1065
+ "[ImagePreview] calculateAutoFitScale 返回 1 (容器宽度无效)",
1066
+ containerWidth
1067
+ );
1023
1068
  return 1;
1024
1069
  }
1025
1070
 
@@ -1030,15 +1075,21 @@ const calculateAutoFitScale = () => {
1030
1075
  const imageWidth = isRotated ? firstPageSize.height : firstPageSize.width;
1031
1076
 
1032
1077
  if (imageWidth <= 0) {
1033
- console.log('[ImagePreview] calculateAutoFitScale 返回 1 (图片宽度无效)', imageWidth);
1078
+ console.log(
1079
+ "[ImagePreview] calculateAutoFitScale 返回 1 (图片宽度无效)",
1080
+ imageWidth
1081
+ );
1034
1082
  return 1;
1035
1083
  }
1036
1084
 
1037
1085
  // 计算缩放比例,使图片宽度完全适应容器宽度
1038
1086
  const calculatedScale = containerWidth / imageWidth;
1039
- const finalScale = Math.max(props.minScale, Math.min(props.maxScale, calculatedScale));
1040
-
1041
- console.log('[ImagePreview] calculateAutoFitScale 计算结果:', {
1087
+ const finalScale = Math.max(
1088
+ props.minScale,
1089
+ Math.min(props.maxScale, calculatedScale)
1090
+ );
1091
+
1092
+ console.log("[ImagePreview] calculateAutoFitScale 计算结果:", {
1042
1093
  containerWidth,
1043
1094
  imageWidth,
1044
1095
  calculatedScale,
@@ -1052,8 +1103,8 @@ const calculateAutoFitScale = () => {
1052
1103
  // 图片加载完成处理
1053
1104
  const onImageLoad = (event: Event, pageNum: number) => {
1054
1105
  const img = event.target as HTMLImageElement;
1055
-
1056
- console.log('[ImagePreview] 图片加载完成:', {
1106
+
1107
+ console.log("[ImagePreview] 图片加载完成:", {
1057
1108
  pageNum,
1058
1109
  naturalWidth: img.naturalWidth,
1059
1110
  naturalHeight: img.naturalHeight,
@@ -1061,7 +1112,7 @@ const onImageLoad = (event: Event, pageNum: number) => {
1061
1112
  isImageReady: isImageReady.value,
1062
1113
  isCalculatingAutoFit: isCalculatingAutoFit.value,
1063
1114
  });
1064
-
1115
+
1065
1116
  // 存储该页的图片尺寸
1066
1117
  imageSizes.set(pageNum, {
1067
1118
  width: img.naturalWidth,
@@ -1070,10 +1121,10 @@ const onImageLoad = (event: Event, pageNum: number) => {
1070
1121
 
1071
1122
  // 如果是第一页且启用自适应宽度,计算并设置初始缩放比例
1072
1123
  if (pageNum === 1 && props.autoFitWidth) {
1073
- console.log('[ImagePreview] 第一页加载完成,开始计算自适应宽度');
1124
+ console.log("[ImagePreview] 第一页加载完成,开始计算自适应宽度");
1074
1125
  // 重置用户缩放标记
1075
1126
  isUserZooming.value = false;
1076
-
1127
+
1077
1128
  // 确保图片是隐藏的(watch 已经设置了,这里再次确认)
1078
1129
  if (!isImageReady.value) {
1079
1130
  isCalculatingAutoFit.value = true;
@@ -1081,7 +1132,7 @@ const onImageLoad = (event: Event, pageNum: number) => {
1081
1132
 
1082
1133
  // 设置超时保护,防止一直显示 loading(最多等待 3 秒)
1083
1134
  const timeoutId = setTimeout(() => {
1084
- console.warn('自适应宽度计算超时,强制显示图片');
1135
+ console.warn("自适应宽度计算超时,强制显示图片");
1085
1136
  isCalculatingAutoFit.value = false;
1086
1137
  isImageReady.value = true;
1087
1138
  }, 3000);
@@ -1092,38 +1143,51 @@ const onImageLoad = (event: Event, pageNum: number) => {
1092
1143
  // 添加小延迟确保容器完全渲染
1093
1144
  setTimeout(() => {
1094
1145
  try {
1095
- console.log('[ImagePreview] onImageLoad: 开始计算自适应宽度...');
1146
+ console.log("[ImagePreview] onImageLoad: 开始计算自适应宽度...");
1096
1147
  const autoScale = calculateAutoFitScale();
1097
- console.log('[ImagePreview] onImageLoad: 自适应宽度计算结果:', {
1148
+ console.log("[ImagePreview] onImageLoad: 自适应宽度计算结果:", {
1098
1149
  autoScale,
1099
1150
  containerRef: !!containerRef.value,
1100
- containerWidth: containerRef.value?.getBoundingClientRect()?.width,
1151
+ containerWidth:
1152
+ containerRef.value?.getBoundingClientRect()?.width,
1101
1153
  firstPageSize: imageSizes.get(1),
1102
1154
  });
1103
-
1155
+
1104
1156
  if (autoScale > 0) {
1105
1157
  scale.value = autoScale;
1106
1158
  initialAutoFitScale.value = autoScale; // 记录初始自适应缩放比例
1107
1159
  // 记录当前容器宽度,用于后续 resize 检查
1108
1160
  if (containerRef.value) {
1109
- lastContainerWidth = containerRef.value.getBoundingClientRect().width;
1161
+ lastContainerWidth =
1162
+ containerRef.value.getBoundingClientRect().width;
1110
1163
  }
1111
- console.log('[ImagePreview] onImageLoad: 缩放比例已设置:', autoScale);
1164
+ console.log(
1165
+ "[ImagePreview] onImageLoad: 缩放比例已设置:",
1166
+ autoScale
1167
+ );
1112
1168
  } else {
1113
- console.warn('[ImagePreview] onImageLoad: 计算出的缩放比例无效:', autoScale);
1169
+ console.warn(
1170
+ "[ImagePreview] onImageLoad: 计算出的缩放比例无效:",
1171
+ autoScale
1172
+ );
1114
1173
  }
1115
1174
  } catch (error) {
1116
- console.error('[ImagePreview] onImageLoad: 计算自适应宽度失败:', error);
1175
+ console.error(
1176
+ "[ImagePreview] onImageLoad: 计算自适应宽度失败:",
1177
+ error
1178
+ );
1117
1179
  } finally {
1118
1180
  // 清除超时保护
1119
1181
  clearTimeout(timeoutId);
1120
- console.log('[ImagePreview] onImageLoad: 更新状态: isCalculatingAutoFit = false, isImageReady = true');
1182
+ console.log(
1183
+ "[ImagePreview] onImageLoad: 更新状态: isCalculatingAutoFit = false, isImageReady = true"
1184
+ );
1121
1185
  // 无论计算结果如何,都要更新状态,避免一直显示 loading
1122
1186
  isCalculatingAutoFit.value = false;
1123
1187
  // 使用 requestAnimationFrame 确保在下一帧显示,避免闪烁
1124
1188
  requestAnimationFrame(() => {
1125
1189
  isImageReady.value = true;
1126
- console.log('[ImagePreview] onImageLoad: 图片已准备好显示');
1190
+ console.log("[ImagePreview] onImageLoad: 图片已准备好显示");
1127
1191
  });
1128
1192
  }
1129
1193
  }, 100); // 增加延迟,确保所有图片都已加载
@@ -1134,7 +1198,7 @@ const onImageLoad = (event: Event, pageNum: number) => {
1134
1198
  isImageReady.value = true;
1135
1199
  isCalculatingAutoFit.value = false;
1136
1200
  }
1137
-
1201
+
1138
1202
  // 如果第一页已经加载完成,且当前页不是第一页,也应用自适应宽度
1139
1203
  if (pageNum > 1 && props.autoFitWidth && initialAutoFitScale.value !== null) {
1140
1204
  // 确保后续页面也使用相同的缩放比例
@@ -1352,7 +1416,7 @@ const renderTextLayer = (pageNum?: number) => {
1352
1416
  // 清除当前页面所有文本块的高亮样式(除了当前文本块)
1353
1417
  // 这样可以防止多个文本块同时高亮的竞态条件
1354
1418
  clearAllHighlights(blockDiv);
1355
-
1419
+
1356
1420
  // 清除跳转高亮标志(如果之前是通过跳转高亮的)
1357
1421
  if (isHighlighted.value) {
1358
1422
  isHighlighted.value = false;
@@ -1436,7 +1500,7 @@ const showAnnotationButtonForBlock = (
1436
1500
 
1437
1501
  try {
1438
1502
  const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1439
-
1503
+
1440
1504
  // 从 blocksData 中查找对应的 content
1441
1505
  const pageBlocksData = getPageBlocksData(currentPage.value);
1442
1506
  const blockData = pageBlocksData.find((block) => {
@@ -1449,7 +1513,7 @@ const showAnnotationButtonForBlock = (
1449
1513
  Math.abs(y2 - bbox[3]) < tolerance
1450
1514
  );
1451
1515
  });
1452
-
1516
+
1453
1517
  if (!blockData) return;
1454
1518
 
1455
1519
  const rect = blockDiv.getBoundingClientRect();
@@ -1537,7 +1601,10 @@ const showAnnotationButtonForBlock = (
1537
1601
  * @param excludeBlockDiv 要排除的文本块(不清除它的样式)
1538
1602
  * @param pageNum 要清除的页码,如果不提供则从 excludeBlockDiv 中获取
1539
1603
  */
1540
- const clearAllHighlights = (excludeBlockDiv?: HTMLElement, pageNum?: number) => {
1604
+ const clearAllHighlights = (
1605
+ excludeBlockDiv?: HTMLElement,
1606
+ pageNum?: number
1607
+ ) => {
1541
1608
  // 确定要清除的页码
1542
1609
  let targetPage = pageNum;
1543
1610
  if (!targetPage && excludeBlockDiv) {
@@ -1571,11 +1638,23 @@ const clearAllHighlights = (excludeBlockDiv?: HTMLElement, pageNum?: number) =>
1571
1638
 
1572
1639
  if (existingAnnotation) {
1573
1640
  // 如果有批注,恢复批注样式(不使用 hover 样式)
1574
- el.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1575
- el.style.setProperty("border", "1px solid rgba(255, 193, 7, 0.7)", "important");
1641
+ el.style.setProperty(
1642
+ "background-color",
1643
+ "rgba(255, 243, 205, 0.5)",
1644
+ "important"
1645
+ );
1646
+ el.style.setProperty(
1647
+ "border",
1648
+ "1px solid rgba(255, 193, 7, 0.7)",
1649
+ "important"
1650
+ );
1576
1651
  el.style.setProperty("border-radius", "3px", "important");
1577
1652
  el.style.setProperty("padding", "1px 3px", "important");
1578
- el.style.setProperty("box-shadow", "0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1653
+ el.style.setProperty(
1654
+ "box-shadow",
1655
+ "0 1px 2px rgba(255, 193, 7, 0.25)",
1656
+ "important"
1657
+ );
1579
1658
  } else {
1580
1659
  // 如果没有批注,清除所有高亮样式
1581
1660
  el.style.backgroundColor = "transparent";
@@ -1639,17 +1718,29 @@ const restoreBlockStyle = (blockDiv: HTMLElement) => {
1639
1718
 
1640
1719
  if (existingAnnotation) {
1641
1720
  // 如果有批注,保持批注样式(使用 !important 确保优先级)
1642
- blockDiv.style.setProperty("background-color", "rgba(255, 243, 205, 0.5)", "important");
1643
- blockDiv.style.setProperty("border", "1px solid rgba(255, 193, 7, 0.7)", "important");
1721
+ blockDiv.style.setProperty(
1722
+ "background-color",
1723
+ "rgba(255, 243, 205, 0.5)",
1724
+ "important"
1725
+ );
1726
+ blockDiv.style.setProperty(
1727
+ "border",
1728
+ "1px solid rgba(255, 193, 7, 0.7)",
1729
+ "important"
1730
+ );
1644
1731
  blockDiv.style.setProperty("border-radius", "3px", "important");
1645
1732
  blockDiv.style.setProperty("padding", "1px 3px", "important");
1646
- blockDiv.style.setProperty("box-shadow", "0 1px 2px rgba(255, 193, 7, 0.25)", "important");
1647
-
1733
+ blockDiv.style.setProperty(
1734
+ "box-shadow",
1735
+ "0 1px 2px rgba(255, 193, 7, 0.25)",
1736
+ "important"
1737
+ );
1738
+
1648
1739
  // 确保元素可见
1649
1740
  blockDiv.style.setProperty("display", "block", "important");
1650
1741
  blockDiv.style.setProperty("visibility", "visible", "important");
1651
1742
  blockDiv.style.setProperty("opacity", "1", "important");
1652
-
1743
+
1653
1744
  console.log("restoreBlockStyle: 已设置批注样式", {
1654
1745
  backgroundColor: blockDiv.style.backgroundColor,
1655
1746
  border: blockDiv.style.border,
@@ -1690,13 +1781,21 @@ const applyHoverStyle = (blockDiv: HTMLElement, hasAnnotation: boolean) => {
1690
1781
  // 有批注时使用 annotationHoverStyle
1691
1782
  const style = props.annotationHoverStyle;
1692
1783
  if (style.backgroundColor) {
1693
- blockDiv.style.setProperty("background-color", style.backgroundColor, "important");
1784
+ blockDiv.style.setProperty(
1785
+ "background-color",
1786
+ style.backgroundColor,
1787
+ "important"
1788
+ );
1694
1789
  }
1695
1790
  if (style.border) {
1696
1791
  blockDiv.style.setProperty("border", style.border, "important");
1697
1792
  }
1698
1793
  if (style.borderRadius) {
1699
- blockDiv.style.setProperty("border-radius", style.borderRadius, "important");
1794
+ blockDiv.style.setProperty(
1795
+ "border-radius",
1796
+ style.borderRadius,
1797
+ "important"
1798
+ );
1700
1799
  }
1701
1800
  if (style.padding) {
1702
1801
  blockDiv.style.setProperty("padding", style.padding, "important");
@@ -1747,12 +1846,7 @@ const hideAnnotationButton = () => {
1747
1846
  const bboxStr = activeBlockDiv.value.dataset.bbox;
1748
1847
  if (bboxStr) {
1749
1848
  try {
1750
- const bbox = JSON.parse(bboxStr) as [
1751
- number,
1752
- number,
1753
- number,
1754
- number
1755
- ];
1849
+ const bbox = JSON.parse(bboxStr) as [number, number, number, number];
1756
1850
  const existingAnnotation = getAnnotationForBlock(bbox);
1757
1851
  if (existingAnnotation) {
1758
1852
  // 如果有批注,恢复批注样式
@@ -1888,8 +1982,10 @@ const openAnnotationInput = (e?: Event) => {
1888
1982
  const existingAnnotation = getAnnotationForBlock(bbox);
1889
1983
  if (existingAnnotation) {
1890
1984
  annotationInput.value = existingAnnotation.content;
1985
+ currentEditingAnnotation.value = existingAnnotation; // 保存当前编辑的批注信息
1891
1986
  } else {
1892
1987
  annotationInput.value = "";
1988
+ currentEditingAnnotation.value = null; // 新批注,清空编辑信息
1893
1989
  }
1894
1990
 
1895
1991
  currentAnnotationBlock.value = {
@@ -2002,9 +2098,10 @@ const adjustAnnotationPopupPosition = () => {
2002
2098
  */
2003
2099
  const closeAnnotationInput = () => {
2004
2100
  currentAnnotationBlock.value = null;
2101
+ currentEditingAnnotation.value = null; // 清空当前编辑的批注信息
2005
2102
  annotationInput.value = "";
2006
2103
  showAnnotationPopup.value = false;
2007
-
2104
+
2008
2105
  // 关闭批注输入弹窗后,恢复文本块的样式
2009
2106
  if (activeBlockDiv.value && !isHighlighted.value) {
2010
2107
  restoreBlockStyle(activeBlockDiv.value);
@@ -2043,13 +2140,16 @@ const saveAnnotation = () => {
2043
2140
  const existingAnnotation = getAnnotationForBlock(bbox);
2044
2141
 
2045
2142
  const annotation: AnnotationInfo = {
2046
- id:
2047
- existingAnnotation?.id || "",
2143
+ id: existingAnnotation?.id || "",
2048
2144
  blockBbox: bbox,
2049
2145
  blockContent: content,
2050
2146
  blockPage: currentPage.value,
2051
2147
  content: annotationContent,
2052
2148
  createTime: existingAnnotation?.createTime || Date.now(),
2149
+ // 保留用户信息(如果存在)
2150
+ username: existingAnnotation?.username,
2151
+ avatar: existingAnnotation?.avatar,
2152
+ userId: existingAnnotation?.userId,
2053
2153
  };
2054
2154
 
2055
2155
  if (existingAnnotation) {
@@ -2075,9 +2175,9 @@ const saveAnnotation = () => {
2075
2175
  */
2076
2176
  const handleScroll = (e: Event) => {
2077
2177
  const container = e.target as HTMLElement;
2078
-
2178
+
2079
2179
  // 检查是否是同步滚动触发的
2080
- const isSyncing = container?.dataset?.syncingScroll === 'true';
2180
+ const isSyncing = container?.dataset?.syncingScroll === "true";
2081
2181
  if (isSyncing) {
2082
2182
  // 即使是被同步滚动触发的,也应该立即更新页码(但不触发翻页动画)
2083
2183
  // 使用 requestAnimationFrame 确保在浏览器渲染后立即更新
@@ -2096,7 +2196,7 @@ const handleScroll = (e: Event) => {
2096
2196
  // 如果没有启用滚动翻页,立即清除标记
2097
2197
  delete container.dataset.syncingScroll;
2098
2198
  }
2099
-
2199
+
2100
2200
  // 同步滚动时,不执行其他逻辑(如隐藏批注按钮等)
2101
2201
  return;
2102
2202
  }
@@ -2137,7 +2237,6 @@ const handleScroll = (e: Event) => {
2137
2237
  }
2138
2238
  };
2139
2239
 
2140
-
2141
2240
  // 记录上次滚动位置,用于判断滚动方向
2142
2241
  let lastScrollTop = 0;
2143
2242
 
@@ -2260,7 +2359,7 @@ const highlightPosition = (
2260
2359
  activeBlockDiv.value = null;
2261
2360
  }
2262
2361
  isHighlighted.value = false;
2263
-
2362
+
2264
2363
  // 如果之前有高亮,触发高亮清除事件
2265
2364
  if (hadHighlight) {
2266
2365
  emit("highlight-clear");
@@ -2351,13 +2450,13 @@ const highlightPosition = (
2351
2450
  // 保存原来的 transition 和 transform,临时禁用以避免与动画冲突
2352
2451
  const originalTransition = elementRef.style.transition || "";
2353
2452
  const originalTransform = elementRef.style.transform || "";
2354
-
2453
+
2355
2454
  // 先移除可能存在的动画类,确保可以重新触发
2356
2455
  elementRef.classList.remove("highlight-animated");
2357
-
2456
+
2358
2457
  // 强制浏览器重新计算样式(触发重排),确保动画可以重新开始
2359
2458
  void elementRef.offsetHeight;
2360
-
2459
+
2361
2460
  // 使用 requestAnimationFrame 确保动画正确触发
2362
2461
  requestAnimationFrame(() => {
2363
2462
  // 临时禁用 transition,避免与动画的 transform 冲突
@@ -2365,11 +2464,12 @@ const highlightPosition = (
2365
2464
  // 清除内联 transform,让动画的 transform 生效
2366
2465
  elementRef.style.transform = "";
2367
2466
  elementRef.style.transformOrigin = "center center";
2368
-
2467
+
2369
2468
  // 使用内联样式直接设置动画(优先级最高)
2370
- elementRef.style.animation = "highlightPulse 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 2";
2469
+ elementRef.style.animation =
2470
+ "highlightPulse 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 2";
2371
2471
  elementRef.style.animationFillMode = "forwards";
2372
-
2472
+
2373
2473
  // 调试:检查动画是否应用
2374
2474
  const computedStyle = window.getComputedStyle(elementRef);
2375
2475
  console.log("跳转高亮动画已应用:", {
@@ -2379,14 +2479,14 @@ const highlightPosition = (
2379
2479
  transform: computedStyle.transform,
2380
2480
  transformOrigin: computedStyle.transformOrigin,
2381
2481
  });
2382
-
2482
+
2383
2483
  // 监听动画开始事件(用于调试)
2384
2484
  const handleAnimationStart = (e: AnimationEvent) => {
2385
2485
  console.log("动画开始", e);
2386
2486
  elementRef.removeEventListener("animationstart", handleAnimationStart);
2387
2487
  };
2388
2488
  elementRef.addEventListener("animationstart", handleAnimationStart);
2389
-
2489
+
2390
2490
  // 监听动画结束事件,移除动画并恢复样式(确保动画可以重复触发)
2391
2491
  const handleAnimationEnd = (e: AnimationEvent) => {
2392
2492
  console.log("动画结束", e);
@@ -2411,7 +2511,7 @@ const highlightPosition = (
2411
2511
  const isVisible = isElementVisible(elementRef, containerRef.value);
2412
2512
  if (!isVisible) {
2413
2513
  // 标记这是定位滚动,不应该被同步滚动干扰
2414
- containerRef.value.dataset.pageScrolling = 'true';
2514
+ containerRef.value.dataset.pageScrolling = "true";
2415
2515
  elementRef.scrollIntoView({ behavior: "smooth", block: "center" });
2416
2516
  // 延迟清除标记,确保滚动完成
2417
2517
  setTimeout(() => {
@@ -2480,7 +2580,9 @@ const jumpToPosition = (
2480
2580
  retryCount++;
2481
2581
  setTimeout(tryHighlight, retryDelay);
2482
2582
  } else {
2483
- console.warn(`无法找到并高亮指定位置: 页码 ${pageNum}, bbox: [${bbox.join(", ")}]`);
2583
+ console.warn(
2584
+ `无法找到并高亮指定位置: 页码 ${pageNum}, bbox: [${bbox.join(", ")}]`
2585
+ );
2484
2586
  }
2485
2587
  };
2486
2588
 
@@ -2590,7 +2692,7 @@ const handleContainerResize = () => {
2590
2692
  lastContainerWidth = currentWidth;
2591
2693
  }
2592
2694
 
2593
- console.log('[ImagePreview] handleContainerResize 被调用:', {
2695
+ console.log("[ImagePreview] handleContainerResize 被调用:", {
2594
2696
  autoFitWidth: props.autoFitWidth,
2595
2697
  isUserZooming: isUserZooming.value,
2596
2698
  isImageReady: isImageReady.value,
@@ -2609,23 +2711,25 @@ const handleContainerResize = () => {
2609
2711
  }
2610
2712
 
2611
2713
  // 宽度变化时不显示 loading,只更新缩放比例(避免看起来像重新加载)
2612
- console.log('[ImagePreview] handleContainerResize: 开始重新计算');
2714
+ console.log("[ImagePreview] handleContainerResize: 开始重新计算");
2613
2715
 
2614
2716
  // 立即计算并应用新的缩放比例,避免过渡期间露出底色
2615
2717
  // 使用 requestAnimationFrame 确保在浏览器重绘前更新
2616
2718
  requestAnimationFrame(() => {
2617
2719
  try {
2618
- console.log('[ImagePreview] handleContainerResize: 开始计算自适应宽度...');
2720
+ console.log(
2721
+ "[ImagePreview] handleContainerResize: 开始计算自适应宽度..."
2722
+ );
2619
2723
  const newScale = calculateAutoFitScale();
2620
- console.log('[ImagePreview] handleContainerResize: 计算结果:', newScale);
2621
-
2724
+ console.log("[ImagePreview] handleContainerResize: 计算结果:", newScale);
2725
+
2622
2726
  if (newScale > 0) {
2623
2727
  // 即使变化很小也立即更新,确保过渡期间图片始终填满容器
2624
2728
  scale.value = newScale;
2625
2729
  initialAutoFitScale.value = newScale;
2626
2730
  }
2627
2731
  } catch (error) {
2628
- console.error('[ImagePreview] handleContainerResize: 计算失败:', error);
2732
+ console.error("[ImagePreview] handleContainerResize: 计算失败:", error);
2629
2733
  }
2630
2734
 
2631
2735
  // 在过渡动画完成后再次检查,确保最终状态正确(处理过渡动画期间的连续变化)
@@ -2637,10 +2741,13 @@ const handleContainerResize = () => {
2637
2741
  initialAutoFitScale.value = finalScale;
2638
2742
  }
2639
2743
  } catch (error) {
2640
- console.error('[ImagePreview] handleContainerResize: 最终计算失败:', error);
2744
+ console.error(
2745
+ "[ImagePreview] handleContainerResize: 最终计算失败:",
2746
+ error
2747
+ );
2641
2748
  } finally {
2642
2749
  // 计算完成,重置标记(不改变图片显示状态,因为宽度变化时不应该显示loading)
2643
- console.log('[ImagePreview] handleContainerResize: 更新状态完成');
2750
+ console.log("[ImagePreview] handleContainerResize: 更新状态完成");
2644
2751
  isResizing = false; // 重置标记
2645
2752
  }
2646
2753
  }, 350); // 350ms 延迟,略大于过渡动画时间(300ms),确保过渡完成后稳定
@@ -2659,14 +2766,14 @@ onMounted(() => {
2659
2766
  // 隐藏图片,显示 loading
2660
2767
  isImageReady.value = false;
2661
2768
  isCalculatingAutoFit.value = true;
2662
-
2769
+
2663
2770
  // 设置超时保护,防止一直显示 loading(最多等待 3 秒)
2664
2771
  const timeoutId = setTimeout(() => {
2665
- console.warn('自适应宽度计算超时,强制显示图片');
2772
+ console.warn("自适应宽度计算超时,强制显示图片");
2666
2773
  isCalculatingAutoFit.value = false;
2667
2774
  isImageReady.value = true;
2668
2775
  }, 3000);
2669
-
2776
+
2670
2777
  nextTick(() => {
2671
2778
  nextTick(() => {
2672
2779
  setTimeout(() => {
@@ -2677,7 +2784,7 @@ onMounted(() => {
2677
2784
  initialAutoFitScale.value = autoScale;
2678
2785
  }
2679
2786
  } catch (error) {
2680
- console.warn('计算自适应宽度失败:', error);
2787
+ console.warn("计算自适应宽度失败:", error);
2681
2788
  } finally {
2682
2789
  // 清除超时保护
2683
2790
  clearTimeout(timeoutId);
@@ -2698,7 +2805,7 @@ onMounted(() => {
2698
2805
 
2699
2806
  // 监听容器尺寸变化(用于响应外部收起/展开操作)
2700
2807
  nextTick(() => {
2701
- if (containerRef.value && typeof ResizeObserver !== 'undefined') {
2808
+ if (containerRef.value && typeof ResizeObserver !== "undefined") {
2702
2809
  resizeObserver = new ResizeObserver((entries) => {
2703
2810
  // 使用防抖,避免频繁触发
2704
2811
  if (resizeDebounceTimer) {
@@ -2715,7 +2822,7 @@ onMounted(() => {
2715
2822
  resizeObserver.observe(containerRef.value);
2716
2823
  } else {
2717
2824
  // 降级方案:监听窗口大小变化
2718
- window.addEventListener('resize', handleContainerResize);
2825
+ window.addEventListener("resize", handleContainerResize);
2719
2826
  }
2720
2827
  });
2721
2828
  });
@@ -2745,7 +2852,7 @@ onBeforeUnmount(() => {
2745
2852
  resizeObserver = null;
2746
2853
  }
2747
2854
  // 移除窗口 resize 监听器(降级方案)
2748
- window.removeEventListener('resize', handleContainerResize);
2855
+ window.removeEventListener("resize", handleContainerResize);
2749
2856
  });
2750
2857
 
2751
2858
  // 获取当前高亮位置信息(用于连接线功能)
@@ -3006,7 +3113,7 @@ defineExpose({
3006
3113
  width: 320px;
3007
3114
  pointer-events: auto;
3008
3115
  background-color: #fff;
3009
- border-radius: 8px;
3116
+ border-radius: 6px;
3010
3117
  box-shadow: 0 4px 16px rgb(0 0 0 / 20%);
3011
3118
  animation: fade-in 0.2s ease;
3012
3119
  overflow: hidden;
@@ -3016,7 +3123,33 @@ defineExpose({
3016
3123
  align-items: center;
3017
3124
  justify-content: space-between;
3018
3125
  padding: 12px 16px;
3019
- border-bottom: 1px solid #e5e7eb;
3126
+ padding-bottom: 8px;
3127
+
3128
+ .annotation-input-header-title {
3129
+ position: relative;
3130
+ max-height: 21px;
3131
+ padding-left: 12px; // 增加左侧内边距,为竖线留出空间
3132
+ padding-right: 6px;
3133
+ color: #8f959e;
3134
+ font-size: 12px;
3135
+ line-height: 20px;
3136
+ overflow: hidden;
3137
+ white-space: nowrap;
3138
+ -o-text-overflow: ellipsis;
3139
+ text-overflow: ellipsis;
3140
+
3141
+ // 左侧竖线,表示引用
3142
+ &::before {
3143
+ content: "";
3144
+ position: absolute;
3145
+ left: 0;
3146
+ top: 2px;
3147
+ width: 2px;
3148
+ height: 16px;
3149
+ background-color: #bbbfc4 !important;
3150
+ border-radius: 1px;
3151
+ }
3152
+ }
3020
3153
 
3021
3154
  .annotation-input-title {
3022
3155
  font-size: 14px;
@@ -3024,6 +3157,27 @@ defineExpose({
3024
3157
  color: #1d2129;
3025
3158
  }
3026
3159
 
3160
+ .annotation-header-user-info {
3161
+ display: flex;
3162
+ align-items: center;
3163
+ gap: 8px;
3164
+ flex: 1;
3165
+ }
3166
+
3167
+ .annotation-user-avatar {
3168
+ width: 24px;
3169
+ height: 24px;
3170
+ border-radius: 50%;
3171
+ object-fit: cover;
3172
+ background-color: #f0f0f0;
3173
+ }
3174
+
3175
+ .annotation-username {
3176
+ font-size: 14px;
3177
+ font-weight: 500;
3178
+ color: #1d2129;
3179
+ }
3180
+
3027
3181
  .annotation-close-btn {
3028
3182
  display: flex;
3029
3183
  align-items: center;
@@ -3037,6 +3191,7 @@ defineExpose({
3037
3191
  border-radius: 4px;
3038
3192
  color: #86909c;
3039
3193
  transition: all 0.2s;
3194
+ margin-left: 10px;
3040
3195
 
3041
3196
  &:hover {
3042
3197
  background-color: #f0f0f0;
@@ -3046,10 +3201,12 @@ defineExpose({
3046
3201
  }
3047
3202
 
3048
3203
  .annotation-input-content {
3049
- padding: 12px 16px;
3204
+ padding: 0px 16px;
3050
3205
 
3051
3206
  .annotation-textarea {
3052
3207
  width: 100%;
3208
+ margin: 12px 0px;
3209
+ margin-bottom: 16px;
3053
3210
  }
3054
3211
  }
3055
3212
 
@@ -3059,10 +3216,20 @@ defineExpose({
3059
3216
  justify-content: flex-end;
3060
3217
  gap: 8px;
3061
3218
  padding: 12px 16px;
3062
- border-top: 1px solid #e5e7eb;
3063
3219
  }
3064
3220
  }
3065
3221
 
3222
+ .annotation-input-popup:before {
3223
+ content: "";
3224
+ position: absolute;
3225
+ top: -2px;
3226
+ left: -1px;
3227
+ right: -1px;
3228
+ border-top: 8px solid rgb(255, 198, 10);
3229
+ border-top-left-radius: 6px;
3230
+ border-top-right-radius: 6px;
3231
+ }
3232
+
3066
3233
  // 有批注的文本块样式
3067
3234
  .text-block.has-annotation {
3068
3235
  position: relative;