|
@@ -80,20 +80,20 @@
|
|
|
@wheel="handleWheel">
|
|
@wheel="handleWheel">
|
|
|
<div v-if="currentFile" class="preview-content">
|
|
<div v-if="currentFile" class="preview-content">
|
|
|
<!-- 图片预览 - 添加缩放拖拽 -->
|
|
<!-- 图片预览 - 添加缩放拖拽 -->
|
|
|
- <img
|
|
|
|
|
- v-if="isImage(currentFile)"
|
|
|
|
|
- :src="getCurrentPageImage(currentFile)"
|
|
|
|
|
- :alt="currentFile.originalName"
|
|
|
|
|
- class="preview-image"
|
|
|
|
|
- :style="{
|
|
|
|
|
- transform: `scale(${zoomLevel}) translate(${dragOffset.x}px, ${dragOffset.y}px)`,
|
|
|
|
|
- cursor: isDragging ? 'grabbing' : 'grab'
|
|
|
|
|
- }"
|
|
|
|
|
- @load="handleImageLoad"
|
|
|
|
|
- @error="handlePreviewError"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <!-- 不支持预览的文件类型 -->
|
|
|
|
|
|
|
+ <div class="image-wrapper" ref="imageWrapper" v-if="isImage(currentFile)">
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="getCurrentPageImage(currentFile)"
|
|
|
|
|
+ :alt="currentFile.originalName"
|
|
|
|
|
+ class="preview-image"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ transform: `scale(${zoomLevel}) translate(${dragOffset.x}px, ${dragOffset.y}px)`,
|
|
|
|
|
+ cursor: isDragging ? 'grabbing' : 'grab'
|
|
|
|
|
+ }"
|
|
|
|
|
+ @load="handleImageLoad"
|
|
|
|
|
+ @error="handlePreviewError"
|
|
|
|
|
+ ref="previewImage"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
<div v-else class="unsupported-preview">
|
|
<div v-else class="unsupported-preview">
|
|
|
<div class="file-type-large" :class="getFileTypeClass(currentFile)">
|
|
<div class="file-type-large" :class="getFileTypeClass(currentFile)">
|
|
|
{{ getFileTypeIcon(currentFile) }}
|
|
{{ getFileTypeIcon(currentFile) }}
|
|
@@ -279,6 +279,8 @@ const isDragging = ref(false)
|
|
|
const lastDragPos = ref({ x: 0, y: 0 })
|
|
const lastDragPos = ref({ x: 0, y: 0 })
|
|
|
const resultContainer = ref<HTMLElement | null>(null)
|
|
const resultContainer = ref<HTMLElement | null>(null)
|
|
|
const previewContainer = ref<HTMLElement | null>(null)
|
|
const previewContainer = ref<HTMLElement | null>(null)
|
|
|
|
|
+const previewImage = ref<HTMLImageElement | null>(null)
|
|
|
|
|
+const imageWrapper = ref<HTMLElement | null>(null)
|
|
|
const textareas = ref<HTMLTextAreaElement[]>([])
|
|
const textareas = ref<HTMLTextAreaElement[]>([])
|
|
|
|
|
|
|
|
// 计算属性
|
|
// 计算属性
|
|
@@ -340,16 +342,38 @@ const selectFile = async (file: FileRecord): Promise<void> => {
|
|
|
// 图片加载完成后的处理
|
|
// 图片加载完成后的处理
|
|
|
const handleImageLoad = (): void => {
|
|
const handleImageLoad = (): void => {
|
|
|
// 确保图片在可视区域内
|
|
// 确保图片在可视区域内
|
|
|
- resetZoom()
|
|
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ resetZoom()
|
|
|
|
|
+ centerImage()
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 居中图片
|
|
|
|
|
+const centerImage = (): void => {
|
|
|
|
|
+ if (!previewImage.value || !imageWrapper.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const container = imageWrapper.value
|
|
|
|
|
+ const img = previewImage.value
|
|
|
|
|
+
|
|
|
|
|
+ // 计算居中偏移
|
|
|
|
|
+ const containerRect = container.getBoundingClientRect()
|
|
|
|
|
+ const imgRect = img.getBoundingClientRect()
|
|
|
|
|
+
|
|
|
|
|
+ // 如果图片比容器小,居中显示
|
|
|
|
|
+ if (imgRect.width < containerRect.width && imgRect.height < containerRect.height) {
|
|
|
|
|
+ dragOffset.value = { x: 0, y: 0 }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 缩放功能
|
|
// 缩放功能
|
|
|
const zoomIn = (): void => {
|
|
const zoomIn = (): void => {
|
|
|
zoomLevel.value = Math.min(zoomLevel.value + 0.1, 3)
|
|
zoomLevel.value = Math.min(zoomLevel.value + 0.1, 3)
|
|
|
|
|
+ constrainDragOffset() // 限制拖拽范围
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const zoomOut = (): void => {
|
|
const zoomOut = (): void => {
|
|
|
zoomLevel.value = Math.max(zoomLevel.value - 0.1, 0.5)
|
|
zoomLevel.value = Math.max(zoomLevel.value - 0.1, 0.5)
|
|
|
|
|
+ constrainDragOffset() // 限制拖拽范围
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const resetZoom = (): void => {
|
|
const resetZoom = (): void => {
|
|
@@ -357,6 +381,28 @@ const resetZoom = (): void => {
|
|
|
dragOffset.value = { x: 0, y: 0 }
|
|
dragOffset.value = { x: 0, y: 0 }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 限制拖拽范围,防止图片被拖出可视区域
|
|
|
|
|
+const constrainDragOffset = (): void => {
|
|
|
|
|
+ if (!previewImage.value || !imageWrapper.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const container = imageWrapper.value
|
|
|
|
|
+ const img = previewImage.value
|
|
|
|
|
+
|
|
|
|
|
+ const containerRect = container.getBoundingClientRect()
|
|
|
|
|
+ const imgRect = {
|
|
|
|
|
+ width: img.naturalWidth * zoomLevel.value,
|
|
|
|
|
+ height: img.naturalHeight * zoomLevel.value
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算最大允许的拖拽偏移
|
|
|
|
|
+ const maxX = Math.max(0, (imgRect.width - containerRect.width) / 2)
|
|
|
|
|
+ const maxY = Math.max(0, (imgRect.height - containerRect.height) / 2)
|
|
|
|
|
+
|
|
|
|
|
+ // 限制拖拽范围
|
|
|
|
|
+ dragOffset.value.x = Math.max(-maxX, Math.min(maxX, dragOffset.value.x))
|
|
|
|
|
+ dragOffset.value.y = Math.max(-maxY, Math.min(maxY, dragOffset.value.y))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 拖拽功能
|
|
// 拖拽功能
|
|
|
const startDrag = (event: MouseEvent): void => {
|
|
const startDrag = (event: MouseEvent): void => {
|
|
|
if (!currentFile.value || !isImage(currentFile.value)) return
|
|
if (!currentFile.value || !isImage(currentFile.value)) return
|
|
@@ -375,6 +421,9 @@ const doDrag = (event: MouseEvent): void => {
|
|
|
dragOffset.value.x += deltaX
|
|
dragOffset.value.x += deltaX
|
|
|
dragOffset.value.y += deltaY
|
|
dragOffset.value.y += deltaY
|
|
|
|
|
|
|
|
|
|
+ // 限制拖拽范围
|
|
|
|
|
+ constrainDragOffset()
|
|
|
|
|
+
|
|
|
lastDragPos.value = { x: event.clientX, y: event.clientY }
|
|
lastDragPos.value = { x: event.clientX, y: event.clientY }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -425,11 +474,6 @@ const autoResizeTextarea = (event: { target: EventTarget | null }): void => {
|
|
|
// 设置新高度,但限制最大高度
|
|
// 设置新高度,但限制最大高度
|
|
|
const newHeight = Math.min(textarea.scrollHeight, 300) // 最大300px
|
|
const newHeight = Math.min(textarea.scrollHeight, 300) // 最大300px
|
|
|
textarea.style.height = `${newHeight}px`
|
|
textarea.style.height = `${newHeight}px`
|
|
|
-
|
|
|
|
|
- // 确保结果容器可以滚动
|
|
|
|
|
- if (resultContainer.value) {
|
|
|
|
|
- resultContainer.value.style.overflowY = 'auto'
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const saveTextEdit = async (): Promise<void> => {
|
|
const saveTextEdit = async (): Promise<void> => {
|
|
@@ -526,6 +570,9 @@ const startOcr = async (): Promise<void> => {
|
|
|
|
|
|
|
|
// 识别成功后重置图片位置,确保在可视区域内
|
|
// 识别成功后重置图片位置,确保在可视区域内
|
|
|
resetZoom()
|
|
resetZoom()
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ centerImage()
|
|
|
|
|
+ })
|
|
|
} else {
|
|
} else {
|
|
|
throw new Error('OCR识别失败')
|
|
throw new Error('OCR识别失败')
|
|
|
}
|
|
}
|
|
@@ -794,6 +841,7 @@ onUnmounted(() => {
|
|
|
gap: 1px;
|
|
gap: 1px;
|
|
|
background: #e9ecef;
|
|
background: #e9ecef;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
+ min-height: 0; /* 重要:允许内容缩小 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.file-panel,
|
|
.file-panel,
|
|
@@ -802,6 +850,7 @@ onUnmounted(() => {
|
|
|
background: white;
|
|
background: white;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ min-height: 0; /* 重要:允许内容缩小 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.panel-header {
|
|
.panel-header {
|
|
@@ -860,6 +909,7 @@ onUnmounted(() => {
|
|
|
overflow: hidden; /* 防止内部溢出 */
|
|
overflow: hidden; /* 防止内部溢出 */
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ min-height: 0; /* 重要:允许内容缩小 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.file-preview {
|
|
.file-preview {
|
|
@@ -879,6 +929,25 @@ onUnmounted(() => {
|
|
|
cursor: grabbing;
|
|
cursor: grabbing;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.preview-content {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.image-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.preview-image {
|
|
.preview-image {
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
max-height: 100%;
|
|
max-height: 100%;
|
|
@@ -887,6 +956,7 @@ onUnmounted(() => {
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
transition: transform 0.1s ease;
|
|
transition: transform 0.1s ease;
|
|
|
transform-origin: center center;
|
|
transform-origin: center center;
|
|
|
|
|
+ position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.file-list {
|
|
.file-list {
|
|
@@ -986,10 +1056,19 @@ onUnmounted(() => {
|
|
|
color: #6c757d;
|
|
color: #6c757d;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 修复识别结果滚动问题 */
|
|
|
.ocr-result {
|
|
.ocr-result {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- overflow-y: auto;
|
|
|
|
|
- padding: 1rem;
|
|
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 0; /* 重要:允许内容缩小 */
|
|
|
|
|
+ overflow: hidden; /* 外层隐藏滚动 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.result-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto; /* 内容区域滚动 */
|
|
|
|
|
+ padding: 0 1rem 1rem 1rem;
|
|
|
min-height: 0; /* 重要:允许内容缩小 */
|
|
min-height: 0; /* 重要:允许内容缩小 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -997,6 +1076,11 @@ onUnmounted(() => {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
padding: 2rem;
|
|
padding: 2rem;
|
|
|
color: #6c757d;
|
|
color: #6c757d;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.spinner {
|
|
.spinner {
|
|
@@ -1019,10 +1103,6 @@ onUnmounted(() => {
|
|
|
margin-top: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.result-content {
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
.text-block {
|
|
.text-block {
|
|
|
margin-bottom: 1rem;
|
|
margin-bottom: 1rem;
|
|
|
padding: 0.5rem;
|
|
padding: 0.5rem;
|
|
@@ -1181,6 +1261,11 @@ onUnmounted(() => {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
padding: 2rem;
|
|
padding: 2rem;
|
|
|
color: #6c757d;
|
|
color: #6c757d;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.result-summary {
|
|
.result-summary {
|