|
|
@@ -62,26 +62,25 @@ function convertDateFormat(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', ...}}
|
|
|
+ // celldata 格式
|
|
|
styleObj = cell.v
|
|
|
} else if (cell.ct || cell.bg || cell.fs || cell.fc) {
|
|
|
- // data 数组格式: {ct: {...}, bg: '#00B0F0', fs: 11, fc: '#000000', ...}
|
|
|
+ // data 数组格式
|
|
|
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)
|
|
|
@@ -89,12 +88,12 @@ function applyCellStyle(excelCell, cell) {
|
|
|
}
|
|
|
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)
|
|
|
@@ -108,16 +107,30 @@ function applyCellStyle(excelCell, cell) {
|
|
|
// 静默处理填充错误
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // 对齐
|
|
|
- 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'
|
|
|
- }
|
|
|
+
|
|
|
+ // 对齐(修正 vt / ht 映射,新增 wrapText)
|
|
|
+ const alignment = {}
|
|
|
+
|
|
|
+ if (styleObj.vt !== undefined) {
|
|
|
+ // luckysheet: 0=middle, 1=top, 2=bottom
|
|
|
+ alignment.vertical =
|
|
|
+ styleObj.vt === 1 ? 'top' :
|
|
|
+ styleObj.vt === 2 ? 'bottom' : 'middle'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (styleObj.ht !== undefined) {
|
|
|
+ // luckysheet: 0=center, 1=left, 2=right
|
|
|
+ alignment.horizontal =
|
|
|
+ styleObj.ht === 1 ? 'left' :
|
|
|
+ styleObj.ht === 2 ? 'right' : 'center'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动换行:luckysheet tb=2 表示自动换行
|
|
|
+ if (styleObj.tb === 2) {
|
|
|
+ alignment.wrapText = true
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Object.keys(alignment).length > 0) {
|
|
|
excelCell.alignment = alignment
|
|
|
}
|
|
|
} catch (error) {
|
|
|
@@ -126,132 +139,139 @@ function applyCellStyle(excelCell, cell) {
|
|
|
}
|
|
|
|
|
|
// 设置单元格值(处理格式)
|
|
|
-function setCellValue(excelCell, cell) {
|
|
|
+function setCellValue(excelCell, cell, r, c) {
|
|
|
if (!cell) return
|
|
|
-
|
|
|
+
|
|
|
let value = ''
|
|
|
let ct = null
|
|
|
let displayText = null
|
|
|
-
|
|
|
- // 处理不同的数据结构
|
|
|
+ let mc = 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
|
|
|
+ // ===== celldata 格式:{r, c, v: {v, m, ct, mc, ...}} =====
|
|
|
+ const vObj = cell.v
|
|
|
+ ct = vObj.ct
|
|
|
+ displayText = vObj.m
|
|
|
+ mc = vObj.mc
|
|
|
+
|
|
|
+ if (vObj.v !== undefined && vObj.v !== null && vObj.v !== '') {
|
|
|
+ value = vObj.v
|
|
|
+ } else if (vObj.m !== undefined && vObj.m !== null && vObj.m !== '') {
|
|
|
+ value = vObj.m
|
|
|
}
|
|
|
- } else if (cell.ct) {
|
|
|
- // 直接包含 ct 格式(来自 data 数组)
|
|
|
- // 注意:data数组中的cell.v是原始值,不是对象
|
|
|
- value = cell.v !== undefined ? cell.v : ''
|
|
|
+ } else {
|
|
|
+ // ===== data 数组格式:{v, m, ct, mc, ...} =====
|
|
|
ct = cell.ct
|
|
|
- displayText = cell.m // 显示文本
|
|
|
- } else if (cell.v !== undefined) {
|
|
|
- // 简单的 v 值
|
|
|
- value = cell.v
|
|
|
+ displayText = cell.m
|
|
|
+ mc = cell.mc
|
|
|
+
|
|
|
+ if (cell.v !== undefined && cell.v !== null && cell.v !== '') {
|
|
|
+ value = cell.v
|
|
|
+ } else if (cell.m !== undefined && cell.m !== null && cell.m !== '') {
|
|
|
+ value = cell.m
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ if ((value === '' || value === null || value === undefined)
|
|
|
+ && ct && Array.isArray(ct.s) && ct.s.length > 0) {
|
|
|
+ let richText = ''
|
|
|
+ ct.s.forEach(seg => {
|
|
|
+ if (seg && seg.v !== undefined && seg.v !== null) {
|
|
|
+ richText += seg.v
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (richText !== '') {
|
|
|
+ value = richText
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mc && typeof mc.r === 'number' && typeof mc.c === 'number') {
|
|
|
+ if (r !== mc.r || c !== mc.c) {
|
|
|
+ return // 非左上角,跳过
|
|
|
+ }
|
|
|
+ // 是左上角,继续走下面的逻辑设置值
|
|
|
+ }
|
|
|
+
|
|
|
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
|
|
|
- )
|
|
|
-
|
|
|
+ ct.fa === 'm/d/yy' ||
|
|
|
+ ct.fa === 'yyyy-mm-dd' ||
|
|
|
+ ct.fa === 'yyyy年mm月dd日' ||
|
|
|
+ ct.fa === 'yyyy/m/d' ||
|
|
|
+ ct.fa === 'yyyy/m/d h:mm' ||
|
|
|
+ ct.fa === 'yyyy/m/d h:mm:ss' ||
|
|
|
+ /^y{2,4}[\/\-\.年]/.test(ct.fa) // 只认 "yy/yyy/yyyy" 开头、后接 / - . 年 的
|
|
|
+)
|
|
|
+
|
|
|
const isTimeFormat = ct && ct.fa && (
|
|
|
- ct.fa === 'h:mm' ||
|
|
|
+ ct.fa === 'h:mm' ||
|
|
|
ct.fa === 'h:mm:ss' ||
|
|
|
ct.fa.includes('h:')
|
|
|
)
|
|
|
-
|
|
|
- // 处理日期格式
|
|
|
+
|
|
|
if (isDateFormat) {
|
|
|
- const numValue = parseFloat(value)
|
|
|
- const textValue = displayText || ''
|
|
|
-
|
|
|
+ const numValue = parseFloat(value)
|
|
|
+ const textValue = displayText || ''
|
|
|
+
|
|
|
+ // 只有序列号在有效范围内(1 ~ 2958465,即 1900/1/1 ~ 9999/12/31)才当日期
|
|
|
+ const isValidDateSerial =
|
|
|
+ !isNaN(numValue) && numValue >= 1 && numValue <= 2958465
|
|
|
+
|
|
|
+ if (isValidDateSerial) {
|
|
|
// 检查是否为两位年份格式(如"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' // 日期格式
|
|
|
+ excelCell.value = `${fullYear}/${month}/${day}`
|
|
|
+ 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
|
|
|
- }
|
|
|
+ const jsDate = new Date((numValue - 25569) * 86400000)
|
|
|
+ excelCell.value = jsDate
|
|
|
+ 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)
|
|
|
-
|
|
|
+ const baseDate = new Date(Date.UTC(1899, 11, 30))
|
|
|
+ const timeDate = new Date(baseDate.getTime() + numValue * 86400000)
|
|
|
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)
|
|
|
- }
|
|
|
+ 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 {
|
|
|
- // 其他类型
|
|
|
+ // inlineStr 或其它:直接写字符串
|
|
|
excelCell.value = value
|
|
|
}
|
|
|
}
|
|
|
@@ -307,72 +327,106 @@ function applyBorders(excelCell, sheet, row, col) {
|
|
|
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
|
|
|
-
|
|
|
+ const merges = [] // 收集当前 sheet 的合并区域
|
|
|
+
|
|
|
// 优先级:使用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)
|
|
|
-
|
|
|
+
|
|
|
+ // 设置值( 传入 r, c)
|
|
|
+ setCellValue(excelCell, cell, r, c)
|
|
|
+
|
|
|
// 设置样式
|
|
|
applyCellStyle(excelCell, cell)
|
|
|
-
|
|
|
+
|
|
|
// 设置边框
|
|
|
applyBorders(excelCell, sheet, r, c)
|
|
|
-
|
|
|
+
|
|
|
+ // 收集合并区域(只收集左上角)
|
|
|
+ const mc = cell.mc || (cell.v && cell.v.mc)
|
|
|
+ if (mc && mc.r === r && mc.c === c) {
|
|
|
+ merges.push(mc)
|
|
|
+ }
|
|
|
+
|
|
|
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)
|
|
|
-
|
|
|
+
|
|
|
+ // 设置值(传入 r, c)
|
|
|
+ setCellValue(excelCell, cellData, cellData.r, cellData.c)
|
|
|
+
|
|
|
// 设置样式
|
|
|
applyCellStyle(excelCell, cellData)
|
|
|
-
|
|
|
+
|
|
|
// 设置边框
|
|
|
applyBorders(excelCell, sheet, cellData.r, cellData.c)
|
|
|
-
|
|
|
+
|
|
|
+ // 收集合并区域(只收集左上角)
|
|
|
+ const mc = cellData.mc || (cellData.v && cellData.v.mc)
|
|
|
+ if (mc && mc.r === cellData.r && mc.c === cellData.c) {
|
|
|
+ merges.push(mc)
|
|
|
+ }
|
|
|
+
|
|
|
if (cellData.r + 1 > maxRow) maxRow = cellData.r + 1
|
|
|
if (cellData.c + 1 > maxCol) maxCol = cellData.c + 1
|
|
|
}
|
|
|
})
|
|
|
-
|
|
|
-
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ // 调试用:打印 sheet 收集到的合并区域
|
|
|
+ console.log(`【sheet "${sheetName}" 收集到的 merges】`, JSON.stringify(merges))
|
|
|
+
|
|
|
+ // 执行合并单元格(必须放在列宽设置之前)
|
|
|
+ // ExcelJS 的 mergeCells 参数顺序:(top, left, bottom, right)
|
|
|
+ // 注意:所有坐标从 1 开始
|
|
|
+ merges.forEach(mc => {
|
|
|
+ try {
|
|
|
+ const top = mc.r + 1
|
|
|
+ const left = mc.c + 1
|
|
|
+ const bottom = mc.r + mc.rs
|
|
|
+ const right = mc.c + mc.cs
|
|
|
+
|
|
|
+ // 只有真正多行或多列时才合并
|
|
|
+ if (bottom > top || right > left) {
|
|
|
+ worksheet.mergeCells(top, left, bottom, right)
|
|
|
+ console.log(`【已合并】sheet="${sheetName}" 区域=${top},${left} → ${bottom},${right}`)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('合并单元格失败:', mc, e)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
// 设置列宽
|
|
|
if (sheet.columnlen && Array.isArray(sheet.columnlen)) {
|
|
|
for (let c = 0; c < Math.min(sheet.columnlen.length, maxCol); c++) {
|
|
|
@@ -383,19 +437,19 @@ export async function exportToExcel(sheetData, fileName = null) {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 生成文件名并保存
|
|
|
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) {
|
|
|
return { success: false, error: error.message }
|
|
|
}
|