adtec-core-package 3.1.6 → 3.1.7
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/adtec-core-package/adtec-core-package.css +1 -0
- package/adtec-core-package/adtec-core-package.js +41216 -0
- package/adtec-core-package/adtec-core-package.umd.cjs +87 -0
- package/adtec-core-package/favicon.ico +0 -0
- package/package.json +4 -2
- package/src/api/DocumentApi.ts +11 -1
- package/src/components/upload/DocxJsViewer.vue +286 -0
- package/src/components/upload/ExcelJsViewer.vue +270 -0
- package/src/components/upload/FileView.vue +102 -159
- package/src/components/upload/OfficePreview.vue +479 -0
- package/src/components/upload/OfficePreviewHeaderBar.vue +315 -0
- package/src/components/upload/OfficePreviewToolbar.vue +203 -0
- package/src/components/upload/PdfJsViewer.vue +920 -0
- package/src/utils/docxPreviewUtil.ts +108 -0
- package/src/utils/excelPreviewUtil.ts +101 -0
- package/src/utils/officePreviewUtil.ts +122 -0
- package/src/utils/pdfSearchUtil.ts +127 -0
- package/src/utils/toSameOriginFileUrl.ts +1 -1
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adtec-core-package",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"@element-plus/icons-vue": "^2.3.1",
|
|
35
35
|
"@eslint/object-schema": "3.0.2",
|
|
36
36
|
"@floating-ui/dom": "1.7.5",
|
|
37
|
-
"@onlyoffice/document-editor-vue": "1.4.0",
|
|
38
37
|
"@tiptap/core": "3.20.0",
|
|
39
38
|
"@tiptap/extension-bold": "3.20.0",
|
|
40
39
|
"@tiptap/extension-code-block": "3.20.0",
|
|
@@ -76,6 +75,7 @@
|
|
|
76
75
|
"@vxe-ui/plugin-render-element": "4.0.10",
|
|
77
76
|
"axios": "^1.7.7",
|
|
78
77
|
"crypto-js": "^4.2.0",
|
|
78
|
+
"docx-preview": "^0.3.7",
|
|
79
79
|
"dom-to-image-more": "3.7.2",
|
|
80
80
|
"echarts": "^5.6.0",
|
|
81
81
|
"element-plus": "2.8.7",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"linq-to-ts": "^1.3.0",
|
|
92
92
|
"lowlight": "3.3.0",
|
|
93
93
|
"nzh": "1.0.14",
|
|
94
|
+
"pdfjs-dist": "^4.10.38",
|
|
94
95
|
"pinia": "^2.3.0",
|
|
95
96
|
"pinia-plugin-store": "^2.2.9",
|
|
96
97
|
"pretty-bytes": "7.1.0",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
"tdesign-vue-next": "1.18.2",
|
|
103
104
|
"uuid": "^11.0.3",
|
|
104
105
|
"vue": "^3.5.13",
|
|
106
|
+
"vue-demi": "^0.14.10",
|
|
105
107
|
"vue-focus-lock": "^3.0.0",
|
|
106
108
|
"vue-i18n": "11.2.8",
|
|
107
109
|
"vue-img-viewr": "2.0.11",
|
package/src/api/DocumentApi.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import request from '../utils/request'
|
|
2
|
+
import axiosRequest from '../utils/AxiosConfig'
|
|
2
3
|
import type { ISysUploadFiles } from '../interface/ISysUploadFiles.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -30,5 +31,14 @@ export default {
|
|
|
30
31
|
},
|
|
31
32
|
delSysUploadFiles(ids: string[]): Promise<Record<string, string>> {
|
|
32
33
|
return request.post(basePath + 'delSysUploadFiles', ids)
|
|
33
|
-
}
|
|
34
|
+
},
|
|
35
|
+
/** 旧版 .doc 转 PDF 流,仅供预览,下载仍用原文件 */
|
|
36
|
+
previewWordAsPdf(id: string): Promise<Blob> {
|
|
37
|
+
return axiosRequest<Blob>({
|
|
38
|
+
url: basePath + 'preview/wordToPdf',
|
|
39
|
+
method: 'get',
|
|
40
|
+
params: { id },
|
|
41
|
+
responseType: 'blob',
|
|
42
|
+
}).then((res) => res.data)
|
|
43
|
+
},
|
|
34
44
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="scrollRef" class="docx-js-viewer">
|
|
3
|
+
<div v-if="loading" class="docx-loading">正在解析 Word 文档…</div>
|
|
4
|
+
<div v-else-if="legacyDoc" class="docx-legacy-tip">
|
|
5
|
+
<el-empty description="旧版 Word(.doc)暂不支持浏览器内预览,请下载后查看或转换为 docx。" />
|
|
6
|
+
</div>
|
|
7
|
+
<div v-else-if="loadError" class="docx-legacy-tip">
|
|
8
|
+
<el-empty :description="loadError" />
|
|
9
|
+
</div>
|
|
10
|
+
<div v-else class="docx-body">
|
|
11
|
+
<div ref="styleHostRef" class="docx-style-host" />
|
|
12
|
+
<div class="docx-scale-host" :style="scaleHostStyle">
|
|
13
|
+
<div ref="bodyHostRef" class="docx-render-host" :style="renderStyle" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { renderAsync } from 'docx-preview'
|
|
21
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
22
|
+
import {
|
|
23
|
+
activateDocxSearchMark,
|
|
24
|
+
clearDocxSearchMarks,
|
|
25
|
+
detectWordBinaryFormat,
|
|
26
|
+
type DocxSearchMatch,
|
|
27
|
+
resolveRawWordExt,
|
|
28
|
+
searchDocxPages,
|
|
29
|
+
} from '../../utils/docxPreviewUtil.ts'
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
src?: Blob | ArrayBuffer
|
|
34
|
+
scale?: number
|
|
35
|
+
fileName?: string
|
|
36
|
+
fileType?: string
|
|
37
|
+
fileUrl?: string
|
|
38
|
+
}>(),
|
|
39
|
+
{ scale: 1 },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
loaded: []
|
|
44
|
+
error: [err?: unknown]
|
|
45
|
+
'page-change': [page: number]
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const scrollRef = ref<HTMLElement | null>(null)
|
|
49
|
+
const styleHostRef = ref<HTMLElement | null>(null)
|
|
50
|
+
const bodyHostRef = ref<HTMLElement | null>(null)
|
|
51
|
+
const loading = ref(false)
|
|
52
|
+
const legacyDoc = ref(false)
|
|
53
|
+
const loadError = ref('')
|
|
54
|
+
const totalPages = ref(0)
|
|
55
|
+
const currentPage = ref(1)
|
|
56
|
+
const pageSections = ref<HTMLElement[]>([])
|
|
57
|
+
const searchMatches = ref<DocxSearchMatch[]>([])
|
|
58
|
+
const activeMatchIndex = ref(-1)
|
|
59
|
+
|
|
60
|
+
const userScale = computed(() => props.scale ?? 1)
|
|
61
|
+
const renderStyle = computed(() => ({
|
|
62
|
+
transform: `scale(${userScale.value})`,
|
|
63
|
+
transformOrigin: 'top left',
|
|
64
|
+
}))
|
|
65
|
+
const scaleHostStyle = computed(() => {
|
|
66
|
+
const section = pageSections.value[currentPage.value - 1]
|
|
67
|
+
if (!section) return { minHeight: '100%' }
|
|
68
|
+
return {
|
|
69
|
+
width: `${Math.max(section.offsetWidth, 1) * userScale.value}px`,
|
|
70
|
+
minHeight: `${Math.max(section.offsetHeight, 1) * userScale.value}px`,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const resetViewer = () => {
|
|
75
|
+
legacyDoc.value = false
|
|
76
|
+
loadError.value = ''
|
|
77
|
+
totalPages.value = 0
|
|
78
|
+
currentPage.value = 1
|
|
79
|
+
pageSections.value = []
|
|
80
|
+
searchMatches.value = []
|
|
81
|
+
activeMatchIndex.value = -1
|
|
82
|
+
styleHostRef.value && (styleHostRef.value.innerHTML = '')
|
|
83
|
+
bodyHostRef.value && (bodyHostRef.value.innerHTML = '')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const collectPageSections = () => {
|
|
87
|
+
const host = bodyHostRef.value
|
|
88
|
+
if (!host) {
|
|
89
|
+
pageSections.value = []
|
|
90
|
+
totalPages.value = 0
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const sections = Array.from(
|
|
94
|
+
host.querySelectorAll<HTMLElement>('section.docx, .docx-wrapper > section, .docx > section'),
|
|
95
|
+
)
|
|
96
|
+
if (sections.length > 0) {
|
|
97
|
+
pageSections.value = sections
|
|
98
|
+
} else if (host.firstElementChild instanceof HTMLElement) {
|
|
99
|
+
pageSections.value = [host.firstElementChild]
|
|
100
|
+
} else {
|
|
101
|
+
pageSections.value = [host]
|
|
102
|
+
}
|
|
103
|
+
totalPages.value = pageSections.value.length
|
|
104
|
+
currentPage.value = 1
|
|
105
|
+
emit('page-change', 1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const loadDocument = async (src: Blob | ArrayBuffer) => {
|
|
109
|
+
loading.value = true
|
|
110
|
+
resetViewer()
|
|
111
|
+
try {
|
|
112
|
+
const buffer = src instanceof Blob ? await src.arrayBuffer() : src
|
|
113
|
+
const binaryFormat = detectWordBinaryFormat(buffer)
|
|
114
|
+
const rawExt = resolveRawWordExt(props.fileType, props.fileName, props.fileUrl)
|
|
115
|
+
if (binaryFormat === 'doc' || rawExt === 'doc') {
|
|
116
|
+
legacyDoc.value = true
|
|
117
|
+
emit('loaded')
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const bodyHost = bodyHostRef.value
|
|
121
|
+
const styleHost = styleHostRef.value
|
|
122
|
+
if (!bodyHost || !styleHost) {
|
|
123
|
+
throw new Error('预览容器未就绪')
|
|
124
|
+
}
|
|
125
|
+
await renderAsync(buffer, bodyHost, styleHost, {
|
|
126
|
+
className: 'docx',
|
|
127
|
+
inWrapper: true,
|
|
128
|
+
breakPages: true,
|
|
129
|
+
ignoreLastRenderedPageBreak: true,
|
|
130
|
+
useBase64URL: true,
|
|
131
|
+
renderHeaders: true,
|
|
132
|
+
renderFooters: true,
|
|
133
|
+
renderFootnotes: true,
|
|
134
|
+
renderEndnotes: true,
|
|
135
|
+
})
|
|
136
|
+
await nextTick()
|
|
137
|
+
collectPageSections()
|
|
138
|
+
emit('loaded')
|
|
139
|
+
} catch (err) {
|
|
140
|
+
loadError.value = 'Word 文档解析失败,请下载后查看'
|
|
141
|
+
emit('error', err)
|
|
142
|
+
} finally {
|
|
143
|
+
loading.value = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const scrollToPage = async (pageNum: number) => {
|
|
148
|
+
const target = Math.min(Math.max(1, pageNum), totalPages.value || 1)
|
|
149
|
+
const section = pageSections.value[target - 1]
|
|
150
|
+
currentPage.value = target
|
|
151
|
+
emit('page-change', target)
|
|
152
|
+
await nextTick()
|
|
153
|
+
const container = scrollRef.value
|
|
154
|
+
if (!container || !section) return
|
|
155
|
+
const top = section.offsetTop - 12
|
|
156
|
+
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const scrollToMatch = async (match: DocxSearchMatch) => {
|
|
160
|
+
await scrollToPage(match.pageIndex + 1)
|
|
161
|
+
await nextTick()
|
|
162
|
+
const host = bodyHostRef.value
|
|
163
|
+
if (!host) return
|
|
164
|
+
activateDocxSearchMark(host, match.markId)
|
|
165
|
+
const mark = host.querySelector<HTMLElement>(`[data-match-id="${match.markId}"]`)
|
|
166
|
+
const container = scrollRef.value
|
|
167
|
+
if (!mark || !container) return
|
|
168
|
+
const top = mark.offsetTop - container.clientHeight * 0.3
|
|
169
|
+
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const runSearch = async (query: string) => {
|
|
173
|
+
const host = bodyHostRef.value
|
|
174
|
+
if (!host || pageSections.value.length === 0) return { total: 0, index: -1 }
|
|
175
|
+
clearDocxSearchMarks(host)
|
|
176
|
+
const matches = searchDocxPages(pageSections.value, query)
|
|
177
|
+
searchMatches.value = matches
|
|
178
|
+
if (matches.length === 0) {
|
|
179
|
+
activeMatchIndex.value = -1
|
|
180
|
+
return { total: 0, index: -1 }
|
|
181
|
+
}
|
|
182
|
+
activeMatchIndex.value = 0
|
|
183
|
+
await scrollToMatch(matches[0])
|
|
184
|
+
return { total: matches.length, index: 0 }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const clearSearch = () => {
|
|
188
|
+
const host = bodyHostRef.value
|
|
189
|
+
if (host) clearDocxSearchMarks(host)
|
|
190
|
+
searchMatches.value = []
|
|
191
|
+
activeMatchIndex.value = -1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const gotoSearchMatch = async (direction: 1 | -1) => {
|
|
195
|
+
if (searchMatches.value.length === 0) return { total: 0, index: -1 }
|
|
196
|
+
const total = searchMatches.value.length
|
|
197
|
+
let next = activeMatchIndex.value + direction
|
|
198
|
+
if (next < 0) next = total - 1
|
|
199
|
+
if (next >= total) next = 0
|
|
200
|
+
activeMatchIndex.value = next
|
|
201
|
+
await scrollToMatch(searchMatches.value[next])
|
|
202
|
+
return { total, index: next }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const refitLayout = () => {
|
|
206
|
+
scrollRef.value?.scrollTo({ top: 0 })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
watch(
|
|
210
|
+
() => props.src,
|
|
211
|
+
(src) => {
|
|
212
|
+
if (src) void loadDocument(src)
|
|
213
|
+
else resetViewer()
|
|
214
|
+
},
|
|
215
|
+
{ immediate: true },
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
defineExpose({
|
|
219
|
+
totalPages,
|
|
220
|
+
scrollToPage,
|
|
221
|
+
runSearch,
|
|
222
|
+
clearSearch,
|
|
223
|
+
gotoSearchMatch,
|
|
224
|
+
refitLayout,
|
|
225
|
+
})
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<style scoped lang="scss">
|
|
229
|
+
.docx-js-viewer {
|
|
230
|
+
flex: 1;
|
|
231
|
+
width: 100%;
|
|
232
|
+
min-height: 0;
|
|
233
|
+
overflow: auto;
|
|
234
|
+
background: #e5e7eb;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.docx-body {
|
|
238
|
+
min-height: 100%;
|
|
239
|
+
padding: 12px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.docx-style-host {
|
|
243
|
+
display: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.docx-scale-host {
|
|
247
|
+
display: inline-block;
|
|
248
|
+
min-width: 100%;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.docx-render-host {
|
|
252
|
+
background: transparent;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.docx-loading,
|
|
256
|
+
.docx-legacy-tip {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
min-height: 240px;
|
|
261
|
+
color: #6b7280;
|
|
262
|
+
font-size: 14px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
:deep(.docx-wrapper) {
|
|
266
|
+
background: #e5e7eb;
|
|
267
|
+
padding: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:deep(.docx-wrapper > section.docx) {
|
|
271
|
+
margin: 0 auto 16px;
|
|
272
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
|
|
273
|
+
background: #fff;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
:deep(mark.docx-search-mark) {
|
|
277
|
+
background: rgba(255, 214, 0, 0.42);
|
|
278
|
+
border-radius: 2px;
|
|
279
|
+
padding: 0 1px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
:deep(mark.docx-search-mark--active) {
|
|
283
|
+
background: rgba(255, 87, 34, 0.5);
|
|
284
|
+
box-shadow: 0 0 0 2px rgba(255, 87, 34, 0.75);
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="scrollRef" class="excel-js-viewer">
|
|
3
|
+
<div v-if="loading" class="excel-loading">正在解析表格…</div>
|
|
4
|
+
<div v-else-if="sheets.length === 0" class="excel-empty">表格为空或无法解析</div>
|
|
5
|
+
<div v-else class="excel-body">
|
|
6
|
+
<div class="excel-sheet-name">{{ activeSheet?.name }}</div>
|
|
7
|
+
<div class="excel-scale-host" :style="scaleHostStyle">
|
|
8
|
+
<table class="excel-table" :style="tableStyle">
|
|
9
|
+
<tbody>
|
|
10
|
+
<tr v-for="(row, rowIndex) in activeSheet?.rows || []" :key="rowIndex">
|
|
11
|
+
<td
|
|
12
|
+
v-for="cell in row"
|
|
13
|
+
:key="`${cell.row}-${cell.col}`"
|
|
14
|
+
:rowspan="cell.rowSpan"
|
|
15
|
+
:colspan="cell.colSpan"
|
|
16
|
+
:data-row="cell.row"
|
|
17
|
+
:data-col="cell.col"
|
|
18
|
+
:class="cellClass(cell)"
|
|
19
|
+
>
|
|
20
|
+
{{ cell.value }}
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
|
32
|
+
import {
|
|
33
|
+
type ExcelPreviewCell,
|
|
34
|
+
type ExcelPreviewSheet,
|
|
35
|
+
type ExcelSearchMatch,
|
|
36
|
+
parseExcelWorkbook,
|
|
37
|
+
searchExcelSheets,
|
|
38
|
+
} from '../../utils/excelPreviewUtil.ts'
|
|
39
|
+
|
|
40
|
+
const props = withDefaults(
|
|
41
|
+
defineProps<{
|
|
42
|
+
src?: Blob | ArrayBuffer
|
|
43
|
+
scale?: number
|
|
44
|
+
}>(),
|
|
45
|
+
{ scale: 1 },
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const emit = defineEmits<{
|
|
49
|
+
loaded: []
|
|
50
|
+
error: [err?: unknown]
|
|
51
|
+
'page-change': [page: number]
|
|
52
|
+
}>()
|
|
53
|
+
|
|
54
|
+
const scrollRef = ref<HTMLElement | null>(null)
|
|
55
|
+
const sheets = shallowRef<ExcelPreviewSheet[]>([])
|
|
56
|
+
const loading = ref(false)
|
|
57
|
+
const currentSheet = ref(1)
|
|
58
|
+
const searchMatches = ref<ExcelSearchMatch[]>([])
|
|
59
|
+
const activeMatchIndex = ref(-1)
|
|
60
|
+
|
|
61
|
+
const totalPages = ref(0)
|
|
62
|
+
const activeSheet = computed(() => sheets.value[currentSheet.value - 1])
|
|
63
|
+
|
|
64
|
+
const userScale = computed(() => props.scale ?? 1)
|
|
65
|
+
const tableStyle = computed(() => ({
|
|
66
|
+
transform: `scale(${userScale.value})`,
|
|
67
|
+
transformOrigin: 'top left',
|
|
68
|
+
}))
|
|
69
|
+
const scaleHostStyle = computed(() => {
|
|
70
|
+
const sheet = activeSheet.value
|
|
71
|
+
if (!sheet) return {}
|
|
72
|
+
const rowCount = sheet.rows.length || 1
|
|
73
|
+
const colCount = Math.max(...sheet.rows.map((row) => row.length), 1)
|
|
74
|
+
const baseWidth = colCount * 96 + 32
|
|
75
|
+
const baseHeight = rowCount * 28 + 48
|
|
76
|
+
return {
|
|
77
|
+
width: `${baseWidth * userScale.value}px`,
|
|
78
|
+
height: `${baseHeight * userScale.value}px`,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const activeMatch = computed(() =>
|
|
83
|
+
activeMatchIndex.value >= 0 ? searchMatches.value[activeMatchIndex.value] : null,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const cellClass = (cell: ExcelPreviewCell) => {
|
|
87
|
+
const classes: string[] = []
|
|
88
|
+
const hasMatch = searchMatches.value.some(
|
|
89
|
+
(m) => m.sheetIndex === currentSheet.value - 1 && m.row === cell.row && m.col === cell.col,
|
|
90
|
+
)
|
|
91
|
+
if (hasMatch) classes.push('excel-cell--match')
|
|
92
|
+
if (
|
|
93
|
+
activeMatch.value &&
|
|
94
|
+
activeMatch.value.sheetIndex === currentSheet.value - 1 &&
|
|
95
|
+
activeMatch.value.row === cell.row &&
|
|
96
|
+
activeMatch.value.col === cell.col
|
|
97
|
+
) {
|
|
98
|
+
classes.push('excel-cell--active')
|
|
99
|
+
}
|
|
100
|
+
if (classes.length === 0) return undefined
|
|
101
|
+
return classes
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const resetViewer = () => {
|
|
105
|
+
sheets.value = []
|
|
106
|
+
totalPages.value = 0
|
|
107
|
+
currentSheet.value = 1
|
|
108
|
+
searchMatches.value = []
|
|
109
|
+
activeMatchIndex.value = -1
|
|
110
|
+
loading.value = false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const loadWorkbook = async (src: Blob | ArrayBuffer) => {
|
|
114
|
+
loading.value = true
|
|
115
|
+
try {
|
|
116
|
+
sheets.value = await parseExcelWorkbook(src)
|
|
117
|
+
totalPages.value = sheets.value.length
|
|
118
|
+
currentSheet.value = 1
|
|
119
|
+
emit('page-change', 1)
|
|
120
|
+
emit('loaded')
|
|
121
|
+
} catch (err) {
|
|
122
|
+
emit('error', err)
|
|
123
|
+
} finally {
|
|
124
|
+
loading.value = false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const scrollToCell = async (match: ExcelSearchMatch) => {
|
|
129
|
+
const targetSheet = match.sheetIndex + 1
|
|
130
|
+
if (currentSheet.value !== targetSheet) {
|
|
131
|
+
currentSheet.value = targetSheet
|
|
132
|
+
emit('page-change', targetSheet)
|
|
133
|
+
}
|
|
134
|
+
await nextTick()
|
|
135
|
+
const container = scrollRef.value
|
|
136
|
+
const cellEl = container?.querySelector(
|
|
137
|
+
`td[data-row="${match.row}"][data-col="${match.col}"]`,
|
|
138
|
+
) as HTMLElement | null
|
|
139
|
+
if (!container || !cellEl) return
|
|
140
|
+
const top = cellEl.offsetTop - container.clientHeight * 0.3
|
|
141
|
+
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const scrollToPage = async (pageNum: number) => {
|
|
145
|
+
const target = Math.min(Math.max(1, pageNum), totalPages.value || 1)
|
|
146
|
+
currentSheet.value = target
|
|
147
|
+
emit('page-change', target)
|
|
148
|
+
await nextTick()
|
|
149
|
+
scrollRef.value?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const runSearch = async (query: string) => {
|
|
153
|
+
const matches = searchExcelSheets(sheets.value, query)
|
|
154
|
+
searchMatches.value = matches
|
|
155
|
+
if (matches.length === 0) {
|
|
156
|
+
activeMatchIndex.value = -1
|
|
157
|
+
return { total: 0, index: -1 }
|
|
158
|
+
}
|
|
159
|
+
activeMatchIndex.value = 0
|
|
160
|
+
await scrollToCell(matches[0])
|
|
161
|
+
return { total: matches.length, index: 0 }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const clearSearch = () => {
|
|
165
|
+
searchMatches.value = []
|
|
166
|
+
activeMatchIndex.value = -1
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const gotoSearchMatch = async (direction: 1 | -1) => {
|
|
170
|
+
if (searchMatches.value.length === 0) return { total: 0, index: -1 }
|
|
171
|
+
const total = searchMatches.value.length
|
|
172
|
+
let next = activeMatchIndex.value + direction
|
|
173
|
+
if (next < 0) next = total - 1
|
|
174
|
+
if (next >= total) next = 0
|
|
175
|
+
activeMatchIndex.value = next
|
|
176
|
+
await scrollToCell(searchMatches.value[next])
|
|
177
|
+
return { total, index: next }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const refitLayout = () => {
|
|
181
|
+
scrollRef.value?.scrollTo({ top: 0 })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
watch(
|
|
185
|
+
() => props.src,
|
|
186
|
+
(src) => {
|
|
187
|
+
resetViewer()
|
|
188
|
+
if (src) void loadWorkbook(src)
|
|
189
|
+
},
|
|
190
|
+
{ immediate: true },
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
defineExpose({
|
|
194
|
+
totalPages,
|
|
195
|
+
scrollToPage,
|
|
196
|
+
runSearch,
|
|
197
|
+
clearSearch,
|
|
198
|
+
gotoSearchMatch,
|
|
199
|
+
refitLayout,
|
|
200
|
+
})
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<style scoped lang="scss">
|
|
204
|
+
.excel-js-viewer {
|
|
205
|
+
flex: 1;
|
|
206
|
+
width: 100%;
|
|
207
|
+
min-height: 0;
|
|
208
|
+
overflow: auto;
|
|
209
|
+
background: #f5f5f5;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.excel-body {
|
|
213
|
+
min-height: 100%;
|
|
214
|
+
padding: 12px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.excel-sheet-name {
|
|
218
|
+
margin: 0 0 8px;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
color: #374151;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.excel-scale-host {
|
|
225
|
+
display: inline-block;
|
|
226
|
+
min-width: 100%;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.excel-table {
|
|
230
|
+
border-collapse: collapse;
|
|
231
|
+
background: #fff;
|
|
232
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
|
233
|
+
font-size: 13px;
|
|
234
|
+
line-height: 1.4;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.excel-table td {
|
|
238
|
+
min-width: 72px;
|
|
239
|
+
max-width: 280px;
|
|
240
|
+
padding: 6px 10px;
|
|
241
|
+
border: 1px solid #e5e7eb;
|
|
242
|
+
color: #111827;
|
|
243
|
+
vertical-align: top;
|
|
244
|
+
word-break: break-word;
|
|
245
|
+
white-space: pre-wrap;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.excel-table tr:nth-child(even) td {
|
|
249
|
+
background: #fafafa;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.excel-cell--match {
|
|
253
|
+
background: rgba(255, 214, 0, 0.35) !important;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.excel-cell--active {
|
|
257
|
+
background: rgba(255, 87, 34, 0.45) !important;
|
|
258
|
+
box-shadow: inset 0 0 0 2px rgba(255, 87, 34, 0.75);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.excel-loading,
|
|
262
|
+
.excel-empty {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
min-height: 240px;
|
|
267
|
+
color: #6b7280;
|
|
268
|
+
font-size: 14px;
|
|
269
|
+
}
|
|
270
|
+
</style>
|