adtec-core-package 3.1.7 → 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.
@@ -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,21 @@
1
1
  import * as XLSX from 'xlsx-js-style'
2
+ import { unzipSync } from 'fflate'
3
+
4
+ export interface ExcelPreviewCellStyle {
5
+ backgroundColor?: string
6
+ color?: string
7
+ fontWeight?: string
8
+ fontStyle?: string
9
+ textDecoration?: string
10
+ textAlign?: 'left' | 'center' | 'right' | 'justify'
11
+ verticalAlign?: 'top' | 'middle' | 'bottom'
12
+ borderTop?: string
13
+ borderRight?: string
14
+ borderBottom?: string
15
+ borderLeft?: string
16
+ fontSize?: string
17
+ whiteSpace?: string
18
+ }
2
19
 
3
20
  export interface ExcelPreviewCell {
4
21
  value: string
@@ -6,11 +23,16 @@ export interface ExcelPreviewCell {
6
23
  col: number
7
24
  rowSpan?: number
8
25
  colSpan?: number
26
+ style?: ExcelPreviewCellStyle
27
+ width?: number
9
28
  }
10
29
 
11
30
  export interface ExcelPreviewSheet {
12
31
  name: string
13
32
  rows: ExcelPreviewCell[][]
33
+ startCol: number
34
+ endCol: number
35
+ colWidths: Record<number, number>
14
36
  }
15
37
 
16
38
  export interface ExcelSearchMatch {
@@ -20,6 +42,326 @@ export interface ExcelSearchMatch {
20
42
  text: string
21
43
  }
22
44
 
45
+ type ColorStyle = { rgb?: string; theme?: number; tint?: number; indexed?: number }
46
+
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) */
55
+ const EXCEL_THEME_COLORS: Record<number, string> = {
56
+ 0: '#FFFFFF',
57
+ 1: '#000000',
58
+ 2: '#E7E6E6',
59
+ 3: '#44546A',
60
+ 4: '#4472C4',
61
+ 5: '#ED7D31',
62
+ 6: '#A5A5A5',
63
+ 7: '#FFC000',
64
+ 8: '#5B9BD5',
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',
128
+ }
129
+
130
+ const BORDER_WIDTH: Record<string, string> = {
131
+ hair: '1px',
132
+ thin: '1px',
133
+ medium: '2px',
134
+ thick: '3px',
135
+ dashed: '1px',
136
+ dotted: '1px',
137
+ double: '3px',
138
+ }
139
+
140
+ const DEFAULT_COL_WIDTH = 72
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
+
152
+ const hexToRgb = (hex: string) => {
153
+ const normalized = hex.replace('#', '')
154
+ const value =
155
+ normalized.length === 8 ? normalized.slice(2) : normalized.length === 6 ? normalized : ''
156
+ if (value.length !== 6) return null
157
+ return {
158
+ r: parseInt(value.slice(0, 2), 16),
159
+ g: parseInt(value.slice(2, 4), 16),
160
+ b: parseInt(value.slice(4, 6), 16),
161
+ }
162
+ }
163
+
164
+ const rgbToHex = (r: number, g: number, b: number) =>
165
+ `#${[r, g, b]
166
+ .map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, '0'))
167
+ .join('')}`
168
+
169
+ const applyTint = (hex: string, tint: number) => {
170
+ const rgb = hexToRgb(hex)
171
+ if (!rgb) return hex
172
+ const mix = tint < 0 ? { r: 0, g: 0, b: 0 } : { r: 255, g: 255, b: 255 }
173
+ const ratio = Math.abs(tint)
174
+ return rgbToHex(
175
+ rgb.r + (mix.r - rgb.r) * ratio,
176
+ rgb.g + (mix.g - rgb.g) * ratio,
177
+ rgb.b + (mix.b - rgb.b) * ratio,
178
+ )
179
+ }
180
+
181
+ const resolveColor = (color?: ColorStyle): string | undefined => {
182
+ if (!color) return undefined
183
+ if (color.rgb) {
184
+ const raw = color.rgb.replace(/[^0-9a-f]/gi, '')
185
+ if (raw.length === 8) return `#${raw.slice(2)}`
186
+ if (raw.length === 6) return `#${raw}`
187
+ return undefined
188
+ }
189
+ if (color.indexed != null) return INDEXED_COLORS[color.indexed]
190
+ if (color.theme != null) {
191
+ const base = EXCEL_THEME_COLORS[color.theme] ?? '#000000'
192
+ return color.tint ? applyTint(base, color.tint) : base
193
+ }
194
+ return undefined
195
+ }
196
+
197
+ const resolveBorderSide = (side?: { style?: string; color?: ColorStyle }) => {
198
+ if (!side?.style || side.style === 'none') return undefined
199
+ const width = BORDER_WIDTH[side.style] ?? '1px'
200
+ const color = resolveColor(side.color) ?? '#000000'
201
+ const lineStyle = side.style === 'dashed' ? 'dashed' : side.style === 'dotted' ? 'dotted' : 'solid'
202
+ return `${width} ${lineStyle} ${color}`
203
+ }
204
+
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 => {
218
+ const css: ExcelPreviewCellStyle = {}
219
+
220
+ const fill = parts.fill
221
+ if (fill && fill.patternType !== 'none') {
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
226
+ }
227
+
228
+ const font = parts.font
229
+ if (font) {
230
+ if (isTruthy(font.bold)) css.fontWeight = 'bold'
231
+ if (isTruthy(font.italic)) css.fontStyle = 'italic'
232
+ if (font.underline) css.textDecoration = 'underline'
233
+ if (font.sz) css.fontSize = `${font.sz}pt`
234
+ const color = resolveColor(font.color as ColorStyle | undefined)
235
+ if (color) css.color = color
236
+ }
237
+
238
+ const alignment = parts.alignment
239
+ if (alignment) {
240
+ const horizontal = alignment.horizontal as string | undefined
241
+ if (horizontal && horizontal !== 'general') {
242
+ css.textAlign = horizontal as ExcelPreviewCellStyle['textAlign']
243
+ }
244
+ const vertical = alignment.vertical as string | undefined
245
+ if (vertical) {
246
+ css.verticalAlign =
247
+ vertical === 'center' ? 'middle' : (vertical as ExcelPreviewCellStyle['verticalAlign'])
248
+ }
249
+ if (isTruthy(alignment.wrapText)) css.whiteSpace = 'pre-wrap'
250
+ }
251
+
252
+ const border = parts.border
253
+ if (border) {
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 })
258
+ if (top) css.borderTop = top
259
+ if (right) css.borderRight = right
260
+ if (bottom) css.borderBottom = bottom
261
+ if (left) css.borderLeft = left
262
+ }
263
+
264
+ return Object.keys(css).length > 0 ? css : undefined
265
+ }
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
+
344
+ export const cellStyleToCss = (style?: ExcelPreviewCellStyle): Record<string, string> => {
345
+ if (!style) return {}
346
+ const css: Record<string, string> = {}
347
+ if (style.backgroundColor) css.backgroundColor = style.backgroundColor
348
+ if (style.color) css.color = style.color
349
+ if (style.fontWeight) css.fontWeight = style.fontWeight
350
+ if (style.fontStyle) css.fontStyle = style.fontStyle
351
+ if (style.textDecoration) css.textDecoration = style.textDecoration
352
+ if (style.textAlign) css.textAlign = style.textAlign
353
+ if (style.verticalAlign) css.verticalAlign = style.verticalAlign
354
+ if (style.borderTop) css.borderTop = style.borderTop
355
+ if (style.borderRight) css.borderRight = style.borderRight
356
+ if (style.borderBottom) css.borderBottom = style.borderBottom
357
+ if (style.borderLeft) css.borderLeft = style.borderLeft
358
+ if (style.fontSize) css.fontSize = style.fontSize
359
+ if (style.whiteSpace) css.whiteSpace = style.whiteSpace
360
+ return css
361
+ }
362
+
363
+ export const toExcelColumnLabel = (colIndex: number) => XLSX.utils.encode_col(colIndex)
364
+
23
365
  const formatCellValue = (cell?: XLSX.CellObject) => {
24
366
  if (!cell) return ''
25
367
  if (cell.w != null && cell.w !== '') return String(cell.w)
@@ -31,18 +373,52 @@ const formatCellValue = (cell?: XLSX.CellObject) => {
31
373
  return String(cell.v)
32
374
  }
33
375
 
376
+ const parseColWidths = (sheet: XLSX.WorkSheet, startCol: number, endCol: number) => {
377
+ const cols = sheet['!cols'] || []
378
+ const widths: Record<number, number> = {}
379
+ for (let c = startCol; c <= endCol; c++) {
380
+ const col = cols[c]
381
+ if (col?.wpx) widths[c] = col.wpx
382
+ else if (col?.wch) widths[c] = Math.max(48, col.wch * 7)
383
+ else widths[c] = DEFAULT_COL_WIDTH
384
+ }
385
+ return widths
386
+ }
387
+
34
388
  export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<ExcelPreviewSheet[]> => {
35
389
  const buffer = src instanceof Blob ? await src.arrayBuffer() : src
36
- const workbook = XLSX.read(buffer, { type: 'array', cellDates: true })
37
- return workbook.SheetNames.map((name) => ({
38
- name,
39
- rows: buildSheetRows(workbook.Sheets[name]),
40
- }))
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) => {
402
+ const sheet = workbook.Sheets[name]
403
+ const range = sheet?.['!ref'] ? XLSX.utils.decode_range(sheet['!ref']) : null
404
+ return {
405
+ name,
406
+ rows: buildSheetRows(sheet, styles, styleIndexMaps[sheetIndex]),
407
+ startCol: range?.s.c ?? 0,
408
+ endCol: range?.e.c ?? 0,
409
+ colWidths: range ? parseColWidths(sheet, range.s.c, range.e.c) : {},
410
+ }
411
+ })
41
412
  }
