فهرست منبع

优化luckysheet导出逻辑,修复合并单元格值和样式丢失

huangws 1 روز پیش
والد
کامیت
dfc3a6611a
2فایلهای تغییر یافته به همراه236 افزوده شده و 147 حذف شده
  1. 61 26
      src/views/component/onlineSheet/onlineSheetData.vue
  2. 175 121
      src/views/component/onlineSheet/onlineSheetExcelExport.js

+ 61 - 26
src/views/component/onlineSheet/onlineSheetData.vue

@@ -294,47 +294,63 @@ export default {
     },
     // 导出 Excel 文件
     async handleExport() {
+      // 1. 先弹框让用户输入文件名
+      let fileName = ''
       try {
-        // 显示加载状态
-        const loading = this.$loading({
-          lock: true,
-          text: '正在准备导出数据...',
-          spinner: 'el-icon-loading',
-          background: 'rgba(0, 0, 0, 0.7)'
+        const defaultName = this.getDefaultFileName()
+        const { value } = await this.$prompt('请输入导出文件名(可省略 .xlsx 后缀)', '导出 Excel', {
+          confirmButtonText: '导出',
+          cancelButtonText: '取消',
+          inputValue: defaultName,
+          inputValidator: (val) => {
+            if (!val || !val.trim()) return '文件名不能为空'
+            // Excel 文件名不能包含这些字符
+            if (/[\\/:*?"<>|]/.test(val)) return '文件名不能包含 \\ / : * ? " < > |'
+            return true
+          }
         })
-        
-        try {
-          // 使用外部导出模块
-          await this.useExternalExport(loading)
-        } catch (error) {
-          this.$message.error(`导出失败: ${error.message || '数据处理错误'}`)
-        } finally {
-          loading.close()
+        fileName = value.trim()
+        if (!/\.xlsx$/i.test(fileName)) {
+          fileName += '.xlsx'
         }
-        
-      } catch (outerError) {
-        this.$message.error('导出过程发生异常')
+      } catch (e) {
+        // 用户点了取消,什么都不做,直接返回
+        return
+      }
+
+      // 2. 再做导出
+      const loading = this.$loading({
+        lock: true,
+        text: '正在准备导出数据...',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      })
+
+      try {
+        await this.useExternalExport(loading, fileName)
+      } catch (error) {
+        this.$message.error(`导出失败: ${error.message || '数据处理错误'}`)
+      } finally {
+        loading.close()
       }
     },
     
     // 使用外部导出模块导出Excel
-    async useExternalExport(loading) {
+    async useExternalExport(loading, fileName) {
       if (!this.sheetData || this.sheetData.length === 0) {
         this.$message.warning('没有表格数据可导出!')
         loading.close()
         return
       }
-      
-      // 从 iframe 获取最新数据
+
       this.sendToIframe('getData')
-      
-      // 等待数据更新
+
       await new Promise(resolve => setTimeout(resolve, 500))
-      
+
       try {
-        // 使用独立的导出模块
-        const result = await exportToExcel(this.sheetData)
-        
+        // 传 fileName 给 exportToExcel
+        const result = await exportToExcel(this.sheetData, fileName)
+
         if (result.success) {
           this.$message.success(`导出成功!文件: ${result.fileName}`)
         } else {
@@ -343,6 +359,25 @@ export default {
       } catch (error) {
         this.$message.error(`导出失败: ${error.message || '未知错误'}`)
       }
+    },
+
+    // 默认文件名:优先取第一个 sheet 的名字
+    getDefaultFileName() {
+      // 取当前年月,如 202609
+      const now = new Date()
+      const yyyy = now.getFullYear()
+      const mm = String(now.getMonth() + 1).padStart(2, '0')  // 月份从 0 开始,所以要 +1
+      const ym = `${yyyy}${mm}`
+
+      if (this.sheetData && this.sheetData.length > 0) {
+        const firstName = this.sheetData[0].name
+        if (firstName && String(firstName).trim()) {
+          return `${String(firstName).trim()}_${ym}`
+        }
+      }
+
+      // 兜底
+      return `在线表格导出_${ym}`
     }
   }
 }

+ 175 - 121
src/views/component/onlineSheet/onlineSheetExcelExport.js

@@ -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 }
   }