adtec-core-package 3.1.8 → 3.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adtec-core-package",
3
- "version": "3.1.8",
3
+ "version": "3.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -82,6 +82,7 @@
82
82
  "es-drager": "1.2.11",
83
83
  "exceljs": "4.4.0",
84
84
  "fast-glob": "3.3.3",
85
+ "fflate": "^0.3.8",
85
86
  "file-saver": "2.0.5",
86
87
  "hotkeys-js": "4.0.2",
87
88
  "jsbarcode": "3.12.3",
@@ -5,7 +5,7 @@
5
5
  <template>
6
6
  <Teleport to="body" v-if="model">
7
7
  <div
8
- v-loading="loading"
8
+ v-loading="showOverlayLoading"
9
9
  element-loading-text="加载中,请稍后。。。"
10
10
  class="file-view-overlay"
11
11
  >
@@ -86,11 +86,16 @@ const isHeaderPreview = computed(() =>
86
86
  isHeaderPreviewFile(undefined, title.value, url.value),
87
87
  )
88
88
 
89
+ /** 富文本预览由 OfficePreview 自带 loading,避免与 v-loading 双层转圈 */
90
+ const showOverlayLoading = computed(() => loading.value && !isHeaderPreview.value)
91
+
89
92
  watch(
90
93
  [model, url],
91
94
  ([visible, fileUrl]) => {
92
95
  if (visible && fileUrl) {
93
- loading.value = true
96
+ if (!isHeaderPreview.value) {
97
+ loading.value = true
98
+ }
94
99
  previewScale.value = 1
95
100
  } else if (!visible) {
96
101
  loading.value = false
@@ -71,7 +71,7 @@
71
71
  @error="handleError"
72
72
  @page-change="previewCurrentPage = $event"
73
73
  />
74
- <div v-if="isPreviewLoading" class="office-rich-loading">
74
+ <div v-if="isPreviewLoading && !fileSrc" class="office-rich-loading">
75
75
  <div class="loading-card">
76
76
  <span class="loading-spinner" />
77
77
  <p class="loading-title">{{ loadingMessage }}</p>
@@ -43,7 +43,7 @@ import {
43
43
  import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
44
44
  import pdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.mjs?url'
45
45
  import { searchPdfDocument, type PdfSearchMatch } from '../../utils/pdfSearchUtil'
46
- import { computePdfDefaultFitScale } from '../../utils/officePreviewUtil.ts'
46
+ import { computePdfDefaultFitScale, buildPdfJsLoadOptions } from '../../utils/officePreviewUtil.ts'
47
47
 
48
48
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
49
49
 
@@ -667,7 +667,7 @@ const loadPdf = async (src: Blob | ArrayBuffer) => {
667
667
  const data = await toArrayBuffer(src)
668
668
  if (token !== loadToken) return
669
669
 
670
- const loadingTask = pdfjsLib.getDocument({ data })
670
+ const loadingTask = pdfjsLib.getDocument(buildPdfJsLoadOptions(data))
671
671
  activeTask = loadingTask
672
672
  const doc = await loadingTask.promise
673
673
  if (token !== loadToken) return
@@ -1,4 +1,5 @@
1
1
  import * as XLSX from 'xlsx-js-style'
2
+ import { unzipSync } from 'fflate'
2
3
 
3
4
  export interface ExcelPreviewCellStyle {
4
5
  backgroundColor?: string
@@ -43,7 +44,14 @@ export interface ExcelSearchMatch {
43
44
 
44
45
  type ColorStyle = { rgb?: string; theme?: number; tint?: number; indexed?: number }
45
46
 
46
- /** Excel 默认主题色(Office 2013+) */
47
+ type WorkbookStyles = {
48
+ Fonts?: Array<Record<string, unknown>>
49
+ Fills?: Array<Record<string, unknown>>
50
+ Borders?: Array<Record<string, unknown>>
51
+ CellXf?: Array<Record<string, unknown>>
52
+ }
53
+
54
+ /** Excel 默认主题色(Office 2013+,含 accent / hyperlink) */
47
55
  const EXCEL_THEME_COLORS: Record<number, string> = {
48
56
  0: '#FFFFFF',
49
57
  1: '#000000',
@@ -55,6 +63,68 @@ const EXCEL_THEME_COLORS: Record<number, string> = {
55
63
  7: '#FFC000',
56
64
  8: '#5B9BD5',
57
65
  9: '#70AD47',
66
+ 10: '#0563C1',
67
+ 11: '#954F72',
68
+ }
69
+
70
+ /** Excel 标准 indexed 色板(常用前 16 色) */
71
+ const INDEXED_COLORS: Record<number, string> = {
72
+ 0: '#000000',
73
+ 1: '#FFFFFF',
74
+ 2: '#FF0000',
75
+ 3: '#00FF00',
76
+ 4: '#0000FF',
77
+ 5: '#FFFF00',
78
+ 6: '#FF00FF',
79
+ 7: '#00FFFF',
80
+ 8: '#000000',
81
+ 9: '#FFFFFF',
82
+ 10: '#FF0000',
83
+ 11: '#00FF00',
84
+ 12: '#0000FF',
85
+ 13: '#FFFF00',
86
+ 14: '#FF00FF',
87
+ 15: '#00FFFF',
88
+ 16: '#800000',
89
+ 17: '#008000',
90
+ 18: '#000080',
91
+ 19: '#808000',
92
+ 20: '#800080',
93
+ 21: '#008080',
94
+ 22: '#C0C0C0',
95
+ 23: '#808080',
96
+ 24: '#9999FF',
97
+ 25: '#993366',
98
+ 26: '#FFFFCC',
99
+ 27: '#CCFFFF',
100
+ 28: '#660066',
101
+ 29: '#FF8080',
102
+ 30: '#0066CC',
103
+ 31: '#CCCCFF',
104
+ 40: '#00CCFF',
105
+ 41: '#CCFFFF',
106
+ 42: '#CCFFCC',
107
+ 43: '#FFFF99',
108
+ 44: '#99CCFF',
109
+ 45: '#FF99CC',
110
+ 46: '#CC99FF',
111
+ 47: '#FFCC99',
112
+ 48: '#3366FF',
113
+ 49: '#33CCCC',
114
+ 50: '#99CC00',
115
+ 51: '#FFCC00',
116
+ 52: '#FF9900',
117
+ 53: '#FF6600',
118
+ 54: '#666699',
119
+ 55: '#969696',
120
+ 56: '#003366',
121
+ 57: '#339966',
122
+ 58: '#003300',
123
+ 59: '#333300',
124
+ 60: '#993300',
125
+ 61: '#993366',
126
+ 62: '#333399',
127
+ 63: '#333333',
58
128
  }
59
129
 
60
130
  const BORDER_WIDTH: Record<string, string> = {
@@ -69,6 +139,16 @@ const BORDER_WIDTH: Record<string, string> = {
69
139
 
70
140
  const DEFAULT_COL_WIDTH = 72
71
141
 
142
+ const isOleXls = (buffer: ArrayBuffer) => {
143
+ const sig = new Uint8Array(buffer, 0, 4)
144
+ return sig[0] === 0xd0 && sig[1] === 0xcf && sig[2] === 0x11 && sig[3] === 0xe0
145
+ }
146
+
147
+ const isZipXlsx = (buffer: ArrayBuffer) => {
148
+ const sig = new Uint8Array(buffer, 0, 2)
149
+ return sig[0] === 0x50 && sig[1] === 0x4b
150
+ }
151
+
72
152
  const hexToRgb = (hex: string) => {
73
153
  const normalized = hex.replace('#', '')
74
154
  const value =
@@ -106,8 +186,9 @@ const resolveColor = (color?: ColorStyle): string | undefined => {
106
186
  if (raw.length === 6) return `#${raw}`
107
187
  return undefined
108
188
  }
189
+ if (color.indexed != null) return INDEXED_COLORS[color.indexed]
109
190
  if (color.theme != null) {
110
- const base = EXCEL_THEME_COLORS[color.theme] ?? '#FFFFFF'
191
+ const base = EXCEL_THEME_COLORS[color.theme] ?? '#000000'
111
192
  return color.tint ? applyTint(base, color.tint) : base
112
193
  }
113
194
  return undefined
@@ -116,63 +197,64 @@ const resolveColor = (color?: ColorStyle): string | undefined => {
116
197
  const resolveBorderSide = (side?: { style?: string; color?: ColorStyle }) => {
117
198
  if (!side?.style || side.style === 'none') return undefined
118
199
  const width = BORDER_WIDTH[side.style] ?? '1px'
119
- const color = resolveColor(side.color) ?? '#d0d0d0'
200
+ const color = resolveColor(side.color) ?? '#000000'
120
201
  const lineStyle = side.style === 'dashed' ? 'dashed' : side.style === 'dotted' ? 'dotted' : 'solid'
121
202
  return `${width} ${lineStyle} ${color}`
122
203
  }
123
204
 
124
- const extractCellStyle = (cell?: XLSX.CellObject): ExcelPreviewCellStyle | undefined => {
125
- const rawStyle = cell?.s
126
- if (!rawStyle || typeof rawStyle === 'number') return undefined
127
- const style = rawStyle as Record<string, unknown>
205
+ const toNumber = (value: unknown, fallback = 0) => {
206
+ const num = Number(value)
207
+ return Number.isFinite(num) ? num : fallback
208
+ }
209
+
210
+ const isTruthy = (value: unknown) => value === true || value === 1 || value === '1'
211
+
212
+ const styleFromParts = (parts: {
213
+ font?: Record<string, unknown>
214
+ fill?: Record<string, unknown>
215
+ border?: Record<string, unknown>
216
+ alignment?: Record<string, unknown>
217
+ }): ExcelPreviewCellStyle | undefined => {
128
218
  const css: ExcelPreviewCellStyle = {}
129
219
 
130
- const fill = style.fill as { patternType?: string; fgColor?: ColorStyle; bgColor?: ColorStyle } | undefined
220
+ const fill = parts.fill
131
221
  if (fill && fill.patternType !== 'none') {
132
- const bg = resolveColor(fill.fgColor) || resolveColor(fill.bgColor)
133
- if (bg) css.backgroundColor = bg
222
+ const fg = fill.fgColor as ColorStyle | undefined
223
+ const bg = fill.bgColor as ColorStyle | undefined
224
+ const background = resolveColor(fg) || resolveColor(bg)
225
+ if (background) css.backgroundColor = background
134
226
  }
135
227
 
136
- const font = style.font as {
137
- bold?: boolean
138
- italic?: boolean
139
- underline?: boolean | string
140
- sz?: number | string
141
- color?: ColorStyle
142
- } | undefined
228
+ const font = parts.font
143
229
  if (font) {
144
- if (font.bold) css.fontWeight = 'bold'
145
- if (font.italic) css.fontStyle = 'italic'
230
+ if (isTruthy(font.bold)) css.fontWeight = 'bold'
231
+ if (isTruthy(font.italic)) css.fontStyle = 'italic'
146
232
  if (font.underline) css.textDecoration = 'underline'
147
233
  if (font.sz) css.fontSize = `${font.sz}pt`
148
- const color = resolveColor(font.color)
234
+ const color = resolveColor(font.color as ColorStyle | undefined)
149
235
  if (color) css.color = color
150
236
  }
151
237
 
152
- const alignment = style.alignment as {
153
- horizontal?: string
154
- vertical?: string
155
- wrapText?: boolean
156
- } | undefined
238
+ const alignment = parts.alignment
157
239
  if (alignment) {
158
- if (alignment.horizontal && alignment.horizontal !== 'general') {
159
- css.textAlign = alignment.horizontal as ExcelPreviewCellStyle['textAlign']
240
+ const horizontal = alignment.horizontal as string | undefined
241
+ if (horizontal && horizontal !== 'general') {
242
+ css.textAlign = horizontal as ExcelPreviewCellStyle['textAlign']
160
243
  }
161
- if (alignment.vertical) {
244
+ const vertical = alignment.vertical as string | undefined
245
+ if (vertical) {
162
246
  css.verticalAlign =
163
- alignment.vertical === 'center'
164
- ? 'middle'
165
- : (alignment.vertical as ExcelPreviewCellStyle['verticalAlign'])
247
+ vertical === 'center' ? 'middle' : (vertical as ExcelPreviewCellStyle['verticalAlign'])
166
248
  }
167
- if (alignment.wrapText) css.whiteSpace = 'pre-wrap'
249
+ if (isTruthy(alignment.wrapText)) css.whiteSpace = 'pre-wrap'
168
250
  }
169
251
 
170
- const border = style.border as Record<string, { style?: string; color?: ColorStyle }> | undefined
252
+ const border = parts.border
171
253
  if (border) {
172
- const top = resolveBorderSide(border.top)
173
- const right = resolveBorderSide(border.right)
174
- const bottom = resolveBorderSide(border.bottom)
175
- const left = resolveBorderSide(border.left)
254
+ const top = resolveBorderSide(border.top as { style?: string; color?: ColorStyle })
255
+ const right = resolveBorderSide(border.right as { style?: string; color?: ColorStyle })
256
+ const bottom = resolveBorderSide(border.bottom as { style?: string; color?: ColorStyle })
257
+ const left = resolveBorderSide(border.left as { style?: string; color?: ColorStyle })
176
258
  if (top) css.borderTop = top
177
259
  if (right) css.borderRight = right
178
260
  if (bottom) css.borderBottom = bottom
@@ -182,6 +264,83 @@ const extractCellStyle = (cell?: XLSX.CellObject): ExcelPreviewCellStyle | undef
182
264
  return Object.keys(css).length > 0 ? css : undefined
183
265
  }
184
266
 
267
+ const resolveXfStyle = (
268
+ styles: WorkbookStyles | undefined,
269
+ xfIndex: number,
270
+ ): ExcelPreviewCellStyle | undefined => {
271
+ if (!styles?.CellXf?.length || xfIndex < 0) return undefined
272
+ const xf = styles.CellXf[xfIndex]
273
+ if (!xf) return undefined
274
+
275
+ const fontId = toNumber(xf.fontId ?? xf.fontid, 0)
276
+ const fillId = toNumber(xf.fillId ?? xf.fillid, 0)
277
+ const borderId = toNumber(xf.borderId ?? xf.borderid, 0)
278
+
279
+ return styleFromParts({
280
+ font: styles.Fonts?.[fontId] as Record<string, unknown> | undefined,
281
+ fill: styles.Fills?.[fillId] as Record<string, unknown> | undefined,
282
+ border: styles.Borders?.[borderId] as Record<string, unknown> | undefined,
283
+ alignment: xf.alignment as Record<string, unknown> | undefined,
284
+ })
285
+ }
286
+
287
+ /** xlsx-js-style 读入后 cell.s 往往只有 fill,字体/边框需用 XML 中的 s 索引关联 CellXf */
288
+ const parseXlsxStyleIndexMaps = (buffer: ArrayBuffer, sheetCount: number) => {
289
+ if (!isZipXlsx(buffer) || sheetCount <= 0) return [] as Map<string, number>[]
290
+
291
+ try {
292
+ const files = unzipSync(new Uint8Array(buffer))
293
+ const decoder = new TextDecoder('utf-8')
294
+ const maps: Map<string, number>[] = []
295
+
296
+ for (let i = 0; i < sheetCount; i++) {
297
+ const path = `xl/worksheets/sheet${i + 1}.xml`
298
+ const xmlBytes = files[path]
299
+ const map = new Map<string, number>()
300
+ if (xmlBytes) {
301
+ const xml = decoder.decode(xmlBytes)
302
+ const cellTagRe = /<c\b([^>/]*)(?:\/>|>)/g
303
+ let match: RegExpExecArray | null
304
+ while ((match = cellTagRe.exec(xml))) {
305
+ const attrs = match[1]
306
+ const addr = attrs.match(/\br="([^"]+)"/)?.[1]
307
+ const styleIndex = attrs.match(/\bs="(\d+)"/)?.[1]
308
+ if (addr && styleIndex != null) {
309
+ map.set(addr, Number(styleIndex))
310
+ }
311
+ }
312
+ }
313
+ maps.push(map)
314
+ }
315
+ return maps
316
+ } catch {
317
+ return Array.from({ length: sheetCount }, () => new Map<string, number>())
318
+ }
319
+ }
320
+
321
+ const extractCellStyle = (
322
+ cell?: XLSX.CellObject,
323
+ styles?: WorkbookStyles,
324
+ xfIndex?: number,
325
+ ): ExcelPreviewCellStyle | undefined => {
326
+ if (styles && xfIndex != null && xfIndex >= 0) {
327
+ const resolved = resolveXfStyle(styles, xfIndex)
328
+ if (resolved) return resolved
329
+ }
330
+
331
+ if (typeof cell?.s === 'number' && styles) {
332
+ return resolveXfStyle(styles, cell.s)
333
+ }
334
+
335
+ const rawStyle = cell?.s
336
+ if (!rawStyle || typeof rawStyle !== 'object') return undefined
337
+
338
+ // cell.s 对象通常仅含 fill,作为兜底
339
+ return styleFromParts({
340
+ fill: rawStyle as Record<string, unknown>,
341
+ })
342
+ }
343
+
185
344
  export const cellStyleToCss = (style?: ExcelPreviewCellStyle): Record<string, string> => {
186
345
  if (!style) return {}
187
346
  const css: Record<string, string> = {}
@@ -228,13 +387,23 @@ const parseColWidths = (sheet: XLSX.WorkSheet, startCol: number, endCol: number)
228
387
 
229
388
  export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<ExcelPreviewSheet[]> => {
230
389
  const buffer = src instanceof Blob ? await src.arrayBuffer() : src
231
- const workbook = XLSX.read(buffer, { type: 'array', cellDates: true, cellStyles: true })
232
- return workbook.SheetNames.map((name) => {
390
+ const workbook = XLSX.read(buffer, {
391
+ type: 'array',
392
+ cellDates: true,
393
+ cellStyles: true,
394
+ ...(isOleXls(buffer) ? {} : {}),
395
+ })
396
+ const styles = workbook.Styles as WorkbookStyles | undefined
397
+ const styleIndexMaps = isZipXlsx(buffer)
398
+ ? parseXlsxStyleIndexMaps(buffer, workbook.SheetNames.length)
399
+ : []
400
+
401
+ return workbook.SheetNames.map((name, sheetIndex) => {
233
402
  const sheet = workbook.Sheets[name]
234
403
  const range = sheet?.['!ref'] ? XLSX.utils.decode_range(sheet['!ref']) : null
235
404
  return {
236
405
  name,
237
- rows: buildSheetRows(sheet),
406
+ rows: buildSheetRows(sheet, styles, styleIndexMaps[sheetIndex]),
238
407
  startCol: range?.s.c ?? 0,
239
408
  endCol: range?.e.c ?? 0,
240
409
  colWidths: range ? parseColWidths(sheet, range.s.c, range.e.c) : {},
@@ -242,7 +411,11 @@ export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<Excel
242
411
  })
243
412
  }
244
413
 
245
- const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
414
+ const buildSheetRows = (
415
+ sheet?: XLSX.WorkSheet,
416
+ styles?: WorkbookStyles,
417
+ styleIndexMap?: Map<string, number>,
418
+ ): ExcelPreviewCell[][] => {
246
419
  if (!sheet || !sheet['!ref']) return []
247
420
  const range = XLSX.utils.decode_range(sheet['!ref'])
248
421
  const colWidths = parseColWidths(sheet, range.s.c, range.e.c)
@@ -268,13 +441,14 @@ const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
268
441
  const addr = XLSX.utils.encode_cell({ r, c })
269
442
  const cellObj = sheet[addr]
270
443
  const merge = mergeStarts.get(`${r},${c}`)
444
+ const xfIndex = styleIndexMap?.get(addr)
271
445
  row.push({
272
446
  value: formatCellValue(cellObj),
273
447
  row: r,
274
448
  col: c,
275
449
  rowSpan: merge?.rowSpan,
276
450
  colSpan: merge?.colSpan,
277
- style: extractCellStyle(cellObj),
451
+ style: extractCellStyle(cellObj, styles, xfIndex),
278
452
  width: colWidths[c],
279
453
  })
280
454
  }
@@ -120,3 +120,20 @@ export const isLegacyDocFile = (type?: string, name?: string, fileUrl?: string)
120
120
  /** 弹层等场景:使用 OfficePreview 内置标题栏(PDF / Word / Excel) */
121
121
  export const isHeaderPreviewFile = (type?: string, name?: string, fileUrl?: string) =>
122
122
  HEADER_PREVIEW_TYPES.has(toOfficePreviewType(type, name, fileUrl))
123
+
124
+ const pdfAssetBase = () => {
125
+ const base = import.meta.env.BASE_URL || '/'
126
+ return base.endsWith('/') ? base : `${base}/`
127
+ }
128
+
129
+ /** pdf.js 加载参数:CJK 预览需 cMap;缺嵌入字体时尝试系统字体 */
130
+ export const buildPdfJsLoadOptions = (data: ArrayBuffer) => {
131
+ const assetBase = pdfAssetBase()
132
+ return {
133
+ data,
134
+ useSystemFonts: true,
135
+ cMapUrl: `${assetBase}pdfjs/cmaps/`,
136
+ cMapPacked: true,
137
+ standardFontDataUrl: `${assetBase}pdfjs/standard_fonts/`,
138
+ }
139
+ }