adtec-core-package 3.1.7 → 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.
@@ -1,16 +1,37 @@
1
1
  import * as XLSX from 'xlsx-js-style'
2
2
 
3
+ export interface ExcelPreviewCellStyle {
4
+ backgroundColor?: string
5
+ color?: string
6
+ fontWeight?: string
7
+ fontStyle?: string
8
+ textDecoration?: string
9
+ textAlign?: 'left' | 'center' | 'right' | 'justify'
10
+ verticalAlign?: 'top' | 'middle' | 'bottom'
11
+ borderTop?: string
12
+ borderRight?: string
13
+ borderBottom?: string
14
+ borderLeft?: string
15
+ fontSize?: string
16
+ whiteSpace?: string
17
+ }
18
+
3
19
  export interface ExcelPreviewCell {
4
20
  value: string
5
21
  row: number
6
22
  col: number
7
23
  rowSpan?: number
8
24
  colSpan?: number
25
+ style?: ExcelPreviewCellStyle
26
+ width?: number
9
27
  }
10
28
 
11
29
  export interface ExcelPreviewSheet {
12
30
  name: string
13
31
  rows: ExcelPreviewCell[][]
32
+ startCol: number
33
+ endCol: number
34
+ colWidths: Record<number, number>
14
35
  }
15
36
 
16
37
  export interface ExcelSearchMatch {
@@ -20,6 +41,168 @@ export interface ExcelSearchMatch {
20
41
  text: string
21
42
  }
22
43
 
44
+ type ColorStyle = { rgb?: string; theme?: number; tint?: number; indexed?: number }
45
+
46
+ /** Excel 默认主题色(Office 2013+) */
47
+ const EXCEL_THEME_COLORS: Record<number, string> = {
48
+ 0: '#FFFFFF',
49
+ 1: '#000000',
50
+ 2: '#E7E6E6',
51
+ 3: '#44546A',
52
+ 4: '#4472C4',
53
+ 5: '#ED7D31',
54
+ 6: '#A5A5A5',
55
+ 7: '#FFC000',
56
+ 8: '#5B9BD5',
57
+ 9: '#70AD47',
58
+ }
59
+
60
+ const BORDER_WIDTH: Record<string, string> = {
61
+ hair: '1px',
62
+ thin: '1px',
63
+ medium: '2px',
64
+ thick: '3px',
65
+ dashed: '1px',
66
+ dotted: '1px',
67
+ double: '3px',
68
+ }
69
+
70
+ const DEFAULT_COL_WIDTH = 72
71
+
72
+ const hexToRgb = (hex: string) => {
73
+ const normalized = hex.replace('#', '')
74
+ const value =
75
+ normalized.length === 8 ? normalized.slice(2) : normalized.length === 6 ? normalized : ''
76
+ if (value.length !== 6) return null
77
+ return {
78
+ r: parseInt(value.slice(0, 2), 16),
79
+ g: parseInt(value.slice(2, 4), 16),
80
+ b: parseInt(value.slice(4, 6), 16),
81
+ }
82
+ }
83
+
84
+ const rgbToHex = (r: number, g: number, b: number) =>
85
+ `#${[r, g, b]
86
+ .map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, '0'))
87
+ .join('')}`
88
+
89
+ const applyTint = (hex: string, tint: number) => {
90
+ const rgb = hexToRgb(hex)
91
+ if (!rgb) return hex
92
+ const mix = tint < 0 ? { r: 0, g: 0, b: 0 } : { r: 255, g: 255, b: 255 }
93
+ const ratio = Math.abs(tint)
94
+ return rgbToHex(
95
+ rgb.r + (mix.r - rgb.r) * ratio,
96
+ rgb.g + (mix.g - rgb.g) * ratio,
97
+ rgb.b + (mix.b - rgb.b) * ratio,
98
+ )
99
+ }
100
+
101
+ const resolveColor = (color?: ColorStyle): string | undefined => {
102
+ if (!color) return undefined
103
+ if (color.rgb) {
104
+ const raw = color.rgb.replace(/[^0-9a-f]/gi, '')
105
+ if (raw.length === 8) return `#${raw.slice(2)}`
106
+ if (raw.length === 6) return `#${raw}`
107
+ return undefined
108
+ }
109
+ if (color.theme != null) {
110
+ const base = EXCEL_THEME_COLORS[color.theme] ?? '#FFFFFF'
111
+ return color.tint ? applyTint(base, color.tint) : base
112
+ }
113
+ return undefined
114
+ }
115
+
116
+ const resolveBorderSide = (side?: { style?: string; color?: ColorStyle }) => {
117
+ if (!side?.style || side.style === 'none') return undefined
118
+ const width = BORDER_WIDTH[side.style] ?? '1px'
119
+ const color = resolveColor(side.color) ?? '#d0d0d0'
120
+ const lineStyle = side.style === 'dashed' ? 'dashed' : side.style === 'dotted' ? 'dotted' : 'solid'
121
+ return `${width} ${lineStyle} ${color}`
122
+ }
123
+
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>
128
+ const css: ExcelPreviewCellStyle = {}
129
+
130
+ const fill = style.fill as { patternType?: string; fgColor?: ColorStyle; bgColor?: ColorStyle } | undefined
131
+ if (fill && fill.patternType !== 'none') {
132
+ const bg = resolveColor(fill.fgColor) || resolveColor(fill.bgColor)
133
+ if (bg) css.backgroundColor = bg
134
+ }
135
+
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
143
+ if (font) {
144
+ if (font.bold) css.fontWeight = 'bold'
145
+ if (font.italic) css.fontStyle = 'italic'
146
+ if (font.underline) css.textDecoration = 'underline'
147
+ if (font.sz) css.fontSize = `${font.sz}pt`
148
+ const color = resolveColor(font.color)
149
+ if (color) css.color = color
150
+ }
151
+
152
+ const alignment = style.alignment as {
153
+ horizontal?: string
154
+ vertical?: string
155
+ wrapText?: boolean
156
+ } | undefined
157
+ if (alignment) {
158
+ if (alignment.horizontal && alignment.horizontal !== 'general') {
159
+ css.textAlign = alignment.horizontal as ExcelPreviewCellStyle['textAlign']
160
+ }
161
+ if (alignment.vertical) {
162
+ css.verticalAlign =
163
+ alignment.vertical === 'center'
164
+ ? 'middle'
165
+ : (alignment.vertical as ExcelPreviewCellStyle['verticalAlign'])
166
+ }
167
+ if (alignment.wrapText) css.whiteSpace = 'pre-wrap'
168
+ }
169
+
170
+ const border = style.border as Record<string, { style?: string; color?: ColorStyle }> | undefined
171
+ 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)
176
+ if (top) css.borderTop = top
177
+ if (right) css.borderRight = right
178
+ if (bottom) css.borderBottom = bottom
179
+ if (left) css.borderLeft = left
180
+ }
181
+
182
+ return Object.keys(css).length > 0 ? css : undefined
183
+ }
184
+
185
+ export const cellStyleToCss = (style?: ExcelPreviewCellStyle): Record<string, string> => {
186
+ if (!style) return {}
187
+ const css: Record<string, string> = {}
188
+ if (style.backgroundColor) css.backgroundColor = style.backgroundColor
189
+ if (style.color) css.color = style.color
190
+ if (style.fontWeight) css.fontWeight = style.fontWeight
191
+ if (style.fontStyle) css.fontStyle = style.fontStyle
192
+ if (style.textDecoration) css.textDecoration = style.textDecoration
193
+ if (style.textAlign) css.textAlign = style.textAlign
194
+ if (style.verticalAlign) css.verticalAlign = style.verticalAlign
195
+ if (style.borderTop) css.borderTop = style.borderTop
196
+ if (style.borderRight) css.borderRight = style.borderRight
197
+ if (style.borderBottom) css.borderBottom = style.borderBottom
198
+ if (style.borderLeft) css.borderLeft = style.borderLeft
199
+ if (style.fontSize) css.fontSize = style.fontSize
200
+ if (style.whiteSpace) css.whiteSpace = style.whiteSpace
201
+ return css
202
+ }
203
+
204
+ export const toExcelColumnLabel = (colIndex: number) => XLSX.utils.encode_col(colIndex)
205
+
23
206
  const formatCellValue = (cell?: XLSX.CellObject) => {
24
207
  if (!cell) return ''
25
208
  if (cell.w != null && cell.w !== '') return String(cell.w)
@@ -31,18 +214,38 @@ const formatCellValue = (cell?: XLSX.CellObject) => {
31
214
  return String(cell.v)
32
215
  }
33
216
 
217
+ const parseColWidths = (sheet: XLSX.WorkSheet, startCol: number, endCol: number) => {
218
+ const cols = sheet['!cols'] || []
219
+ const widths: Record<number, number> = {}
220
+ for (let c = startCol; c <= endCol; c++) {
221
+ const col = cols[c]
222
+ if (col?.wpx) widths[c] = col.wpx
223
+ else if (col?.wch) widths[c] = Math.max(48, col.wch * 7)
224
+ else widths[c] = DEFAULT_COL_WIDTH
225
+ }
226
+ return widths
227
+ }
228
+
34
229
  export const parseExcelWorkbook = async (src: Blob | ArrayBuffer): Promise<ExcelPreviewSheet[]> => {
35
230
  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
- }))
231
+ const workbook = XLSX.read(buffer, { type: 'array', cellDates: true, cellStyles: true })
232
+ return workbook.SheetNames.map((name) => {
233
+ const sheet = workbook.Sheets[name]
234
+ const range = sheet?.['!ref'] ? XLSX.utils.decode_range(sheet['!ref']) : null
235
+ return {
236
+ name,
237
+ rows: buildSheetRows(sheet),
238
+ startCol: range?.s.c ?? 0,
239
+ endCol: range?.e.c ?? 0,
240
+ colWidths: range ? parseColWidths(sheet, range.s.c, range.e.c) : {},
241
+ }
242
+ })
41
243
  }