42
413
 
43
- const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
414
+ const buildSheetRows = (
415
+ sheet?: XLSX.WorkSheet,
416
+ styles?: WorkbookStyles,
417
+ styleIndexMap?: Map<string, number>,
418
+ ): ExcelPreviewCell[][] => {
44
419
  if (!sheet || !sheet['!ref']) return []
45
420
  const range = XLSX.utils.decode_range(sheet['!ref'])
421
+ const colWidths = parseColWidths(sheet, range.s.c, range.e.c)
46
422
  const mergeStarts = new Map<string, { rowSpan: number; colSpan: number }>()
47
423
  const covered = new Set<string>()
48
424
 
@@ -63,13 +439,17 @@ const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
63
439
  for (let c = range.s.c; c <= range.e.c; c++) {
64
440
  if (covered.has(`${r},${c}`)) continue
65
441
  const addr = XLSX.utils.encode_cell({ r, c })
442
+ const cellObj = sheet[addr]
66
443
  const merge = mergeStarts.get(`${r},${c}`)
444
+ const xfIndex = styleIndexMap?.get(addr)
67
445
  row.push({
68
- value: formatCellValue(sheet[addr]),
446
+ value: formatCellValue(cellObj),
69
447
  row: r,
70
448
  col: c,
71
449
  rowSpan: merge?.rowSpan,
72
450
  colSpan: merge?.colSpan,
451
+ style: extractCellStyle(cellObj, styles, xfIndex),
452
+ width: colWidths[c],
73
453
  })
74
454
  }
75
455
  if (row.length > 0) rows.push(row)
@@ -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
+ }