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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adtec-core-package",
3
- "version": "3.1.6",
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",
@@ -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>