42
244
 
43
245
  const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
44
246
  if (!sheet || !sheet['!ref']) return []
45
247
  const range = XLSX.utils.decode_range(sheet['!ref'])
248
+ const colWidths = parseColWidths(sheet, range.s.c, range.e.c)
46
249
  const mergeStarts = new Map<string, { rowSpan: number; colSpan: number }>()
47
250
  const covered = new Set<string>()
48
251
 
@@ -63,13 +266,16 @@ const buildSheetRows = (sheet?: XLSX.WorkSheet): ExcelPreviewCell[][] => {
63
266
  for (let c = range.s.c; c <= range.e.c; c++) {
64
267
  if (covered.has(`${r},${c}`)) continue
65
268
  const addr = XLSX.utils.encode_cell({ r, c })
269
+ const cellObj = sheet[addr]
66
270
  const merge = mergeStarts.get(`${r},${c}`)
67
271
  row.push({
68
- value: formatCellValue(sheet[addr]),
272
+ value: formatCellValue(cellObj),
69
273
  row: r,
70
274
  col: c,
71
275
  rowSpan: merge?.rowSpan,
72
276
  colSpan: merge?.colSpan,
277
+ style: extractCellStyle(cellObj),
278
+ width: colWidths[c],
73
279
  })
74
280
  }
75
281
  if (row.length > 0) rows.push(row)