|
|
@@ -0,0 +1,403 @@
|
|
|
+import ExcelJS from 'exceljs'
|
|
|
+import FileSaver from 'file-saver'
|
|
|
+
|
|
|
+// 颜色转换辅助方法
|
|
|
+function convertColorToHex(color) {
|
|
|
+ if (!color) return '000000'
|
|
|
+
|
|
|
+ // 如果已经是6位HEX(不含#)
|
|
|
+ if (/^[0-9A-Fa-f]{6}$/.test(color)) return color.toUpperCase()
|
|
|
+
|
|
|
+ // 如果带#,去掉#
|
|
|
+ if (color.startsWith('#')) {
|
|
|
+ const hex = color.substring(1)
|
|
|
+ // 如果是3位简写,转换为6位
|
|
|
+ if (hex.length === 3) {
|
|
|
+ return hex.split('').map(c => c + c).join('').toUpperCase()
|
|
|
+ }
|
|
|
+ return hex.toUpperCase()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理rgb/rgba格式
|
|
|
+ if (color.startsWith('rgb')) {
|
|
|
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/)
|
|
|
+ if (match) {
|
|
|
+ const r = parseInt(match[1]).toString(16).padStart(2, '0')
|
|
|
+ const g = parseInt(match[2]).toString(16).padStart(2, '0')
|
|
|
+ const b = parseInt(match[3]).toString(16).padStart(2, '0')
|
|
|
+ return (r + g + b).toUpperCase()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认返回黑色
|
|
|
+ return '000000'
|
|
|
+}
|
|
|
+
|
|
|
+// 格式转换辅助方法
|
|
|
+function convertDateFormat(format) {
|
|
|
+ if (!format) return format
|
|
|
+
|
|
|
+ // 常见的中文格式转换
|
|
|
+ const formatMap = {
|
|
|
+ 'yyyy-mm-dd': 'yyyy/mm/dd',
|
|
|
+ 'yyyy年mm月dd日': 'yyyy"年"mm"月"dd"日"',
|
|
|
+ 'm/d/yy': 'yyyy/m/d', // 将两位年份格式改为四位年份
|
|
|
+ 'yyyy/m/d': 'yyyy/m/d',
|
|
|
+ 'h:mm': 'h:mm',
|
|
|
+ 'h:mm:ss': 'h:mm:ss',
|
|
|
+ '@': '@',
|
|
|
+ 'General': 'General',
|
|
|
+ '0_ ': '0', // 清理带空格的格式
|
|
|
+ '0%': '0%',
|
|
|
+ '0.00%': '0.00%',
|
|
|
+ '0.0%': '0.0%',
|
|
|
+ '0.0': '0.0',
|
|
|
+ '0.00': '0.00'
|
|
|
+ }
|
|
|
+
|
|
|
+ return formatMap[format] || format
|
|
|
+}
|
|
|
+
|
|
|
+// 应用单元格样式
|
|
|
+function applyCellStyle(excelCell, cell) {
|
|
|
+ try {
|
|
|
+ if (!cell) return
|
|
|
+
|
|
|
+ // 根据数据结构提取样式对象
|
|
|
+ let styleObj = null
|
|
|
+
|
|
|
+ if (cell.v && typeof cell.v === 'object') {
|
|
|
+ // celldata 格式: {r: 4, c: 0, v: {ct: {...}, bg: '#00B0F0', ...}}
|
|
|
+ styleObj = cell.v
|
|
|
+ } else if (cell.ct || cell.bg || cell.fs || cell.fc) {
|
|
|
+ // data 数组格式: {ct: {...}, bg: '#00B0F0', fs: 11, fc: '#000000', ...}
|
|
|
+ styleObj = cell
|
|
|
+ } else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styleObj) return
|
|
|
+
|
|
|
+ // 字体样式
|
|
|
+ if (styleObj.fs || styleObj.fc || styleObj.ff || styleObj.bl) {
|
|
|
+ const font = {}
|
|
|
+
|
|
|
+ if (styleObj.fs) font.size = styleObj.fs
|
|
|
+ if (styleObj.fc) {
|
|
|
+ const colorHex = convertColorToHex(styleObj.fc)
|
|
|
+ font.color = { argb: 'FF' + colorHex }
|
|
|
+ }
|
|
|
+ if (styleObj.ff) font.name = styleObj.ff
|
|
|
+ if (styleObj.bl === 1) font.bold = true
|
|
|
+
|
|
|
+ if (Object.keys(font).length > 0) {
|
|
|
+ excelCell.font = font
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 填充(背景色)
|
|
|
+ if (styleObj.bg) {
|
|
|
+ const bgColorHex = convertColorToHex(styleObj.bg)
|
|
|
+ try {
|
|
|
+ excelCell.fill = {
|
|
|
+ type: 'pattern',
|
|
|
+ pattern: 'solid',
|
|
|
+ fgColor: { argb: 'FF' + bgColorHex }
|
|
|
+ }
|
|
|
+ } catch (fillError) {
|
|
|
+ // 静默处理填充错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对齐
|
|
|
+ if (styleObj.vt !== undefined || styleObj.ht !== undefined) {
|
|
|
+ const alignment = {}
|
|
|
+ if (styleObj.vt !== undefined) {
|
|
|
+ alignment.vertical = styleObj.vt === 0 ? 'top' : styleObj.vt === 2 ? 'bottom' : 'middle'
|
|
|
+ }
|
|
|
+ if (styleObj.ht !== undefined) {
|
|
|
+ alignment.horizontal = styleObj.ht === 0 ? 'left' : styleObj.ht === 2 ? 'right' : 'center'
|
|
|
+ }
|
|
|
+ excelCell.alignment = alignment
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 静默处理样式应用错误
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 设置单元格值(处理格式)
|
|
|
+function setCellValue(excelCell, cell) {
|
|
|
+ if (!cell) return
|
|
|
+
|
|
|
+ let value = ''
|
|
|
+ let ct = null
|
|
|
+ let displayText = null
|
|
|
+
|
|
|
+ // 处理不同的数据结构
|
|
|
+ if (cell.v && typeof cell.v === 'object' && cell.v !== null) {
|
|
|
+ // cell.v 是对象格式(来自 celldata)
|
|
|
+ if (cell.v.ct || cell.v.v !== undefined || cell.v.m !== undefined) {
|
|
|
+ // 包含 ct 属性或者 v/m 属性
|
|
|
+ value = cell.v.v !== undefined ? cell.v.v : cell.v
|
|
|
+ ct = cell.v.ct
|
|
|
+ displayText = cell.v.m // 显示文本
|
|
|
+
|
|
|
+ // 如果是合并单元格,不设置值
|
|
|
+ if (cell.v.mc) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // cell.v是简单对象但不是ct格式
|
|
|
+ value = cell.v
|
|
|
+ }
|
|
|
+ } else if (cell.ct) {
|
|
|
+ // 直接包含 ct 格式(来自 data 数组)
|
|
|
+ // 注意:data数组中的cell.v是原始值,不是对象
|
|
|
+ value = cell.v !== undefined ? cell.v : ''
|
|
|
+ ct = cell.ct
|
|
|
+ displayText = cell.m // 显示文本
|
|
|
+ } else if (cell.v !== undefined) {
|
|
|
+ // 简单的 v 值
|
|
|
+ value = cell.v
|
|
|
+ }
|
|
|
+
|
|
|
+ if (value === null || value === undefined || value === '') return
|
|
|
+
|
|
|
+ // 首先检查是否为日期或时间格式
|
|
|
+ const isDateFormat = ct && ct.fa && (
|
|
|
+ ct.fa === 'm/d/yy' ||
|
|
|
+ ct.fa === 'yyyy-mm-dd' ||
|
|
|
+ ct.fa === 'yyyy年mm月dd日' ||
|
|
|
+ (ct.fa.includes('d') && !ct.fa.includes('h')) // 包含d但不包含h
|
|
|
+ )
|
|
|
+
|
|
|
+ const isTimeFormat = ct && ct.fa && (
|
|
|
+ ct.fa === 'h:mm' ||
|
|
|
+ ct.fa === 'h:mm:ss' ||
|
|
|
+ ct.fa.includes('h:')
|
|
|
+ )
|
|
|
+
|
|
|
+ // 处理日期格式
|
|
|
+ if (isDateFormat) {
|
|
|
+ const numValue = parseFloat(value)
|
|
|
+ const textValue = displayText || ''
|
|
|
+
|
|
|
+ // 检查是否为两位年份格式(如"3/3/26")
|
|
|
+ if (textValue && /^\d{1,2}\/\d{1,2}\/\d{2}$/.test(textValue)) {
|
|
|
+ const parts = textValue.split('/')
|
|
|
+ if (parts.length === 3) {
|
|
|
+ let [month, day, year] = parts
|
|
|
+ // 将两位年份转换为四位年份(假设2000-2099年)
|
|
|
+ const fullYear = parseInt(year) < 30 ? 2000 + parseInt(year) : 1900 + parseInt(year)
|
|
|
+ const formattedDate = `${fullYear}/${month}/${day}`
|
|
|
+
|
|
|
+ excelCell.value = formattedDate
|
|
|
+ excelCell.numFmt = 'yyyy/m/d' // 日期格式
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不是两位年份格式,使用Date对象
|
|
|
+ if (!isNaN(numValue)) {
|
|
|
+ // Excel日期序列号转JS Date
|
|
|
+ const excelEpoch = new Date(Date.UTC(1899, 11, 30)) // 1899-12-30 UTC
|
|
|
+ const days = Math.floor(numValue) - 1 // 减去1天修正闰年bug
|
|
|
+ const timeFraction = numValue - Math.floor(numValue) // 获取小数部分(时间)
|
|
|
+
|
|
|
+ const date = new Date(excelEpoch.getTime() + days * 86400000 + timeFraction * 86400000)
|
|
|
+ excelCell.value = date
|
|
|
+ excelCell.numFmt = 'yyyy/m/d'
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理时间格式
|
|
|
+ if (isTimeFormat) {
|
|
|
+ const numValue = parseFloat(value)
|
|
|
+ if (!isNaN(numValue)) {
|
|
|
+ // 使用UTC时间避免时区问题
|
|
|
+ const baseDate = new Date(Date.UTC(1899, 11, 30)) // 基准日期
|
|
|
+ const timeInMs = numValue * 86400000 // 一天的毫秒数
|
|
|
+ const timeDate = new Date(baseDate.getTime() + timeInMs)
|
|
|
+
|
|
|
+ excelCell.value = timeDate
|
|
|
+ excelCell.numFmt = convertDateFormat(ct.fa)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理其他类型
|
|
|
+ if (ct && ct.t === 'n') {
|
|
|
+ // 数字类型
|
|
|
+ const numValue = parseFloat(value)
|
|
|
+ if (!isNaN(numValue)) {
|
|
|
+ if (ct.fa) {
|
|
|
+ excelCell.numFmt = convertDateFormat(ct.fa)
|
|
|
+ }
|
|
|
+ excelCell.value = numValue
|
|
|
+ } else {
|
|
|
+ excelCell.value = value
|
|
|
+ }
|
|
|
+ } else if (ct && ct.t === 's') {
|
|
|
+ // 字符串类型
|
|
|
+ excelCell.value = value
|
|
|
+ if (ct.fa === '@' || (!ct.fa && /^\d+$/.test(value))) {
|
|
|
+ excelCell.numFmt = '@'
|
|
|
+ }
|
|
|
+ } else if (ct && ct.t === 'g') {
|
|
|
+ // 一般类型(通常是文本)
|
|
|
+ excelCell.value = value
|
|
|
+ if (!ct.fa && /^\d+$/.test(value)) {
|
|
|
+ excelCell.numFmt = '@'
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 其他类型
|
|
|
+ excelCell.value = value
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理边框
|
|
|
+function applyBorders(excelCell, sheet, row, col) {
|
|
|
+ if (!sheet.config || !sheet.config.borderInfo) return
|
|
|
+
|
|
|
+ const borderInfo = sheet.config.borderInfo.find(b =>
|
|
|
+ b.rangeType === 'cell' &&
|
|
|
+ b.value.row_index === row &&
|
|
|
+ b.value.col_index === col
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!borderInfo || !borderInfo.value) return
|
|
|
+
|
|
|
+ const border = {}
|
|
|
+ const borderValue = borderInfo.value
|
|
|
+
|
|
|
+ if (borderValue.t) {
|
|
|
+ border.top = {
|
|
|
+ style: borderValue.t.style === 1 ? 'thin' : 'thin',
|
|
|
+ color: { argb: 'FF' + convertColorToHex(borderValue.t.color || '#000000') }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (borderValue.r) {
|
|
|
+ border.right = {
|
|
|
+ style: borderValue.r.style === 1 ? 'thin' : 'thin',
|
|
|
+ color: { argb: 'FF' + convertColorToHex(borderValue.r.color || '#000000') }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (borderValue.b) {
|
|
|
+ border.bottom = {
|
|
|
+ style: borderValue.b.style === 1 ? 'thin' : 'thin',
|
|
|
+ color: { argb: 'FF' + convertColorToHex(borderValue.b.color || '#000000') }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (borderValue.l) {
|
|
|
+ border.left = {
|
|
|
+ style: borderValue.l.style === 1 ? 'thin' : 'thin',
|
|
|
+ color: { argb: 'FF' + convertColorToHex(borderValue.l.color || '#000000') }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Object.keys(border).length > 0) {
|
|
|
+ excelCell.border = border
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 主导出函数
|
|
|
+export async function exportToExcel(sheetData, fileName = null) {
|
|
|
+ try {
|
|
|
+ const workbook = new ExcelJS.Workbook()
|
|
|
+
|
|
|
+ // 处理所有工作表
|
|
|
+ for (let sheetIndex = 0; sheetIndex < sheetData.length; sheetIndex++) {
|
|
|
+ const sheet = sheetData[sheetIndex]
|
|
|
+ const sheetName = sheet.name || `Sheet${sheetIndex + 1}`
|
|
|
+ const worksheet = workbook.addWorksheet(sheetName)
|
|
|
+
|
|
|
+ let maxRow = 0
|
|
|
+ let maxCol = 0
|
|
|
+
|
|
|
+ // 优先级:使用data数组(完整二维数组)
|
|
|
+ if (sheet.data && Array.isArray(sheet.data)) {
|
|
|
+ for (let r = 0; r < sheet.data.length; r++) {
|
|
|
+ const row = sheet.data[r]
|
|
|
+
|
|
|
+ // 设置行高
|
|
|
+ if (sheet.rowlen && sheet.rowlen[r]) {
|
|
|
+ worksheet.getRow(r + 1).height = sheet.rowlen[r]
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(row)) {
|
|
|
+ for (let c = 0; c < row.length; c++) {
|
|
|
+ const cell = row[c]
|
|
|
+ if (cell) {
|
|
|
+ const excelCell = worksheet.getCell(r + 1, c + 1)
|
|
|
+
|
|
|
+ // 设置值
|
|
|
+ setCellValue(excelCell, cell)
|
|
|
+
|
|
|
+ // 设置样式
|
|
|
+ applyCellStyle(excelCell, cell)
|
|
|
+
|
|
|
+ // 设置边框
|
|
|
+ applyBorders(excelCell, sheet, r, c)
|
|
|
+
|
|
|
+ if (c + 1 > maxCol) maxCol = c + 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (r + 1 > maxRow) maxRow = r + 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 使用celldata稀疏格式
|
|
|
+ else if (sheet.celldata && Array.isArray(sheet.celldata)) {
|
|
|
+ sheet.celldata.forEach((cellData) => {
|
|
|
+ if (cellData && cellData.r !== undefined && cellData.c !== undefined) {
|
|
|
+ const excelCell = worksheet.getCell(cellData.r + 1, cellData.c + 1)
|
|
|
+
|
|
|
+ // 设置值
|
|
|
+ setCellValue(excelCell, cellData)
|
|
|
+
|
|
|
+ // 设置样式
|
|
|
+ applyCellStyle(excelCell, cellData)
|
|
|
+
|
|
|
+ // 设置边框
|
|
|
+ applyBorders(excelCell, sheet, cellData.r, cellData.c)
|
|
|
+
|
|
|
+ if (cellData.r + 1 > maxRow) maxRow = cellData.r + 1
|
|
|
+ if (cellData.c + 1 > maxCol) maxCol = cellData.c + 1
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置列宽
|
|
|
+ if (sheet.columnlen && Array.isArray(sheet.columnlen)) {
|
|
|
+ for (let c = 0; c < Math.min(sheet.columnlen.length, maxCol); c++) {
|
|
|
+ const width = sheet.columnlen[c]
|
|
|
+ if (width && width > 0) {
|
|
|
+ worksheet.getColumn(c + 1).width = width / 7
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成文件名并保存
|
|
|
+ const finalFileName = fileName || `在线表格导出_${new Date().getTime()}.xlsx`
|
|
|
+
|
|
|
+ const buffer = await workbook.xlsx.writeBuffer()
|
|
|
+
|
|
|
+ FileSaver.saveAs(
|
|
|
+ new Blob([buffer], { type: 'application/octet-stream' }),
|
|
|
+ finalFileName
|
|
|
+ )
|
|
|
+
|
|
|
+ return { success: true, fileName: finalFileName }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Excel导出错误:', error)
|
|
|
+ return { success: false, error: error.message }
|
|
|
+ }
|
|
|
+}
|