LuckySheet 实现 excel 的导入与导出

需求:前端使用了 LuckySheet 来实现表格,现在需要实现对这个表格带有样式的导入及导出。在导入时只导入绿色背景单元格中的数据,其他背景颜色单元格的数据将有 LuckySheet 公式自动计算得出。

安装的库

首先安装 luckysheet

<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script>

然后是其他一些库

   "dependencies": {
    ...
    "luckyexcel": "1.0.1",
    "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.9/xlsx-0.18.9.tgz",
    "xlsx-style": "file:./src/libs/xlsx-style" // 在 xlsx 中有一些代码异常,需要
  },

其中 xlsx-style 项目有一个 bug,所以我把项目放到本地进行了小改动。所以可以看到 xlsx-style 的路径是本地路径。

全局配置

// config.js
export default {
  SHEET_COLUMN_KEYS: [ 'a', 'b', 'c' ],
  VALUE_CELL_COLORS: ['#00B050'], // 可以编辑区域的背景色
  DOWNLOAD_FILE_NAME: 'file',
}

导出 Excel

// exportExcel.js
import * as XLSX from 'xlsx'
import * as XLSXStyle from 'xlsx-style'
import config from './config'

export default function () {
  // 1. 获取到要下载的数据
  const allSheetData = window.luckysheet.getluckysheetfile()
  const sheet1 = allSheetData[0]
  const downOriginData = sheet1.data

  let arr = [] // 所有的单元格数据组成的二维数组
  const fConfig = {}
  let bgConfig = {}
  let percentageReg = /%$/
  //列下标 数字转字母
  function chatatABC (n) {
    var orda = 'a'.charCodeAt(0)
    var ordz = 'z'.charCodeAt(0)
    var len = ordz - orda + 1
    var s = ''
    while (n >= 0) {
      s = String.fromCharCode((n % len) + orda) + s
      n = Math.floor(n / len) - 1
    }
    return s.toUpperCase()
  }

  // 获取单元格的背景色
  function setBackground (row, col, bg) {
    var colA = chatatABC(col)
    var key = colA + (row + 1)
    bgConfig[key] = bg.replace(/\#?/, '')
  }

  function setFormat (row, col, f) {
    var colA = chatatABC(col)
    var key = colA + (row + 1)
    fConfig[key] = f
  }

  // 判断值类型是否为百分比 %
  function isPercentage (value) {
    return percentageReg.test(value.m) && value.ct && value.ct.t === 'n'
  }

  // 获取二维数组
  for (let row = 0; row < downOriginData.length; row++) {
    let arrRow = []
    for (let col = 0; col < downOriginData[row].length; col++) {
      const cellValue = downOriginData[row][col]
      // if (row === 0) console.log('cellValue', cellValue)

      if (cellValue) {
        // 处理单元格的背景颜色
        if (cellValue.bg) {
          setBackground(row, col, cellValue.bg)
        }
        if (cellValue.f) {
          setFormat(row, col, cellValue.f)
        }
        if (cellValue.ct != null && cellValue.ct.t == 'd') {
          //  d为时间格式  2019-01-01   或者2019-01-01 10:10:10
          arrRow.push(new Date(cellValue.m.replace(/\-/g, '/'))) //兼容IE
        } else if (cellValue.m && isPercentage(cellValue)) {
          //百分比问题
          arrRow.push(cellValue.m)
        } else {
          arrRow.push(cellValue.v)
        }
      } else {
        arrRow.push(undefined)
      }
    }
    arr.push(arrRow)
  }

  // 2. 通过SheetJs将数据转化为excel格式数据
  let opts = {
    dateNF: 'm/d/yy h:mm',
    cellDates: true,
    cellStyles: true
  }
  let ws = XLSX.utils.aoa_to_sheet(arr, opts)

  // 3. 设置单元格的类型以及单元格样式
  let reg = /[\u4e00-\u9fa5]/g
  for (let key in ws) {
    let item = ws[key]
    if (item.t === 'd') {
      if (item.w) {
        //时间格式的设置
        let arr = item.w.split(' ')
        if (arr[1] && arr[1] == '0:00') {
          ws[key].z = 'm/d/yy'
        } else {
          item.z = 'yyyy/m/d h:mm:ss'
        }
      }
    } else if (item.t === 's') {
      //百分比设置格式
      if (item.v && !item.v.match(reg) && item.v.indexOf('%') > -1) {
        item.t = 'n'
        item.z = '0.00%'
        item.v = Number.parseFloat(item.v) / 100
      } else if (item.v && item.v.match(reg)) {
        //含有中文的设置居中样式
        item['s'] = {
          alignment: { vertical: 'center', horizontal: 'center' }
        }
      }
    }
    // 设置单元格样式
    if (bgConfig[key]) {
      ws[key].s = {
        alignment: { vertical: 'center', horizontal: 'center' },
        fill: { bgColor: { indexed: 32 }, fgColor: { rgb: bgConfig[key] } },
        border: {
          top: { style: 'thin', color: { rgb: '999999' } },
          bottom: { style: 'thin', color: { rgb: '999999' } },
          left: { style: 'thin', color: { rgb: '999999' } },
          right: { style: 'thin', color: { rgb: '999999' } }
        }
      }
    }
    if (fConfig[key]) {
      ws[key].f = fConfig[key]
    }
  }

  console.log('ws', ws)

  // 4. 组装下载数据格式
  let name = 'sheet1'
  let tmpWB = {
    SheetNames: [name], //保存的表标题
    Sheets: {
      [name]: Object.assign({}, ws) //内容
    }
  }

  // 5. 合并单元格配置
  let mergeConfig = sheet1.config.merge
  let mergeArr = []
  if (JSON.stringify(mergeConfig) !== '{}') {
    mergeArr = handleMergeData(mergeConfig)
    tmpWB.Sheets[name]['!merges'] = mergeArr
  }
  //处理合并单元格config数据
  function handleMergeData (origin) {
    let result = []
    if (origin instanceof Object) {
      var r = 'r',
        c = 'c',
        cs = 'cs',
        rs = 'rs'
      for (var key in origin) {
        var startR = origin[key][r]
        var endR = origin[key][r]
        var startC = origin[key][c]
        var endC = origin[key][c]

        // 如果只占一行 为1 如果占两行 为2
        if (origin[key][cs] > 0) {
          endC = startC + (origin[key][cs] - 1)
        }
        if (origin[key][rs] > 0) {
          endR = startR + (origin[key][rs] - 1)
        }
        // s为合并单元格的开始坐标  e为结束坐标
        var obj = { s: { r: startR, c: startC }, e: { r: endR, c: endC } }
        result.push(obj)
      }
    }
    return result
  }

  // 6. 写入文件
  let fileName = config.DOWNLOAD_FILE_NAME
  // sheetjs js-xlsx 的方法 ,导出不带有样式的表格
  // XLSX.writeFile(tmpWB, fileName + '.xlsx')

  /**
   * 通过 xlsx-style 来下载带有颜色的表格数据
   * 目前存在一个小问题,需要去 node_modules 里面清理一行代码否则会报错。
   * https://github.com/protobi/js-xlsx/issues/78#issuecomment-315063340
   */

  function s2ab (s) {
    if (typeof ArrayBuffer !== 'undefined') {
      var buf = new ArrayBuffer(s.length)
      var view = new Uint8Array(buf)
      for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
      return buf
    } else {
      var buf = new Array(s.length)
      for (var i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff
      return buf
    }
  }

  function saveAs (obj, fileName) {
    var tmpa = document.createElement('a')
    tmpa.download = fileName || 'download'
    tmpa.href = URL.createObjectURL(obj)
    tmpa.click()
    setTimeout(function () {
      URL.revokeObjectURL(obj)
    }, 100)
  }

  ws = new Blob([
    s2ab(
      XLSXStyle.write(tmpWB, {
        bookType: 'xlsx',
        bookSST: false,
        type: 'binary'
      }) //这里的数据是用来定义导出的格式类型
    )
  ])
  saveAs(ws, fileName + '.xlsx')
}

导入 excel

// importExcel.js
import LuckyExcel from 'luckyexcel'
import InitExcelJson from './data'
import config from './config'

import { Message } from 'element-ui'

export default function (files, cb) {
  if (files == null || files.length == 0) {
    Message.warning('取消导入文件!')
    return
  }

  let name = files[0].name
  let suffixArr = name.split('.'),
    suffix = suffixArr[suffixArr.length - 1]
  if (suffix != 'xlsx') {
    Message.warning('当前只支持导入 xlsx 文件!')
    return
  }

  // 得到xlsx文件后
  LuckyExcel.transformExcelToLucky(
    files[0],
    function (exportJson, luckysheetfile) {
      console.log('import', exportJson, luckysheetfile)
      // 转换后获取工作表数据
      if (exportJson.sheets == null || exportJson.sheets.length == 0) {
        Message.warning('读取 xlsx 文件失败!')
        return
      }

      updateSheet(exportJson)
      if (cb) {
        cb()
      }
      // const mixinJson = mixinStyle(exportJson) // 进行一次转化

      // window.luckysheet.destroy()

      // window.luckysheet.create({
      //   container: 'luckysheet', //luckysheet is the container id
      //   showinfobar: false,
      //   data: mixinJson.sheets,
      //   title: mixinJson.info.name,
      //   userInfo: mixinJson.info.name.creator
      // })
    },
    function (error) {
      console.log('error', error)
      // 如果抛出任何错误,则处理错误
    }
  )
}

// 通过代码算法来保持原有 data.js 配置
function mixinStyle (json) {
  const result = InitExcelJson

  const sourceCellData = result.sheets[0].celldata
  const targetCellData = json.sheets[0].celldata

  const targetCellMap = {}
  targetCellData.forEach(cell => {
    targetCellMap[`${cell.r}-${cell.c}`] = cell
  })

  sourceCellData.forEach(cell => {
    const findCell = targetCellMap[`${cell.r}-${cell.c}`]
    if (findCell) {
      if (findCell.v.v) cell.v.v = findCell.v.v
      if (findCell.v.m) cell.v.m = findCell.v.m
    }
  })

  return result
}

function updateSheet (json) {
  const { VALUE_CELL_COLORS } = config // 可以编辑区域的背景色

  const sourceCellData = InitExcelJson.sheets[0].celldata
  const targetCellData = json.sheets[0].celldata

  const targetCellMap = {}
  targetCellData.forEach(cell => {
    targetCellMap[`${cell.r}-${cell.c}`] = cell
  })

  sourceCellData.forEach(cell => {
    if (VALUE_CELL_COLORS.includes(cell.v.bg)) {
      const findCell = targetCellMap[`${cell.r}-${cell.c}`]
      if (findCell) {
        window.luckysheet.setCellValue(cell.r, cell.c, {
          v: findCell.v.v
        })
      }
    }
  })
}

具体到 Vue 项目中的使用:

 <input type="file" ref="importInput" @change="importExcel" />
    importExcel(evt) {
      this.loading = true;
      importExcel(evt.target.files);
      setTimeout(() => {
        this.loading = false;
        this.$refs.importInput.value = ''
      }, 1000);
    },

最后

最后吐槽一句,xlsx-style 的坑挺多的,而且这玩意已经不维护了,请注意~

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容