前端導出可修改樣式的Excel表格


最近實現了一個純前端下載Excel,並可以修改Excel樣式的功能。由於實現過程比較曲折,沒搜到較完整的示例,英文文檔看起來也比較吃力,所以這里分享一個完整的示例。下面是兩種方法分別介紹純前端實現不修改樣式和修改樣式時導出Excel的方法:

  1. 使用SheetJS/js-xlsx(https://github.com/SheetJS/js-xlsx#input-type)導出Excel表格。
    1. 優點:簡單。
    2. 缺點:免費版不支持修改表格樣式。
    3. 安裝:npm install xlsx。
    4. 性能:經測試,導出30多列,幾百條數據的表格比較快;上千條大概需要等待3-5秒。
    5. 補充:支持很多種類的數據解析和導出,這里僅涉及導入二維數組,導出xlsx。持續更新中,最新的更新日期是2019年8月。
    6. 實現:
      1. 引入:
        import XLSX from 'xlsx'
      2. 輸入(數據源):
        const sheetDatas = [
           ['序號, '姓名', '性別'],
           [1, 'Lily', '女'],
           [2, 'John', '男'],
           [3, 'Mary', '女']
        ]
      3. 調用方法:
        const wb = XLSX.utils.book_new() // 創建一個工作簿
        const ws = XLSX.utils.aoa_to_sheet(sheetDatas) // 使用二維數組創建一個工作表對象
        XLSX.utils.book_append_sheet(wb, ws, '人員信息') // 向工作簿追加一個工作表
        XLSX.writeFile(wb, `人員信息${moment().format('YYYYMMDD')}.xlsx`) // 寫入文件
      4. 輸出(Excel):

         

          
  2. 使用protobi/js-xlsx(https://github.com/protobi/js-xlsx)導出Excel表格。
    1. 優點:可修改表格樣式(如字體、合並單元格、顏色等)。
    2. 缺點:比較復雜。
    3. 安裝:npm install xlsx-style --save。
    4. 性能:經測試,導出30多列,幾百條數據的表格比較快;上千條大概需要等待3-5秒。
    5. 補充:基於SheetJS/js-xlsx開發(注意:安裝依賴的時候不需要裝SheetJS/js-xlsx),開發停留在2017年,后面沒有更新,沒有維護,SheetJS/js-xlsx中的新方法不支持。
    6. 實現:
      1. 引入:
        import XLSXStyle from 'js-xlsx'
        1. 我是在Vue中引入的這個依賴,引入之后會報錯,解決方法:
          1. 修改源碼:修改xlsx-style/dist/cpexcel.js文件中的第807行
            var cpt = require('./cpt' + 'able');
            修改為 
            var cpt = cptable;
          2. 修改webpack配置,與entry同一級新增一個字段,如圖:
      2. 輸入:
        const sheetTitle = ["序號", "姓名", "性別"] // 表格第一行的標題

        const sheetDatas = [ // 表格內容
         [1, 'Lily', '女'], [2, 'John', '男'], [3, 'Mary', '女'] ]
      3. 完整代碼(在Vue中的實現):
        <template>
        <div>
          <Button type="primary" @click='handleClick'>
            <slot></slot>
          </Button>
        </div>  
        </template>
        <script>
          import moment from 'moment'
          import XLSXStyle from 'js-xlsx'
        
          export default {
            data() {
              return {
                sheetTitle: ["序號", "姓名", "性別"],
                sheetDatas: [
                  [1,'Lily','女'],
                  [2,'John','男'],
                  [3,'Mary','女']
                ]
              }
            },
            methods: {
              handleClick() {
                this.downloadExl(this.sheetDatas)
              },
              downloadExl(sourceData) {
                const wopts = { bookType: 'xlsx', bookSST: true, type: 'binary', cellHeadStyles: true };
        
                // excel表格樣式
                let fontBold = {
                  font: { 
                    sz: 10, 
                    bold: true, // 粗字體
                    color: { rgb: "000000" },
                    name: "Times New Roman" 
                  }
                }
                let fontThin = {
                  font: { 
                    sz: 10, 
                    bold: false, 
                    color: { rgb: "000000" },
                    name: "Times New Roman" 
                  }
                }
                let alignmentCenter = { alignment: { horizontal: 'center' } } // 居中對齊
                let alignmentLeft = { alignment: { horizontal: 'left' } }
                let borderStyle = { // 邊框粗細和顏色
                  border: {
                    right: { 
                      style: 'thin',
                      color: { rgb: "000000" } 
                    },
                    bottom: { 
                      style: 'thin',
                      color: { rgb: "000000" } 
                    },
                  }
                }
                let cellBaseStyle = Object.assign({}, {
                  fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "FFFFFF" } },
                }, borderStyle, alignmentCenter)
                let cellHeadStyle = Object.assign({}, cellBaseStyle, fontBold)
                let cellBodyStyle = Object.assign({}, cellBaseStyle, fontThin)
        
                // 數據
                let tmpdata = sourceData[0],
                    keyMap = [],
                    outputPos1 = [],
                    outputPos2 = [], 
                    noteStyle = {},
                    titleStyle = {},
                    cols = Object.keys(this.sheetDatas[0]).length
        
                if(sourceData[0] instanceof Array) sourceData.unshift({})
                for (let k in tmpdata) {
                  keyMap.push(k)
                  sourceData[0][k] = k
                }
                tmpdata = [];//用來保存轉換好的sourceData
                sourceData.map((v, i) => keyMap.map((k, j) => Object.assign({}, {
                  v: v[k],
                  position: (j > 25 ? this.getCharCol(j) : String.fromCharCode(65 + j)) + (i + 1)
                }))).reduce((prev, next) => prev.concat(next)).forEach((v, i) => tmpdata[v.position] = {
                  v: v.v,
                  s: cellBodyStyle
                });
                outputPos1 = Object.keys(tmpdata) // 第一部分數據區域
                for(let i=0; i<cols; i++) {
                  let len = this.sheetDatas.length,
                      letter = i > 25 ? this.getCharCol(i) : String.fromCharCode(65 + i), // A~AI
                      noteObj = {},
                      titleObj = {}
        
                  outputPos2.push(letter+(this.sheetDatas.length+2)) // 第二部分數據區域,用於添加備注行
        
                  // 第一行標題樣式 
                  titleObj = {
                    [letter+'1']: {
                      v: this.sheetTitle[i],
                      s: cellHeadStyle
                    }
                  }
                  Object.assign(titleStyle, titleObj)
        
                  // 最后一行備注信息樣式
                  if(i == 0) { // 第一個cell
                    noteObj = {
                      [letter+(len+1)]: {
                        v: '備注:數據生成於' + this.sheetTime,
                        t: 's',
                        s: Object.assign({}, cellBodyStyle, alignmentLeft)
                      },
                    }
                  } else if(i == (cols-1)) { // 最后一個cell
                    noteObj = { [letter+(len+1)]: { s: borderStyle } }
                  } else {
                    noteObj = {
                      [letter+(len+1)]: {
                        s: {
                          border: {
                            bottom: { 
                              style: 'thin',
                              color: { rgb: "000000" } 
                            },
                          }
                        }
                      }
                    }
                  }
                  Object.assign(noteStyle, noteObj)
                }
        
                let outputPos = [...outputPos1, ...outputPos2]  //設置區域,比如表格從A1到D10
                let tmpdataStyle = {
                  '!merges': [{
                    s: {c: 0, r: this.sheetDatas.length},
                    e: {c: cols-1, r: this.sheetDatas.length}
                  }],
                  '!cols': [ //設置列寬
                    {wpx: 45}, /*a*/  {wpx: 85}, /*b*/  {wpx: 85}
                  ]
                };
                let tmpdataAll = Object.assign({}, tmpdata, tmpdataStyle, noteStyle, titleStyle)
                let tmpWB = {
                  SheetNames: ['人員信息'], //保存的表標題
                  Sheets: {
                    '人員信息': Object.assign({}, tmpdataAll, //內容
                    {
                      '!ref': outputPos[0] + ':' + outputPos[outputPos.length-1] //設置填充區域
                    })
                  }
                };
                let tmpDown = new Blob([this.s2ab(XLSXStyle.write(tmpWB,
                  { bookType: 'xlsx', bookSST: false, type: 'binary' } //定義導出的格式類型
                ))], { type: "" });
                this.saveAs(tmpDown, `人員信息${moment().format('YYYYMMDD')}` + '.' + (wopts.bookType == "biff2" ? "xlsx" : wopts.bookType));
              },
              saveAs(obj, fileName) {
                let tmpa = document.createElement("a");
                tmpa.download = fileName;
                tmpa.href = URL.createObjectURL(obj);
                tmpa.click();
                setTimeout(function () {
                  URL.revokeObjectURL(obj);
                }, 100);
              },
              s2ab(s) {
                let buf = new ArrayBuffer(s.length);
                let view = new Uint8Array(buf);
                for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
                return buf;
              },
              getCharCol(n) {
                let temCol = '',
                    s = '',
                    m = 0
                while (n > 0) {
                  m = n % 26 + 1
                  s = String.fromCharCode(m + 64) + s
                  n = (n - m) / 26
                }
                return s
              }
            },
            mounted() {}
          }
        </script>
      4. 輸出:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM