@koi-br/ocr-web-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +754 -0
- package/dist/index.cjs.js +538 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.esm.js +85525 -0
- package/dist/preview/PdfPreview.vue.d.ts +2 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/style.css +1 -0
- package/package.json +52 -0
- package/preview/ImagePreview.vue +235 -0
- package/preview/PdfPreview.vue +2571 -0
- package/preview/docxPreview.vue +216 -0
- package/preview/index.vue +317 -0
- package/preview/ofdPreview.vue +107 -0
- package/preview/tifPreview.vue +362 -0
- package/preview/xlsxPreview.vue +168 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="preview-container" @wheel.prevent="handleWheel">
|
|
3
|
+
<div v-if="isLoading" class="loading-container">
|
|
4
|
+
<div>正在加载TIFF文件...</div>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-else-if="error" class="preview-error-container">
|
|
7
|
+
<div>{{ error }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div v-else class="img-container">
|
|
10
|
+
<!-- 工具栏 -->
|
|
11
|
+
<div class="preview-toolbar preview-toolbar-right">
|
|
12
|
+
<div class="toolbar-group">
|
|
13
|
+
<span class="scale-text">
|
|
14
|
+
{{ Math.round(scale * 100) }}%
|
|
15
|
+
</span>
|
|
16
|
+
<div class="toolbar-divider"></div>
|
|
17
|
+
<a-tooltip mini position="bottom" content="重置">
|
|
18
|
+
<a-button
|
|
19
|
+
size="small"
|
|
20
|
+
type="outline"
|
|
21
|
+
@click="reset"
|
|
22
|
+
>
|
|
23
|
+
<RefreshCcw :size="16" />
|
|
24
|
+
</a-button>
|
|
25
|
+
</a-tooltip>
|
|
26
|
+
<a-tooltip
|
|
27
|
+
v-if="originalId"
|
|
28
|
+
mini
|
|
29
|
+
position="bottom"
|
|
30
|
+
content="查看原图"
|
|
31
|
+
>
|
|
32
|
+
<a-button
|
|
33
|
+
size="small"
|
|
34
|
+
type="outline"
|
|
35
|
+
@click="original"
|
|
36
|
+
>
|
|
37
|
+
<Maximize2 :size="16" />
|
|
38
|
+
</a-button>
|
|
39
|
+
</a-tooltip>
|
|
40
|
+
<a-tooltip mini position="bottom" content="缩小">
|
|
41
|
+
<a-button
|
|
42
|
+
size="small"
|
|
43
|
+
type="outline"
|
|
44
|
+
:disabled="scale <= minScale"
|
|
45
|
+
@click="zoom(-0.1)"
|
|
46
|
+
>
|
|
47
|
+
<ZoomOut :size="16" />
|
|
48
|
+
</a-button>
|
|
49
|
+
</a-tooltip>
|
|
50
|
+
<a-tooltip mini position="bottom" content="放大">
|
|
51
|
+
<a-button
|
|
52
|
+
size="small"
|
|
53
|
+
type="outline"
|
|
54
|
+
:disabled="scale >= maxScale"
|
|
55
|
+
@click="zoom(0.1)"
|
|
56
|
+
>
|
|
57
|
+
<ZoomIn :size="16" />
|
|
58
|
+
</a-button>
|
|
59
|
+
</a-tooltip>
|
|
60
|
+
<a-tooltip
|
|
61
|
+
v-if="isDownload"
|
|
62
|
+
mini
|
|
63
|
+
position="bottom"
|
|
64
|
+
content="下载"
|
|
65
|
+
>
|
|
66
|
+
<a-button
|
|
67
|
+
size="small"
|
|
68
|
+
type="outline"
|
|
69
|
+
@click="emit('download')"
|
|
70
|
+
>
|
|
71
|
+
<Download :size="16" />
|
|
72
|
+
</a-button>
|
|
73
|
+
</a-tooltip>
|
|
74
|
+
<div class="toolbar-divider"></div>
|
|
75
|
+
<a-tooltip mini position="bottom" content="向左旋转">
|
|
76
|
+
<a-button
|
|
77
|
+
size="small"
|
|
78
|
+
type="outline"
|
|
79
|
+
@click="rotateImage('left')"
|
|
80
|
+
>
|
|
81
|
+
<RotateCw :size="16" />
|
|
82
|
+
</a-button>
|
|
83
|
+
</a-tooltip>
|
|
84
|
+
<a-tooltip mini position="bottom" content="向右旋转">
|
|
85
|
+
<a-button
|
|
86
|
+
size="small"
|
|
87
|
+
type="outline"
|
|
88
|
+
@click="rotateImage('right')"
|
|
89
|
+
>
|
|
90
|
+
<RotateCcw :size="16" />
|
|
91
|
+
</a-button>
|
|
92
|
+
</a-tooltip>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- 图片容器 -->
|
|
97
|
+
<div
|
|
98
|
+
class="relative w-full h-full flex items-center justify-center overflow-hidden"
|
|
99
|
+
@mousedown="startPan"
|
|
100
|
+
@mousemove="pan"
|
|
101
|
+
@mouseup="stopPan"
|
|
102
|
+
@mouseleave="stopPan"
|
|
103
|
+
>
|
|
104
|
+
<img
|
|
105
|
+
ref="imgRef"
|
|
106
|
+
:src="imgDataUrl"
|
|
107
|
+
alt="TIFF Preview"
|
|
108
|
+
class="w-full"
|
|
109
|
+
:style="{
|
|
110
|
+
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${scale})`,
|
|
111
|
+
cursor: isPanning ? 'grabbing' : 'grab'
|
|
112
|
+
}"
|
|
113
|
+
@load="onImageLoad"
|
|
114
|
+
@error="onImageError"
|
|
115
|
+
@contextmenu.prevent
|
|
116
|
+
@mousedown.prevent
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
122
|
+
|
|
123
|
+
<script setup>
|
|
124
|
+
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
|
|
125
|
+
import { ZoomIn, ZoomOut, RefreshCcw, RotateCw, Download, Maximize2 } from 'lucide-vue-next';
|
|
126
|
+
|
|
127
|
+
const props = defineProps({
|
|
128
|
+
url: {
|
|
129
|
+
type: String,
|
|
130
|
+
required: true
|
|
131
|
+
},
|
|
132
|
+
minScale: {
|
|
133
|
+
type: Number,
|
|
134
|
+
default: 0.1
|
|
135
|
+
},
|
|
136
|
+
maxScale: {
|
|
137
|
+
type: Number,
|
|
138
|
+
default: 5
|
|
139
|
+
},
|
|
140
|
+
clickStep: {
|
|
141
|
+
type: Number,
|
|
142
|
+
default: 0.25
|
|
143
|
+
},
|
|
144
|
+
wheelStep: {
|
|
145
|
+
type: Number,
|
|
146
|
+
default: 0.1
|
|
147
|
+
},
|
|
148
|
+
originalId: {
|
|
149
|
+
type: String,
|
|
150
|
+
default: ''
|
|
151
|
+
},
|
|
152
|
+
isDownload: {
|
|
153
|
+
type: Boolean,
|
|
154
|
+
default: false
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
const emit = defineEmits(['load', 'error', 'original', 'download']);
|
|
158
|
+
|
|
159
|
+
const isLoading = ref(false);
|
|
160
|
+
const error = ref('');
|
|
161
|
+
const imgDataUrl = ref('');
|
|
162
|
+
const imgRef = ref(null);
|
|
163
|
+
|
|
164
|
+
// 缩放、旋转和拖拽相关状态
|
|
165
|
+
const rotation = ref(0);
|
|
166
|
+
const scale = ref(1);
|
|
167
|
+
const position = ref({ x: 0, y: 0 });
|
|
168
|
+
const isPanning = ref(false);
|
|
169
|
+
const lastPosition = ref({ x: 0, y: 0 });
|
|
170
|
+
|
|
171
|
+
// 旋转功能
|
|
172
|
+
const rotateImage = direction => {
|
|
173
|
+
if (direction === 'left') {
|
|
174
|
+
rotation.value = (rotation.value - 90) % 360;
|
|
175
|
+
} else {
|
|
176
|
+
rotation.value = (rotation.value + 90) % 360;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 缩放功能
|
|
181
|
+
const zoom = (delta, isWheel = false) => {
|
|
182
|
+
const step = isWheel ? props.wheelStep : props.clickStep;
|
|
183
|
+
const newScale = scale.value + delta * step;
|
|
184
|
+
if (newScale <= props.minScale) {
|
|
185
|
+
scale.value = props.minScale;
|
|
186
|
+
} else if (newScale >= props.maxScale) {
|
|
187
|
+
scale.value = props.maxScale;
|
|
188
|
+
} else {
|
|
189
|
+
scale.value = newScale;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 鼠标滚轮缩放
|
|
194
|
+
const handleWheel = e => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
const delta = e.deltaY > 0 ? -1 : 1;
|
|
197
|
+
zoom(delta, true);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 拖拽功能
|
|
201
|
+
const startPan = e => {
|
|
202
|
+
if (e.button !== 0) return;
|
|
203
|
+
isPanning.value = true;
|
|
204
|
+
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const pan = e => {
|
|
208
|
+
if (!isPanning.value) return;
|
|
209
|
+
|
|
210
|
+
const deltaX = e.clientX - lastPosition.value.x;
|
|
211
|
+
const deltaY = e.clientY - lastPosition.value.y;
|
|
212
|
+
|
|
213
|
+
position.value = {
|
|
214
|
+
x: position.value.x + deltaX,
|
|
215
|
+
y: position.value.y + deltaY
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
lastPosition.value = { x: e.clientX, y: e.clientY };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const stopPan = () => {
|
|
222
|
+
isPanning.value = false;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// 重置功能
|
|
226
|
+
const reset = () => {
|
|
227
|
+
rotation.value = 0;
|
|
228
|
+
scale.value = 1;
|
|
229
|
+
position.value = { x: 0, y: 0 };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// 查看原图功能
|
|
233
|
+
const original = () => {
|
|
234
|
+
emit('original');
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// 清理资源
|
|
238
|
+
const cleanup = () => {
|
|
239
|
+
if (imgDataUrl.value) {
|
|
240
|
+
// 释放blob URL
|
|
241
|
+
if (imgDataUrl.value.startsWith('blob:')) {
|
|
242
|
+
URL.revokeObjectURL(imgDataUrl.value);
|
|
243
|
+
}
|
|
244
|
+
imgDataUrl.value = '';
|
|
245
|
+
}
|
|
246
|
+
error.value = '';
|
|
247
|
+
// 重置缩放和位置
|
|
248
|
+
reset();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// 加载TIFF文件
|
|
252
|
+
const loadTiff = async () => {
|
|
253
|
+
if (!props.url) return;
|
|
254
|
+
cleanup();
|
|
255
|
+
isLoading.value = true;
|
|
256
|
+
error.value = '';
|
|
257
|
+
try {
|
|
258
|
+
// 检查 TIFF 库是否已加载
|
|
259
|
+
if (typeof window.Tiff === 'undefined') {
|
|
260
|
+
throw new Error('TIFF 库未加载。请确保 tiff.min.js 已正确引入。');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const response = await fetch(props.url);
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
266
|
+
}
|
|
267
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
268
|
+
|
|
269
|
+
// 创建TIFF实例
|
|
270
|
+
const tiff = new window.Tiff({ buffer: arrayBuffer });
|
|
271
|
+
// 获取图像数据
|
|
272
|
+
const canvas = tiff.toCanvas();
|
|
273
|
+
// 转换为DataURL
|
|
274
|
+
imgDataUrl.value = canvas.toDataURL('image/png');
|
|
275
|
+
error.value = '';
|
|
276
|
+
|
|
277
|
+
// 清理 TIFF 实例
|
|
278
|
+
tiff.close();
|
|
279
|
+
|
|
280
|
+
emit('load', {
|
|
281
|
+
width: canvas.width,
|
|
282
|
+
height: canvas.height
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error('加载TIFF文件失败:', err);
|
|
286
|
+
error.value = `加载TIFF文件失败: ${err.message}`;
|
|
287
|
+
emit('error', err);
|
|
288
|
+
} finally {
|
|
289
|
+
isLoading.value = false;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// 图片加载成功回调
|
|
294
|
+
const onImageLoad = () => {
|
|
295
|
+
console.log('TIFF图片渲染成功');
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// 图片加载失败回调
|
|
299
|
+
const onImageError = () => {
|
|
300
|
+
error.value = '图片渲染失败';
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// 监听URL变化
|
|
304
|
+
watch(
|
|
305
|
+
() => props.url,
|
|
306
|
+
(newUrl, oldUrl) => {
|
|
307
|
+
if (newUrl !== oldUrl) {
|
|
308
|
+
loadTiff();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// 挂载时加载
|
|
314
|
+
onMounted(() => {
|
|
315
|
+
if (props.url) {
|
|
316
|
+
loadTiff();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 组件销毁时清理
|
|
321
|
+
onBeforeUnmount(() => {
|
|
322
|
+
cleanup();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 暴露方法给父组件
|
|
326
|
+
defineExpose({
|
|
327
|
+
cleanup,
|
|
328
|
+
reload: loadTiff,
|
|
329
|
+
reset
|
|
330
|
+
});
|
|
331
|
+
</script>
|
|
332
|
+
|
|
333
|
+
<style lang="less" scoped>
|
|
334
|
+
// 样式已统一到公共样式文件,这里只保留组件特定样式
|
|
335
|
+
.img-container {
|
|
336
|
+
flex: 1;
|
|
337
|
+
width: 100%;
|
|
338
|
+
height: 100%;
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
position: relative;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.img-container .relative {
|
|
346
|
+
flex: 1;
|
|
347
|
+
position: relative;
|
|
348
|
+
width: 100%;
|
|
349
|
+
height: 100%;
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
overflow: hidden;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.img-container img {
|
|
357
|
+
max-width: 100%;
|
|
358
|
+
max-height: 100%;
|
|
359
|
+
object-fit: contain;
|
|
360
|
+
transition: transform 0.1s ease;
|
|
361
|
+
}
|
|
362
|
+
</style>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="preview-container">
|
|
3
|
+
<!-- 工具栏 -->
|
|
4
|
+
<div class="preview-toolbar preview-toolbar-right">
|
|
5
|
+
<div class="toolbar-group">
|
|
6
|
+
<span class="scale-text">
|
|
7
|
+
{{ Math.round(scale * 100) }}%
|
|
8
|
+
</span>
|
|
9
|
+
<div class="toolbar-divider"></div>
|
|
10
|
+
<a-tooltip mini position="bottom" content="重置">
|
|
11
|
+
<a-button
|
|
12
|
+
size="small"
|
|
13
|
+
type="outline"
|
|
14
|
+
@click="reset"
|
|
15
|
+
>
|
|
16
|
+
<RefreshCcw :size="16" />
|
|
17
|
+
</a-button>
|
|
18
|
+
</a-tooltip>
|
|
19
|
+
<a-tooltip mini position="bottom" content="缩小">
|
|
20
|
+
<a-button
|
|
21
|
+
size="small"
|
|
22
|
+
type="outline"
|
|
23
|
+
:disabled="scale <= 0.5"
|
|
24
|
+
@click="zoom(-0.1)"
|
|
25
|
+
>
|
|
26
|
+
<ZoomOut :size="16" />
|
|
27
|
+
</a-button>
|
|
28
|
+
</a-tooltip>
|
|
29
|
+
<a-tooltip mini position="bottom" content="放大">
|
|
30
|
+
<a-button
|
|
31
|
+
size="small"
|
|
32
|
+
type="outline"
|
|
33
|
+
:disabled="scale >= 2"
|
|
34
|
+
@click="zoom(0.1)"
|
|
35
|
+
>
|
|
36
|
+
<ZoomIn :size="16" />
|
|
37
|
+
</a-button>
|
|
38
|
+
</a-tooltip>
|
|
39
|
+
<a-tooltip
|
|
40
|
+
v-if="isDownload"
|
|
41
|
+
mini
|
|
42
|
+
position="bottom"
|
|
43
|
+
content="下载"
|
|
44
|
+
>
|
|
45
|
+
<a-button
|
|
46
|
+
size="small"
|
|
47
|
+
type="outline"
|
|
48
|
+
@click="emit('download')"
|
|
49
|
+
>
|
|
50
|
+
<Download :size="16" />
|
|
51
|
+
</a-button>
|
|
52
|
+
</a-tooltip>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div
|
|
56
|
+
class="excel-wrapper"
|
|
57
|
+
:style="{ transform: `scale(${scale})`, transformOrigin: 'top left' }"
|
|
58
|
+
>
|
|
59
|
+
<VueOfficeExcel
|
|
60
|
+
v-if="excel"
|
|
61
|
+
:src="excel"
|
|
62
|
+
style="width: 100%; height: 75vh"
|
|
63
|
+
@rendered="onRendered"
|
|
64
|
+
@error="onError"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup>
|
|
71
|
+
import { ref, watch } from 'vue';
|
|
72
|
+
import { Message } from '@arco-design/web-vue';
|
|
73
|
+
import { ZoomIn, ZoomOut, RefreshCcw, Download } from 'lucide-vue-next';
|
|
74
|
+
import VueOfficeExcel from '@vue-office/excel/lib/v3/index';
|
|
75
|
+
import '@vue-office/excel/lib/v3/index.css';
|
|
76
|
+
|
|
77
|
+
const props = defineProps({
|
|
78
|
+
url: {
|
|
79
|
+
type: String,
|
|
80
|
+
required: true
|
|
81
|
+
},
|
|
82
|
+
isDownload: {
|
|
83
|
+
type: Boolean,
|
|
84
|
+
default: false
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits(['download']);
|
|
89
|
+
|
|
90
|
+
const excel = ref('');
|
|
91
|
+
const loading = ref(false);
|
|
92
|
+
const scale = ref(1);
|
|
93
|
+
|
|
94
|
+
// 缩放方法
|
|
95
|
+
const zoom = delta => {
|
|
96
|
+
const newScale = scale.value + delta;
|
|
97
|
+
if (newScale >= 0.5 && newScale <= 2) {
|
|
98
|
+
scale.value = newScale;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 重置方法
|
|
103
|
+
const reset = () => {
|
|
104
|
+
scale.value = 1;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// 放大
|
|
108
|
+
const zoomIn = () => {
|
|
109
|
+
zoom(0.1);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// 缩小
|
|
113
|
+
const zoomOut = () => {
|
|
114
|
+
zoom(-0.1);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// 重置缩放
|
|
118
|
+
const resetZoom = () => {
|
|
119
|
+
reset();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 文件加载成功
|
|
123
|
+
const onRendered = () => {
|
|
124
|
+
loading.value = false;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// 文件加载失败
|
|
128
|
+
const onError = error => {
|
|
129
|
+
loading.value = false;
|
|
130
|
+
Message.error('Excel文件加载失败');
|
|
131
|
+
console.error('Excel文件加载失败:', error);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// 监听URL变化
|
|
135
|
+
watch(
|
|
136
|
+
() => props.url,
|
|
137
|
+
newUrl => {
|
|
138
|
+
if (newUrl) {
|
|
139
|
+
loading.value = true;
|
|
140
|
+
excel.value = newUrl;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{ immediate: true }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// 清理函数
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
loading.value = false;
|
|
149
|
+
excel.value = '';
|
|
150
|
+
scale.value = 1;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// 暴露清理方法给父组件
|
|
154
|
+
defineExpose({
|
|
155
|
+
cleanup
|
|
156
|
+
});
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<style lang="less" scoped>
|
|
160
|
+
// 样式已统一到公共样式文件,这里只保留组件特定样式
|
|
161
|
+
.excel-wrapper {
|
|
162
|
+
flex: 1;
|
|
163
|
+
display: inline-block;
|
|
164
|
+
min-width: 100%;
|
|
165
|
+
overflow: auto;
|
|
166
|
+
transition: transform 0.2s ease;
|
|
167
|
+
}
|
|
168
|
+
</style>
|