vue 下使用 exceljs + x-spreadsheet 带样式导入Excel

导入的样式包括字体,字号,列宽,合并单元格,【部分能识别】的背景色,文字颜色

【说明】导入的Excel 字体颜色和背景色只能识别【标准色】,别的如"主题颜色",exceljs 解析出来不是颜色值

 原Excel样式如下

 导入到 x-data-spreadsheet 如下图

 完整代码如下

<template>
  <div class="container">
    <div class="toolbar">
      <input
        type="file"
        @change="loadExcelFile"
        accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
      />
      <button @click="exportJson">导出JSON</button>
      <button @click="exportExcel">导出xlsx</button>
    </div>
    <!--web spreadsheet组件-->
    <div ref="sheetContainer" class="grid" id="x-spreadsheet-demo"></div>
  </div>
</template>

<script>
// 在spreadsheet.js中初始化json数据 https://blog.youkuaiyun.com/CBGCampus/article/details/125366246
// 带样式导出 https://blog.youkuaiyun.com/weixin_42302145/article/details/121476579
// 引入依赖包
import Spreadsheet from "x-data-spreadsheet";
import zhCN from 'x-data-spreadsheet/src/locale/zh-cn'
import _ from "lodash";
import * as XLSX from "xlsx";
import * as Excel from 'exceljs/dist/exceljs'
import * as tinycolor from "tinycolor2";
export default {
  name: "xspreadsheet-demo",
  data() {
    return {
      xs: null,
      jsondata: {
        type: "",
        label: "",
      },
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      //设置中文
      Spreadsheet.locale("zh-cn", zhCN);
      this.xs = new Spreadsheet("#x-spreadsheet-demo", {
        mode: "edit",
        showToolbar: true,
        showGrid: true,
        showContextmenu: true,
        showBottomBar: true,
        view: {
          height: () => this.$refs.sheetContainer.offsetHeight,
          width: () => this.$refs.sheetContainer.offsetWidth,
        },
        formats: [],
        fonts: [],
        formula: [],
        row: {
          len: 100,
          height: 25,
        },
        col: {
          len: 26,
          width: 100,
          indexWidth: 60,
          minWidth: 60,
        },
        style: {
          bgcolor: "#ffffff",
          align: "left",
          valign: "middle",
          textwrap: false,
          textDecoration: "normal",
          strikethrough: false,
          color: "#0a0a0a",
          font: {
            name: "Helvetica",
            size: 10,
            bold: false,
            italic: false,
          }
        },
      })
        .loadData([])
        .change((cdata) => {
          // console.log(cdata);
          console.log(">>>", this.xs.getData());
        });

      this.xs
        .on("cell-selected", (cell, ri, ci) => {
          console.log("cell:", cell, ", ri:", ri, ", ci:", ci);
        })
        .on("cell-edited", (text, ri, ci) => {
          console.log("text:", text, ", ri: ", ri, ", ci:", ci);
        });

      setTimeout(() => {
        // xs.loadData([{ rows }]);
        // xs.cellText(14, 3, 'cell-text').reRender();
        // console.log('cell(8, 8):', this.xs.cell(8, 8));
        // console.log('cellStyle(8, 8):', this.xs.cellStyle(8, 8));
      }, 5000);
    },
    transRgba(rgba) {
          let result = '';
          let reg = /\(.*\)/;  //字符串匹配括号内的子串
          let arr = []
          result = reg.exec(rgba)[0];  // 截取'(255,255,255,0.6)'
          result = result.substr(1, result.length - 2); //截取十进制的'255,255,255,0.6'
          arr = result.split(','); //字符串切割 ['255','255','255','0.6']
          for (let i = 0; i < arr.length; i++) {
              arr[i] = parseFloat(arr[i]); //字符串类型转为浮点数类型
              if (i == arr.length - 1) {  //对于最后一个透明度数据,需要先将0-1的数值*255
                  arr[i] = arr[i] * 255;
              }
  
              arr[i] = trans10to16(arr[i]);
          }
          return arr.join('');
    },
    // 向x-spreadsheet 带样式导入Excel
      loadExcelFile(e) {
      const wb = new Excel.Workbook();
      const reader = new FileReader()
      reader.readAsArrayBuffer(e.target.files[0])
      reader.onload = () => {
        const buffer = reader.result;
        // 微软的 Excel ColorIndex 一个索引数字对应一个颜色
        const indexedColors = [
          '000000',
          'FFFFFF',
          'FF0000',
          '00FF00',
          '0000FF',
          'FFFF00',
          'FF00FF',
          '00FFFF',
          '000000',
          'FFFFFF',
          'FF0000',
          '00FF00',
          '0000FF',
          'FFFF00',
          'FF00FF',
          '00FFFF',
          '800000',
          '008000',
          '000080',
          '808000',
          '800080',
          '008080',
          'C0C0C0',
          '808080',
          '9999FF',
          '993366',
          'FFFFCC',
          'CCFFFF',
          '660066',
          'FF8080',
          '0066CC',
          'CCCCFF',
          '000080',
          'FF00FF',
          'FFFF00',
          '00FFFF',
          '800080',
          '800000',
          '008080',
          '0000FF',
          '00CCFF',
          'CCFFFF',
          'CCFFCC',
          'FFFF99',
          '99CCFF',
          'FF99CC',
          'CC99FF',
          'FFCC99',
          '3366FF',
          '33CCCC',
          '99CC00',
          'FFCC00',
          'FF9900',
          'FF6600',
          '666699',
          '969696',
          '003366',
          '339966',
          '003300',
          '333300',
          '993300',
          '993366',
          '333399',
          '333333',
        ];
        wb.xlsx.load(buffer).then(workbook => {
          let workbookData = []
          console.log(workbook)
          workbook.eachSheet((sheet, sheetIndex) => {
            // 构造x-data-spreadsheet 的 sheet 数据源结构
            let sheetData = { name: sheet.name,styles : [], rows: {}, merges:[] }
            // 收集合并单元格信息
            let mergeAddressData = []
            for(let mergeRange in sheet._merges) {
              sheetData.merges.push(sheet._merges[mergeRange].shortRange)
              let mergeAddress = {}
              // 合并单元格起始地址
              mergeAddress.startAddress = sheet._merges[mergeRange].tl
              // 合并单元格终止地址
              mergeAddress.endAddress = sheet._merges[mergeRange].br
              // Y轴方向跨度
              mergeAddress.YRange = sheet._merges[mergeRange].model.bottom - sheet._merges[mergeRange].model.top
              // X轴方向跨度
              mergeAddress.XRange = sheet._merges[mergeRange].model.right - sheet._merges[mergeRange].model.left
              mergeAddressData.push(mergeAddress)
            }
            sheetData.cols = {}
            for(let i = 0;i < sheet.columns.length; i++)
            {
              sheetData.cols[i.toString()] = {}
              if(sheet.columns[i].width) {
                // 不知道为什么从 exceljs 读取的宽度显示到 x-data-spreadsheet 特别小, 这里乘以8
                sheetData.cols[i.toString()].width = sheet.columns[i].width * 8
              } else {
                // 默认列宽
                sheetData.cols[i.toString()].width = 100
              }
            }

            // 遍历行
            sheet.eachRow((row, rowIndex) => {
              sheetData.rows[(rowIndex - 1).toString()] = { cells: {} }         
              //includeEmpty = false 不包含空白单元格
              row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
                let cellText = ''
                if(cell.value && cell.value.result) {
                  // Excel 单元格有公式
                  cellText = cell.value.result
                } else if(cell.value && cell.value.richText) {
                  // Excel 单元格是多行文本
                  for(let text in cell.value.richText) {
                    // 多行文本做累加
                    cellText += cell.value.richText[text].text
                  }
                }
                else {
                  // Excel 单元格无公式
                  cellText = cell.value
                }
                
                //解析单元格,包含样式
                //*********************单元格存在背景色******************************
                // 单元格存在背景色
                let backGroundColor = null
                if(cell.style.fill && cell.style.fill.fgColor && cell.style.fill.fgColor.argb) {
                  // 8位字符颜色先转rgb再转16进制颜色
                  backGroundColor = ((val) => {
                    val = val.trim().toLowerCase();  //去掉前后空格
                    let color = {};
                    try {
                        let argb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(val);
                        color.r = parseInt(argb[2], 16);
                        color.g = parseInt(argb[3], 16);
                        color.b = parseInt(argb[4], 16);
                        color.a = parseInt(argb[1], 16) / 255;
                        return tinycolor(`rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`).toHexString()
                    } catch (e) {
                      console.log(e)
                    }
                  })(cell.style.fill.fgColor.argb)
                } 

                if(backGroundColor) {
                  cell.style.bgcolor = backGroundColor
                }
                //*************************************************************************** */
                
                //*********************字体存在背景色******************************
                // 字体颜色
                let fontColor = null
                if(cell.style.font && cell.style.font.color && cell.style.font.color.argb) {
                  // 8位字符颜色先转rgb再转16进制颜色
                  fontColor = ((val) => {
                    val = val.trim().toLowerCase();  //去掉前后空格
                    let color = {};
                    try {
                        let argb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(val)
                        color.r = parseInt(argb[2], 16);
                        color.g = parseInt(argb[3], 16);
                        color.b = parseInt(argb[4], 16);
                        color.a = parseInt(argb[1], 16) / 255;
                        return tinycolor(`rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`).toHexString()
                    } catch (e) {
                      console.log(e)
                    }
                  })(cell.style.font.color.argb)
                }
                if(fontColor) {
                  //console.log(fontColor)
                  cell.style.color = fontColor
                }
                //************************************************************************ */

                // exceljs 对齐的格式转成 x-date-spreedsheet 能识别的对齐格式 
                if(cell.style.alignment && cell.style.alignment.horizontal) {
                  cell.style.align = cell.style.alignment.horizontal
                  cell.style.valign = cell.style.alignment.vertical
                }

                //处理合并单元格
                let mergeAddress = _.find(mergeAddressData, function(o) { return o.startAddress == cell._address })
                if(mergeAddress) 
                {
                  // 遍历的单元格属于合并单元格
                  if(cell.master.address != mergeAddress.startAddress){
                    // 不是合并单元格中的第一个单元格不需要计入数据源
                    return
                  }
                  // 说明是合并单元格区域的起始单元格
                  sheetData.rows[(rowIndex - 1).toString()].cells[(colNumber - 1).toString()] = { text: cellText, style: 0, merge: [mergeAddress.YRange, mergeAddress.XRange] }
                  sheetData.styles.push(cell.style)
                  //对应的style存放序号
                  sheetData.rows[(rowIndex - 1).toString()].cells[(colNumber - 1).toString()].style = sheetData.styles.length - 1
                }
                else {
                  // 非合并单元格
                  sheetData.rows[(rowIndex - 1).toString()].cells[(colNumber - 1).toString()] = { text: cellText, style: 0 }
                  //解析单元格,包含样式
                  sheetData.styles.push(cell.style)
                  //对应的style存放序号
                  sheetData.rows[(rowIndex - 1).toString()].cells[(colNumber - 1).toString()].style = sheetData.styles.length - 1
                }
              });
            })
            workbookData.push(sheetData)
          })
          this.xs.loadData(workbookData);
        })
      }
    },
    // 导出excel
    exportExcel() {
      let new_wb = XLSX.utils.book_new();
      this.xs.getData().forEach(function (xws) {
        let aoa = [[]];
        let rowobj = xws.rows;
        for (let ri = 0; ri < rowobj.len; ++ri) {
          let row = rowobj[ri];
          if (!row) continue;
          aoa[ri] = [];
          Object.keys(row.cells).forEach(function (k) {
            let idx = +k;
            if (isNaN(idx)) return;
            aoa[ri][idx] = row.cells[k].text;
          });
        }
        let ws = XLSX.utils.aoa_to_sheet(aoa);
        XLSX.utils.book_append_sheet(new_wb, ws, xws.name);
      });
      XLSX.writeFile(new_wb, "调配单变量配置.xlsx");
    },
    // 导出为 JSON
    exportJson() {
      let sheetsData = this.xs.getData();
      let rows = Object.entries(sheetsData[0].rows);
      let objectProperties = [
        "Index",
        "OrderIndex",
        "OrderNo",
        "ProductName",
        "OrderStatus",
      ];
      let jsonData = [];
      // 遍历数据,跳过第一行表头
      for (let i = 1; i < rows.length; i++) {
        if (rows[i] && rows[i][1] && rows[i][1].cells) {
          let row = Object.entries(rows[i][1].cells);
          // 构造行对象
          let JsonRow = {
            Index: null,
            OrderIndex: null,
            OrderNo: null,
            ProductName: null,
            OrderStatus: null,
          };
          for (let k = 0; k < row.length; k++) {
            let cells = row[k];
            JsonRow[objectProperties[k]] = cells[1].text;
          }
          jsonData.push(JsonRow);
        }
      }
      console.log(jsonData);
    },
    stox(wb) {
      var out = [];
      wb.SheetNames.forEach(function (name) {
        var o = { name: name, rows: {} };
        var ws = wb.Sheets[name];
        var aoa = XLSX.utils.sheet_to_json(ws, { raw: false, header: 1 });
        aoa.forEach(function (r, i) {
          var cells = {};
          r.forEach(function (c, j) {
            cells[j] = { text: c };
          });
          o.rows[i] = { cells: cells };
        });
        out.push(o);
      });
      return out;
    },
    fixData(data) {
      var o = "",
        l = 0,
        w = 10240;
      for (; l < data.byteLength / w; ++l)
        o += String.fromCharCode.apply(
          null,
          new Uint8Array(data.slice(l * w, l * w + w))
        );
      o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)));
      return o;
    },
  },
};
</script>
<style lang="less" scoped>
.container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .toolbar {
    width: 100%;
    height: 50px;
  }
  .grid {
    width: 100%;
    height: calc(100% - 80px);
  }
  /deep/ .x-spreadsheet-toolbar {
    padding: 0px;
    width: calc(100% - 2px) !important;
  }
}
</style>

