@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.
@@ -0,0 +1,216 @@
1
+ <template>
2
+ <div class="preview-container">
3
+ <div
4
+ v-if="isLoading"
5
+ class="preview-loading-overlay"
6
+ >
7
+ <a-spin size="large" />
8
+ <p class="preview-loading-title">正在加载文档...</p>
9
+ </div>
10
+ <!-- 工具栏 -->
11
+ <div class="preview-toolbar preview-toolbar-right">
12
+ <div class="toolbar-group">
13
+ <a-tooltip
14
+ v-if="isDownload"
15
+ mini
16
+ position="bottom"
17
+ content="下载"
18
+ >
19
+ <a-button
20
+ size="small"
21
+ type="outline"
22
+ @click="emit('download')"
23
+ >
24
+ <Download :size="16" />
25
+ </a-button>
26
+ </a-tooltip>
27
+ </div>
28
+ </div>
29
+ <!-- 文档容器 -->
30
+ <div ref="bodyContainer" class="docx-body-container" />
31
+ <div ref="styleContainer" />
32
+ </div>
33
+ </template>
34
+
35
+ <script setup>
36
+ import { renderAsync } from 'docx-preview';
37
+ import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
38
+ import { Download } from 'lucide-vue-next';
39
+
40
+ // 定义 props
41
+ const props = defineProps({
42
+ // 文档 URL 地址
43
+ url: {
44
+ type: String,
45
+ required: true
46
+ },
47
+ // 文档名称
48
+ docName: {
49
+ type: String,
50
+ default: '文档预览'
51
+ },
52
+ // 可选:直接传入文档数据(优先级高于 url)
53
+ docData: {
54
+ type: [Blob, ArrayBuffer],
55
+ default: null
56
+ },
57
+ // 缩放控制
58
+ minScale: {
59
+ type: Number,
60
+ default: 0.1
61
+ },
62
+ maxScale: {
63
+ type: Number,
64
+ default: 3
65
+ },
66
+ clickStep: {
67
+ type: Number,
68
+ default: 0.25
69
+ },
70
+ wheelStep: {
71
+ type: Number,
72
+ default: 0.1
73
+ },
74
+ isDownload: {
75
+ type: Boolean,
76
+ default: false
77
+ }
78
+ });
79
+
80
+ const emit = defineEmits(['download']);
81
+
82
+ const containerRef = ref(null);
83
+ const isLoading = ref(false);
84
+ const error = ref('');
85
+ const bodyContainer = ref(null);
86
+ const styleContainer = ref(null);
87
+
88
+ // 新增状态管理
89
+ const scale = ref(1);
90
+ const rotation = ref(0);
91
+
92
+ // 缩放相关逻辑
93
+ const zoom = (delta, isWheel = false) => {
94
+ const step = isWheel ? props.wheelStep : props.clickStep;
95
+ const newScale = scale.value + delta * step;
96
+ if (newScale <= props.minScale) {
97
+ scale.value = props.minScale;
98
+ } else if (newScale >= props.maxScale) {
99
+ scale.value = props.maxScale;
100
+ } else {
101
+ scale.value = newScale;
102
+ }
103
+ };
104
+
105
+ // 旋转处理
106
+ const rotate = delta => {
107
+ rotation.value = (rotation.value + delta + 360) % 360;
108
+ };
109
+
110
+ // 滚轮处理函数
111
+ const handleWheel = e => {
112
+ if (e.ctrlKey || e.metaKey) {
113
+ e.preventDefault();
114
+ const delta = e.deltaY > 0 ? -1 : 1;
115
+ zoom(delta, true);
116
+ }
117
+ };
118
+
119
+ // 重置功能
120
+ const reset = () => {
121
+ scale.value = 1;
122
+ rotation.value = 0;
123
+ if (containerRef.value) {
124
+ containerRef.value.scrollTop = 0;
125
+ containerRef.value.scrollLeft = 0;
126
+ }
127
+ };
128
+
129
+ // 渲染文档的方法
130
+ const renderDocument = async data => {
131
+ try {
132
+ if (!bodyContainer.value) {
133
+ throw new Error('bodyContainer 元素未找到');
134
+ }
135
+ bodyContainer.value.innerHTML = '';
136
+ const result = await renderAsync(
137
+ data,
138
+ bodyContainer.value,
139
+ styleContainer.value,
140
+ {
141
+ inWrapper: true,
142
+ styleMap: null,
143
+ defaultStyleMap: true,
144
+ useBase64URL: true,
145
+ ignoreHeight: false,
146
+ ignoreWidth: false,
147
+ ignoreFonts: false,
148
+ breakPages: true
149
+ }
150
+ );
151
+ } catch (err) {
152
+ error.value = `文档渲染失败: ${err.message}`;
153
+ throw err;
154
+ }
155
+ };
156
+
157
+ // 加载文档数据
158
+ const loadDocument = async () => {
159
+ try {
160
+ isLoading.value = true;
161
+ error.value = '';
162
+ let documentData = null;
163
+ if (props.url) {
164
+ // 通过 URL 请求获取文档数据
165
+ const response = await fetch(props.url);
166
+
167
+ if (!response.ok) {
168
+ throw new Error(`HTTP error! status: ${response.status}`);
169
+ }
170
+ documentData = await response.arrayBuffer();
171
+ } else {
172
+ throw new Error('未提供文档 URL 或数据');
173
+ }
174
+ isLoading.value = false;
175
+ await nextTick();
176
+ // 渲染文档
177
+ await renderDocument(documentData);
178
+ } catch (err) {
179
+ console.error('文档加载失败:', err);
180
+ error.value = `加载文档失败: ${err.message}`;
181
+ isLoading.value = false;
182
+ }
183
+ };
184
+
185
+ watch(
186
+ [() => props.url, () => props.docData],
187
+ () => {
188
+ if (props.url || props.docData) {
189
+ reset();
190
+ loadDocument();
191
+ }
192
+ },
193
+ { immediate: true }
194
+ );
195
+
196
+ // 组件挂载后加载文档
197
+ onMounted(() => {
198
+ if (props.url || props.docData) {
199
+ loadDocument();
200
+ }
201
+ });
202
+
203
+ // 暴露方法给父组件使用
204
+ defineExpose({
205
+ reset,
206
+ zoom,
207
+ rotate
208
+ });
209
+ </script>
210
+
211
+ <style lang="less" scoped>
212
+ // 样式已统一到公共样式文件,这里只保留组件特定样式
213
+ :deep(.docx-wrapper) {
214
+ background-color: #fff;
215
+ }
216
+ </style>
@@ -0,0 +1,317 @@
1
+ <template>
2
+ <div class="file-preview-container">
3
+ <!-- 下载进度条 - 居中显示 -->
4
+ <div
5
+ v-if="downloading"
6
+ class="preview-loading-overlay"
7
+ >
8
+ <div class="preview-loading-content">
9
+ <div class="preview-loading-title">
10
+ 正在加载文件
11
+ <div class="preview-loading-filename">{{ currentFileName }}</div>
12
+ </div>
13
+ <div class="preview-progress-bar">
14
+ <div
15
+ class="preview-progress-fill"
16
+ :style="{ width: `${progress}%` }"
17
+ />
18
+ </div>
19
+ <div class="preview-progress-text">
20
+ {{ progress }}%
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <!-- PDF预览 -->
26
+ <PdfPreview
27
+ v-if="fileType === 'pdf'"
28
+ ref="pdfPreviewRef"
29
+ :pdf-url="fileUrl"
30
+ :is-download="props.isDownload"
31
+ @download="handleDownload"
32
+ />
33
+
34
+ <!-- 图片预览 -->
35
+ <ImagePreview
36
+ v-else-if="fileType === 'image'"
37
+ ref="imagePreviewRef"
38
+ :url="fileUrl"
39
+ :original-id="originalId"
40
+ :is-download="props.isDownload"
41
+ @original="handleOriginal"
42
+ @download="handleDownload"
43
+ />
44
+
45
+ <!-- ttf预览 -->
46
+ <TifPreview
47
+ v-else-if="fileType === 'tif'"
48
+ ref="tifPreviewRe"
49
+ :url="fileUrl"
50
+ :is-download="props.isDownload"
51
+ @download="handleDownload"
52
+ />
53
+
54
+ <!-- docx预览 -->
55
+ <DocxPreview
56
+ v-else-if="fileType === 'docx'"
57
+ ref="docxPreviewRef"
58
+ :url="fileUrl"
59
+ :is-download="props.isDownload"
60
+ @download="handleDownload"
61
+ />
62
+
63
+ <!-- OFD预览 -->
64
+ <OfdPreview
65
+ v-else-if="fileType === 'ofd'"
66
+ ref="ofdPreviewRef"
67
+ :url="fileUrl"
68
+ :is-download="props.isDownload"
69
+ @download="handleDownload"
70
+ />
71
+
72
+ <!-- xlsx预览 -->
73
+ <XLSXPreview
74
+ v-else-if="fileType === 'xlsx'"
75
+ ref="xlsxPreviewRef"
76
+ :url="fileUrl"
77
+ :is-download="props.isDownload"
78
+ @download="handleDownload"
79
+ />
80
+
81
+ <!-- 不支持的文件类型 -->
82
+ <div v-else class="preview-empty-container">
83
+ <div class="preview-empty-message">{{ unsupportedMessage }}</div>
84
+ <a-button
85
+ v-if="downloadUrl"
86
+ type="primary"
87
+ @click="handleDownload"
88
+ >
89
+ 点击下载文件
90
+ </a-button>
91
+ </div>
92
+ </div>
93
+ </template>
94
+
95
+ <script setup>
96
+ import { ref, onBeforeUnmount } from 'vue';
97
+ import { Message } from '@arco-design/web-vue';
98
+ import ImagePreview from './ImagePreview.vue';
99
+ import PdfPreview from './PdfPreview.vue';
100
+ import TifPreview from './tifPreview.vue';
101
+ import OfdPreview from './ofdPreview.vue';
102
+ import DocxPreview from './docxPreview.vue';
103
+ import XLSXPreview from './xlsxPreview.vue';
104
+
105
+ // 定义 props
106
+ const props = defineProps({
107
+ isDownload: {
108
+ type: Boolean,
109
+ default: false
110
+ }
111
+ });
112
+
113
+ const fileType = ref('');
114
+ const currentFileName = ref('');
115
+ const fileUrl = ref('');
116
+ const downloading = ref(false);
117
+ const progress = ref(0);
118
+ const unsupportedMessage = ref('暂无预览内容');
119
+ const downloadUrl = ref('');
120
+ const imagePreviewRef = ref(null);
121
+ const pdfPreviewRef = ref(null);
122
+ const tifPreviewRef = ref(null);
123
+ const ofdPreviewRef = ref(null);
124
+ const docxPreviewRef = ref(null);
125
+ const xlsxPreviewRef = ref(null);
126
+
127
+ const originalId = ref('');
128
+
129
+ // 添加 AbortController 的引用
130
+ const abortController = ref(null);
131
+
132
+ // 通过文件名判断类型
133
+ const getFileType = (fileName = '') => {
134
+ const ext = fileName.split('.').pop()?.toLowerCase();
135
+ if (ext === 'pdf') return 'pdf';
136
+ if (['tif'].includes(ext)) return 'tif';
137
+ if (['ofd'].includes(ext)) return 'ofd';
138
+ if (['docx'].includes(ext)) return 'docx';
139
+ if (['xlsx'].includes(ext)) return 'xlsx';
140
+ if (
141
+ [
142
+ 'jpg',
143
+ 'jpeg',
144
+ 'png',
145
+ 'bmp',
146
+ 'gif',
147
+ 'webp',
148
+ 'svg',
149
+ 'apng',
150
+ 'avif'
151
+ ].includes(ext)
152
+ )
153
+ return 'image';
154
+ return 'unknown';
155
+ };
156
+
157
+ // 获取文件的MIME类型
158
+ const getMimeType = (fileName = '') => {
159
+ const ext = fileName.split('.').pop()?.toLowerCase();
160
+ const mimeTypes = {
161
+ pdf: 'application/pdf',
162
+ tif: 'image/tif',
163
+ docx: 'application/docx',
164
+ jpg: 'image/jpeg',
165
+ jpeg: 'image/jpeg',
166
+ bmp: 'image/bmp',
167
+ apng: 'image/apng',
168
+ avif: 'image/avif',
169
+ png: 'image/png',
170
+ gif: 'image/gif',
171
+ webp: 'image/webp',
172
+ svg: 'image/svg+xml',
173
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
174
+ xls: 'application/vnd.ms-excel'
175
+ };
176
+ return mimeTypes[ext] || 'application/octet-stream';
177
+ };
178
+
179
+ // 清除预览
180
+ const clearPreview = () => {
181
+ // 取消之前的请求
182
+ if (abortController.value) {
183
+ abortController.value.abort();
184
+ abortController.value = null;
185
+ }
186
+
187
+ if (fileUrl.value) {
188
+ URL.revokeObjectURL(fileUrl.value);
189
+ fileUrl.value = '';
190
+ }
191
+ fileType.value = '';
192
+ downloading.value = false;
193
+ progress.value = 0;
194
+ unsupportedMessage.value = '暂无预览内容';
195
+
196
+ // 重置图片预览组件
197
+ if (imagePreviewRef.value) {
198
+ imagePreviewRef.value.reset();
199
+ }
200
+
201
+ // 重置 PDF 预览组件
202
+ if (pdfPreviewRef.value) {
203
+ pdfPreviewRef.value.cleanup();
204
+ }
205
+
206
+ // 重置 TIFF 预览组件
207
+ if (tifPreviewRef.value) {
208
+ tifPreviewRef.value.cleanup();
209
+ }
210
+
211
+ // 重置 OFD 预览组件
212
+ if (ofdPreviewRef.value) {
213
+ ofdPreviewRef.value.cleanup();
214
+ }
215
+
216
+ // 重置 DOCX 预览组件
217
+ if (docxPreviewRef.value) {
218
+ docxPreviewRef.value.reset?.();
219
+ }
220
+
221
+ // 重置 XLSX 预览组件
222
+ if (xlsxPreviewRef.value) {
223
+ xlsxPreviewRef.value.cleanup?.();
224
+ }
225
+ };
226
+
227
+ // 添加下载处理函数
228
+ const handleDownload = () => {
229
+ if (!downloadUrl.value) return;
230
+ const link = document.createElement('a');
231
+ link.href = downloadUrl.value;
232
+ link.download = currentFileName.value;
233
+ document.body.appendChild(link);
234
+ link.click();
235
+ document.body.removeChild(link);
236
+ };
237
+
238
+ /**
239
+ * 预览方法 - SDK模式
240
+ * @param {string|Blob|File} file - 文件URL、Blob对象或File对象
241
+ * @param {string} fileName - 文件名(可选,用于判断文件类型)
242
+ * @param {object} options - 可选配置
243
+ * @param {Function} options.onProgress - 下载进度回调函数 (progress) => void
244
+ * @param {string} options.originalId - 原图ID(用于图片预览)
245
+ */
246
+ const preview = async (file, fileName, options = {}) => {
247
+ if (!file) {
248
+ Message.warning('请提供文件');
249
+ return;
250
+ }
251
+
252
+ const { onProgress, originalId: originalFileId } = options;
253
+
254
+ // 清除旧的预览
255
+ clearPreview();
256
+
257
+ // 如果传入的是URL字符串
258
+ if (typeof file === 'string') {
259
+ fileUrl.value = file;
260
+ downloadUrl.value = file;
261
+ currentFileName.value = fileName || '文件预览';
262
+ originalId.value = originalFileId || '';
263
+
264
+ // 判断文件类型
265
+ const type = getFileType(fileName || file);
266
+ if (type === 'unknown') {
267
+ unsupportedMessage.value = `暂不支持 ${fileName || file} 文件预览`;
268
+ return;
269
+ }
270
+ fileType.value = type;
271
+ downloading.value = false;
272
+ return;
273
+ }
274
+
275
+ // 如果传入的是Blob或File对象
276
+ if (file instanceof Blob || file instanceof File) {
277
+ currentFileName.value = fileName || (file instanceof File ? file.name : '文件预览');
278
+ originalId.value = originalFileId || '';
279
+
280
+ // 判断文件类型
281
+ const type = getFileType(currentFileName.value);
282
+ if (type === 'unknown') {
283
+ unsupportedMessage.value = `暂不支持 ${currentFileName.value} 文件预览`;
284
+ return;
285
+ }
286
+
287
+ fileType.value = type;
288
+ fileUrl.value = URL.createObjectURL(file);
289
+ downloading.value = false;
290
+ progress.value = 100;
291
+ return;
292
+ }
293
+
294
+ Message.error('不支持的文件格式');
295
+ };
296
+
297
+ const handleOriginal = () => {
298
+ if (fileUrl.value) {
299
+ preview(fileUrl.value, currentFileName.value, { originalId: fileUrl.value });
300
+ }
301
+ };
302
+
303
+ // 暴露方法给父组件
304
+ defineExpose({
305
+ preview,
306
+ clearPreview
307
+ });
308
+
309
+ // 组件销毁时清理资源
310
+ onBeforeUnmount(() => {
311
+ clearPreview();
312
+ });
313
+ </script>
314
+
315
+ <style lang="less" scoped>
316
+ // 样式已统一到公共样式文件,无需额外样式
317
+ </style>
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <div class="preview-container">
3
+ <!-- 工具栏 -->
4
+ <div v-if="props.isDownload" class="preview-toolbar preview-toolbar-right">
5
+ <div class="toolbar-group">
6
+ <a-tooltip
7
+ mini
8
+ position="bottom"
9
+ content="下载"
10
+ >
11
+ <a-button
12
+ size="small"
13
+ type="outline"
14
+ @click="emit('download')"
15
+ >
16
+ <Download :size="16" />
17
+ </a-button>
18
+ </a-tooltip>
19
+ </div>
20
+ </div>
21
+ <OfdView
22
+ :show-open-file-button="false"
23
+ :ofd-link="props.url"
24
+ class="ofd-view"
25
+ @load="handleDocumentLoad"
26
+ />
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { ref, watch, onMounted } from 'vue';
32
+ import { OfdView } from 'bestofdview';
33
+ import 'bestofdview/dist/style.css';
34
+ import { Message } from '@arco-design/web-vue';
35
+ import { Download } from 'lucide-vue-next';
36
+
37
+ const props = defineProps({
38
+ url: {
39
+ type: String,
40
+ required: true
41
+ },
42
+ isDownload: {
43
+ type: Boolean,
44
+ default: false
45
+ }
46
+ });
47
+
48
+ const emit = defineEmits(['download']);
49
+
50
+ const loading = ref(false);
51
+
52
+ // 监听文档加载状态
53
+ const handleDocumentLoad = () => {
54
+ loading.value = true;
55
+ // 使用 fetch 检查文件是否可访问
56
+ if (props.url) {
57
+ fetch(props.url)
58
+ .then(response => {
59
+ if (response.ok) {
60
+ // 文件可访问,等待 bestofdview 内部加载完成
61
+ setTimeout(() => {
62
+ loading.value = false;
63
+ }, 1000);
64
+ } else {
65
+ loading.value = false;
66
+ Message.error(`加载失败: ${response.status}`);
67
+ }
68
+ })
69
+ .catch(error => {
70
+ console.error('检查OFD文件失败:', error);
71
+ loading.value = false;
72
+ });
73
+ }
74
+ };
75
+
76
+ // 监听URL变化
77
+ watch(
78
+ () => props.url,
79
+ newUrl => {
80
+ if (newUrl) {
81
+ handleDocumentLoad();
82
+ }
83
+ },
84
+ { immediate: true }
85
+ );
86
+
87
+ // 组件挂载时处理
88
+ onMounted(() => {
89
+ if (props.url) {
90
+ handleDocumentLoad();
91
+ }
92
+ });
93
+
94
+ // 清理函数(保持接口兼容性)
95
+ const cleanup = () => {
96
+ loading.value = false;
97
+ };
98
+
99
+ // 暴露清理方法给父组件
100
+ defineExpose({
101
+ cleanup
102
+ });
103
+ </script>
104
+
105
+ <style lang="less" scoped>
106
+ // 样式已统一到公共样式文件,这里只保留组件特定样式
107
+ </style>