adtec-core-package 3.1.6 → 3.1.8

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.
Files changed (31) hide show
  1. package/adtec-core-package/adtec-core-package.css +1 -0
  2. package/adtec-core-package/adtec-core-package.js +41216 -0
  3. package/adtec-core-package/adtec-core-package.umd.cjs +87 -0
  4. package/adtec-core-package/favicon.ico +0 -0
  5. package/adtec-core-package-3.1.7.tgz +0 -0
  6. package/package/.editorconfig +6 -0
  7. package/package/adtec-core-package/adtec-core-package.css +1 -0
  8. package/package/adtec-core-package/adtec-core-package.js +41216 -0
  9. package/package/adtec-core-package/adtec-core-package.umd.cjs +87 -0
  10. package/package/adtec-core-package/favicon.ico +0 -0
  11. package/package/index.html +13 -0
  12. package/package/prebuilt/umo-editor/favicon.ico +0 -0
  13. package/package/prebuilt/umo-editor/umo-editor.css +1 -0
  14. package/package/public/favicon.ico +0 -0
  15. package/package/src/assets/base.css +86 -0
  16. package/package/src/assets/main.css +35 -0
  17. package/package/src/components/editor-main/src/extensions/bookmark.js +110 -0
  18. package/package.json +4 -2
  19. package/src/api/DocumentApi.ts +11 -1
  20. package/src/components/upload/DocxJsViewer.vue +286 -0
  21. package/src/components/upload/ExcelJsViewer.vue +444 -0
  22. package/src/components/upload/FileView.vue +102 -159
  23. package/src/components/upload/OfficePreview.vue +481 -0
  24. package/src/components/upload/OfficePreviewHeaderBar.vue +315 -0
  25. package/src/components/upload/OfficePreviewToolbar.vue +203 -0
  26. package/src/components/upload/PdfJsViewer.vue +920 -0
  27. package/src/utils/docxPreviewUtil.ts +108 -0
  28. package/src/utils/excelPreviewUtil.ts +307 -0
  29. package/src/utils/officePreviewUtil.ts +122 -0
  30. package/src/utils/pdfSearchUtil.ts +127 -0
  31. package/src/utils/toSameOriginFileUrl.ts +1 -1
Binary file
@@ -0,0 +1,86 @@
1
+ /* color palette from <https://github.com/vuejs/theme> */
2
+ :root {
3
+ --vt-c-white: #ffffff;
4
+ --vt-c-white-soft: #f8f8f8;
5
+ --vt-c-white-mute: #f2f2f2;
6
+
7
+ --vt-c-black: #181818;
8
+ --vt-c-black-soft: #222222;
9
+ --vt-c-black-mute: #282828;
10
+
11
+ --vt-c-indigo: #2c3e50;
12
+
13
+ --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14
+ --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15
+ --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16
+ --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17
+
18
+ --vt-c-text-light-1: var(--vt-c-indigo);
19
+ --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20
+ --vt-c-text-dark-1: var(--vt-c-white);
21
+ --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22
+ }
23
+
24
+ /* semantic color variables for this project */
25
+ :root {
26
+ --color-background: var(--vt-c-white);
27
+ --color-background-soft: var(--vt-c-white-soft);
28
+ --color-background-mute: var(--vt-c-white-mute);
29
+
30
+ --color-border: var(--vt-c-divider-light-2);
31
+ --color-border-hover: var(--vt-c-divider-light-1);
32
+
33
+ --color-heading: var(--vt-c-text-light-1);
34
+ --color-text: var(--vt-c-text-light-1);
35
+
36
+ --section-gap: 160px;
37
+ }
38
+
39
+ @media (prefers-color-scheme: dark) {
40
+ :root {
41
+ --color-background: var(--vt-c-black);
42
+ --color-background-soft: var(--vt-c-black-soft);
43
+ --color-background-mute: var(--vt-c-black-mute);
44
+
45
+ --color-border: var(--vt-c-divider-dark-2);
46
+ --color-border-hover: var(--vt-c-divider-dark-1);
47
+
48
+ --color-heading: var(--vt-c-text-dark-1);
49
+ --color-text: var(--vt-c-text-dark-2);
50
+ }
51
+ }
52
+
53
+ *,
54
+ *::before,
55
+ *::after {
56
+ box-sizing: border-box;
57
+ margin: 0;
58
+ font-weight: normal;
59
+ }
60
+
61
+ body {
62
+ min-height: 100vh;
63
+ color: var(--color-text);
64
+ background: var(--color-background);
65
+ transition:
66
+ color 0.5s,
67
+ background-color 0.5s;
68
+ line-height: 1.6;
69
+ font-family:
70
+ Inter,
71
+ -apple-system,
72
+ BlinkMacSystemFont,
73
+ 'Segoe UI',
74
+ Roboto,
75
+ Oxygen,
76
+ Ubuntu,
77
+ Cantarell,
78
+ 'Fira Sans',
79
+ 'Droid Sans',
80
+ 'Helvetica Neue',
81
+ sans-serif;
82
+ font-size: 15px;
83
+ text-rendering: optimizeLegibility;
84
+ -webkit-font-smoothing: antialiased;
85
+ -moz-osx-font-smoothing: grayscale;
86
+ }
@@ -0,0 +1,35 @@
1
+ @import './base.css';
2
+
3
+ #app {
4
+ max-width: 1280px;
5
+ margin: 0 auto;
6
+ padding: 2rem;
7
+ font-weight: normal;
8
+ }
9
+
10
+ a,
11
+ .green {
12
+ text-decoration: none;
13
+ color: hsla(160, 100%, 37%, 1);
14
+ transition: 0.4s;
15
+ padding: 3px;
16
+ }
17
+
18
+ @media (hover: hover) {
19
+ a:hover {
20
+ background-color: hsla(160, 100%, 37%, 0.2);
21
+ }
22
+ }
23
+
24
+ @media (min-width: 1024px) {
25
+ body {
26
+ display: flex;
27
+ place-items: center;
28
+ }
29
+
30
+ #app {
31
+ display: grid;
32
+ grid-template-columns: 1fr 1fr;
33
+ padding: 0 2rem;
34
+ }
35
+ }
@@ -0,0 +1,110 @@
1
+ import { Mark, mergeAttributes } from '@tiptap/core'
2
+ import { TextSelection } from '@tiptap/pm/state'
3
+
4
+ import { shortId } from '../utils/short-id'
5
+
6
+ // 书签格式 创建一个书签
7
+ export default Mark.create({
8
+ name: 'bookmark',
9
+ priority: 1000,
10
+ keepOnSplit: false,
11
+ exitable: true,
12
+ addOptions() {
13
+ return {
14
+ bookmarkName: '',
15
+ class: 'umo-editor-bookmark',
16
+ }
17
+ },
18
+ addAttributes() {
19
+ return {
20
+ bookmarkName: {
21
+ default: 'bookmarkName',
22
+ },
23
+ class: {
24
+ default: this.options.class,
25
+ },
26
+ }
27
+ },
28
+
29
+ parseHTML() {
30
+ return [
31
+ {
32
+ tag: 'bookmark',
33
+ },
34
+ ]
35
+ },
36
+
37
+ renderHTML({ HTMLAttributes }) {
38
+ return ['bookmark', mergeAttributes(this.options, HTMLAttributes), 0]
39
+ },
40
+
41
+ addCommands() {
42
+ return {
43
+ // 设置书签 若书签有选中区域数据 否则默认值为书签名称
44
+ setBookmark:
45
+ (attributes) =>
46
+ ({ chain, editor }) => {
47
+ try {
48
+ chain().setMark(this.name, attributes).run()
49
+ const { empty } = editor.state.selection
50
+ if (empty && attributes.bookmarkName) {
51
+ chain().focus().insertContent(attributes.bookmarkName).run()
52
+ }
53
+ return true
54
+ } catch (e) {
55
+ return false
56
+ }
57
+ },
58
+ focusBookmark:
59
+ (bookmarkName) =>
60
+ ({ editor, tr }) => {
61
+ if (bookmarkName) {
62
+ const element = editor.view.dom.querySelector(
63
+ `bookmark[bookmarkName="${bookmarkName}"]`,
64
+ )
65
+ if (element) {
66
+ element.scrollIntoView({
67
+ behavior: 'smooth',
68
+ block: 'center',
69
+ inline: 'nearest',
70
+ })
71
+ const pos = editor.view.posAtDOM(element, 0)
72
+ if (tr) {
73
+ tr.setSelection(new TextSelection(tr.doc.resolve(pos)))
74
+ editor.view.dispatch(tr)
75
+ editor.view.focus()
76
+ }
77
+ }
78
+ return true
79
+ } else return false
80
+ },
81
+ getAllBookmarks:
82
+ (callback) =>
83
+ ({ editor }) => {
84
+ const bookmarkData = []
85
+ try {
86
+ const alltext = editor.getHTML()
87
+ const parser = new DOMParser()
88
+ const doc = parser.parseFromString(alltext, 'text/html')
89
+ // 获取所有的 <bookmark> 元素
90
+ const bookmarks = doc.body.querySelectorAll(this.name)
91
+ const keyNode = []
92
+ Array.from(bookmarks).forEach((node) => {
93
+ if (node !== null) {
94
+ const bookName = node.getAttribute('bookmarkName')
95
+ if (bookName && !keyNode.includes(bookName)) {
96
+ keyNode.push(bookName)
97
+ bookmarkData.push({
98
+ bookmarkRowId: shortId(),
99
+ bookmarkRowName: bookName,
100
+ })
101
+ }
102
+ }
103
+ })
104
+ } catch (e) {}
105
+ callback(bookmarkData)
106
+ return true
107
+ },
108
+ }
109
+ },
110
+ })
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.8",
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>