不少人说这报错,那报错的,我整个代码都好几年前的了,我也不想再去看,所幸源码还在,我把package.json 贴出来,你们自己研究吧

{
	"name": "vue_study",
	"version": "20230905.134743",
	"description": "A Vue.js project",
	"author": "",
	"private": true,
	"scripts": {
		"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
		"start": "npm run dev",
		"lint": "eslint --ext .js,.vue src",
		"build": "node modifyVersion.js && node build/build.js",
		"demo": "node modifyVersion.js && node build/build.demo.js",
		"ali": "node modifyVersion.js && node build/build.ali.js",
		"local": "node modifyVersion.js && node build/build.local.js"
	},
	"dependencies": {
		"-": "0.0.1",
		"@fullcalendar/core": "^6.0.2",
		"@fullcalendar/daygrid": "^6.0.2",
		"@fullcalendar/interaction": "^6.0.2",
		"@fullcalendar/timegrid": "^6.0.2",
		"@fullcalendar/vue": "^6.0.2",
		"@jiaminghi/data-view": "^2.10.0",
		"@riophae/vue-treeselect": "^0.4.0",
		"axios": "^0.21.1",
		"cancel_axios_request": "^1.0.5",
		"default-passive-events": "^2.0.0",
		"echarts": "^5.3.3",
		"echarts-gl": "^2.0.9",
		"element-ui": "^2.15.13",
		"exceljs": "^4.3.0",
		"font-awesome": "^4.7.0",
		"fullcalendar": "^6.0.2",
		"highcharts": "^11.0.1",
		"interactjs": "^1.10.11",
		"jquery": "^3.6.0",
		"js-guid": "^1.0.0",
		"js-md5": "^0.7.3",
		"jsplumb": "^2.15.6",
		"leader-line": "^1.0.7",
		"leader-line-vue": "^2.1.1",
		"less": "^4.1.2",
		"linq-es2015": "^2.5.1",
		"lodash": "^4.17.21",
		"mockjs": "^1.1.0",
		"moment": "^2.29.1",
		"moment-recur": "^1.0.7",
		"nzh": "^1.0.8",
		"print-js": "^1.6.0",
		"print.js": "^1.0.18",
		"qrious": "^4.0.2",
		"qs": "^6.10.1",
		"save": "^2.5.0",
		"side-channel": "^1.0.4",
		"signalr": "^2.4.2",
		"snapsvg": "^0.5.1",
		"snapsvg-cjs": "^0.0.6",
		"sortablejs": "^1.15.0",
		"tinycolor2": "^1.4.2",
		"vue": "^2.6.0",
		"vue-axios": "^3.2.4",
		"vue-froala-wysiwyg": "^4.0.8",
		"vue-giant-tree": "^0.1.5",
		"vue-grid-layout": "^2.3.7",
		"vue-interactjs": "^0.1.10",
		"vue-resize": "^1.0.1",
		"vue-resize-directive": "^1.2.0",
		"vue-resize-observer": "^1.0.37",
		"vue-router": "^3.0.7",
		"vue-text-format": "^1.2.6",
		"vuex": "^3.6.2",
		"webpack-cli": "^4.9.2",
		"x-data-spreadsheet": "^1.1.9",
		"xlsx": "^0.18.5"
	},
	"devDependencies": {
		"autoprefixer": "^7.1.2",
		"babel-core": "^6.22.1",
		"babel-eslint": "^8.2.1",
		"babel-helper-vue-jsx-merge-props": "^2.0.3",
		"babel-loader": "^7.1.1",
		"babel-plugin-syntax-jsx": "^6.18.0",
		"babel-plugin-transform-runtime": "^6.22.0",
		"babel-plugin-transform-vue-jsx": "^3.5.0",
		"babel-preset-env": "^1.3.2",
		"babel-preset-stage-2": "^6.22.0",
		"chalk": "^2.0.1",
		"copy-webpack-plugin": "^4.0.1",
		"css-loader": "^0.28.0",
		"eslint": "^4.15.0",
		"eslint-config-standard": "^10.2.1",
		"eslint-friendly-formatter": "^3.0.0",
		"eslint-loader": "^1.7.1",
		"eslint-plugin-import": "^2.7.0",
		"eslint-plugin-node": "^5.2.0",
		"eslint-plugin-promise": "^3.4.0",
		"eslint-plugin-standard": "^3.0.1",
		"eslint-plugin-vue": "^4.0.0",
		"extract-text-webpack-plugin": "^3.0.0",
		"file-loader": "^1.1.4",
		"friendly-errors-webpack-plugin": "^1.6.1",
		"html-webpack-plugin": "^2.30.1",
		"less-loader": "^5.0.0",
		"node-notifier": "^5.1.2",
		"optimize-css-assets-webpack-plugin": "^3.2.0",
		"ora": "^1.2.0",
		"portfinder": "^1.0.13",
		"postcss-import": "^11.0.0",
		"postcss-loader": "^2.0.8",
		"postcss-url": "^7.2.1",
		"rimraf": "^2.6.0",
		"semver": "^5.3.0",
		"shelljs": "^0.7.6",
		"uglifyjs-webpack-plugin": "^1.1.1",
		"url-loader": "^0.5.8",
		"vue-loader": "^13.3.0",
		"vue-style-loader": "^3.0.1",
		"vue-template-compiler": "^2.6.0",
		"webpack": "^3.6.0",
		"webpack-bundle-analyzer": "^2.9.0",
		"webpack-dev-server": "^2.11.5",
		"webpack-merge": "^4.1.0"
	},
	"engines": {
		"node": ">= 6.0.0",
		"npm": ">= 3.0.0"
	},
	"browserslist": [
		"> 1%",
		"last 2 versions",
		"not ie <= 8"
	],
	"lastBuildTime": "2023-09-05 13:47:43"
}

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值