基于Handsontable.js + Excel.js实现表格预览和导出功能(公式渲染)

本文记录在html中基于Handsontable.js + Excel.js实现表格预览功能。

Handsontable官方文档

一、开发前的准备引入相关依赖库
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>基于Handsontable.js + Excel.js实现表格预览功能</title>
  <!-- handsontable的css文件 https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.css -->
  <link rel="stylesheet" href="./lib/handsontable.full.min.css">
</head>
<body>
  <!-- https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js -->
  <script src="./lib/handsontable.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/handsontable/dist/languages/zh-CN.js -->
  <script src="./lib/zh-CN.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/color-js -->
  <script src="./lib/color-js.js"></script>
  <!-- https://cdnjs.cloudflare.com/ajax/libs/hyperformula/1.4.0/hyperformula.min.js -->
  <script src="./lib/hyperformula.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/exceljs/dist/exceljs.min.js -->
  <script src="./lib/exceljs.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js -->
  <script src="./lib/xlsx.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/fxparser@1.0.0/dist/fxparser.min.js -->
  <script src="./lib/fxparser.min.js"></script>
</body>
</html>
二、编写页面布局
<body>
  <input type="file" id="file">
  <button id="btn">预览</button>
  <button id="export">导出</button>
</body>
三、编写预览核心代码
<script>
  var hot; //handsontable实例
  var themeJson; //主题json
  var sheet; //当前sheet
  var Color = net.brehaut.Color; //引入color-js库
  
  // 自定义渲染器函数
  function customRenderer(hotInstance, td, row, column, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer(hotInstance, td, row, column, prop, value, cellProperties);
    // 填充样式
    if ("fill" in cellProperties) {
      // 背景颜色
      if ("fgColor" in cellProperties.fill && cellProperties.fill.fgColor) {
        td.style.background = getColor(cellProperties.fill.fgColor,  themeJson);
      }
    }
    // 字体样式
    if ("font" in cellProperties) {
      // 加粗
      if ("bold" in cellProperties.font && cellProperties.font.bold) {
        td.style.fontWeight = "700";
      }
      // 字体颜色
      if ("color" in cellProperties.font && cellProperties.font.color) {
        td.style.color = getColor(cellProperties.font.color, themeJson);
      }
      // 字体大小
      if ("size" in cellProperties.font && cellProperties.font.size) {
        td.style.fontSize = cellProperties.font.size + "px";
      }
      // 字体类型
      if ("name" in cellProperties.font && cellProperties.font.name) {
        td.style.fontFamily = cellProperties.font.name;
      }
      // 字体倾斜
      if ("italic" in cellProperties.font && cellProperties.font.italic) {
        td.style.fontStyle = "italic";
      }
      // 下划线
      if (
        "underline" in cellProperties.font &&
        cellProperties.font.underline
      ) {
        // 其实还有双下划线,但是双下划綫css中没有提供直接的设置方式,需要使用额外的css设置,所以我也就先懒得弄了
        td.style.textDecoration = "underline";
        // 删除线
        if ("strike" in cellProperties.font && cellProperties.font.strike) {
          td.style.textDecoration = "underline line-through";
        }
      } else {
        // 删除线
        if ("strike" in cellProperties.font && cellProperties.font.strike) {
          td.style.textDecoration = "line-through";
        }
      }
    }
    // 对齐
    if ("alignment" in cellProperties) {
        if ("horizontal" in cellProperties.alignment) {
          // 水平
          // 这里我直接用handsontable内置类做了,设置成类似htLeft的样子。
          //(handsontable)其实至支持htLeft, htCenter, htRight, htJustify四种,但是其是它还有centerContinuous、distributed、fill,遇到这几种就会没有效果,也可以自己设置,但是我还是懒的弄了,用到的时候再说吧
          const name =
            cellProperties.alignment.horizontal.charAt(0).toUpperCase() +
            cellProperties.alignment.horizontal.slice(1);
            td.classList.add(`ht${name}`);
        }
        if ("vertical" in cellProperties.alignment) {
          // 垂直
          // 这里我直接用handsontable内置类做了,设置成类似htTop的样子。
          const name =
            cellProperties.alignment.vertical.charAt(0).toUpperCase() +
            cellProperties.alignment.vertical.slice(1);
            td.classList.add(`ht${name}`);
        }
    }
    // 边框
    if ("border" in cellProperties) {
        if ("left" in cellProperties.border && cellProperties.border.left) {
          // 左边框
          const [borderWidth, borderStyle] = setBorder(
            cellProperties.border.left.style
          );
          let color = "";
          // console.log(row, column, borderWidth, borderStyle);
          if (cellProperties.border.left.color) {
            color = getColor(cellProperties.border.left.color, themeJson);
          }
          td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
        }
        if ("right" in cellProperties.border && cellProperties.border.right) {
          // 左边框
          const [borderWidth, borderStyle] = setBorder(
            cellProperties.border.right.style
          );
          // console.log(row, column, borderWidth, borderStyle);
          let color = "";
          if (cellProperties.border.right.color) {
            color = getColor(cellProperties.border.right.color, themeJson);
          }
          td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
        }
        if ("top" in cellProperties.border && cellProperties.border.top) {
          // 左边框
          const [borderWidth, borderStyle] = setBorder(
            cellProperties.border.top.style
          );
          let color = "";
          // console.log(row, column, borderWidth, borderStyle);
          if (cellProperties.border.top.color) {
            color = getColor(cellProperties.border.top.color, themeJson);
          }
          td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
        }
      if ("bottom" in cellProperties.border && cellProperties.border.bottom) {
        // 左边框
        const [borderWidth, borderStyle] = setBorder(
          cellProperties.border.bottom.style
        );
        let color = "";
        // console.log(row, column, borderWidth, borderStyle);
        if (cellProperties.border.bottom.color) {
          color = getColor(cellProperties.border.bottom.color, themeJson);
        }
        td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
      }
    }
  }

  // 在 Handsontable 初始化之前注册渲染器
  Handsontable.renderers.registerRenderer('customStylesRenderer', customRenderer);

  // 点击预览按钮
  document.getElementById('btn').addEventListener('click', async function () {
    var hotData = null; // Handsontable 数据
    var file = document.getElementById('file').files[0]; // 获取文件对象
    const workbook = new ExcelJS.Workbook(); // 创建一个工作簿对象
    await workbook.xlsx.load(file); // 加载Excel文件
    const worksheet = workbook.getWorksheet(1); // 获取第一个工作表
    sheet = worksheet; // 将工作表赋值给全局变量

    // 遍历工作表中的所有行(包括空行)
    const sheetData = [];
    worksheet.eachRow({ includeEmpty: true }, function (row, rowNumber) {
      const row_values = row.values.slice(1); // 获取行数据,并排除第一列为null的数据
      const newRowValue = [...row_values];
      // 将行数据添加到sheetData数组中
      sheetData.push(newRowValue);
    });
    // 将数据赋值给Handsontable
    hotData = sheetData; 

    // 将主题xml转换成json
    const themeXml = workbook._themes.theme1;
    const options = {
      ignoreAttributes: false,
      attributeNamePrefix: "_",
    };
    const parser = new XMLParser(options);
    const json = parser.parse(themeXml);
    themeJson = json

    // 获取合并的单元格
    const mergeCells = [];
    for (let i in worksheet._merges) {
      const { top, left, bottom, right } = worksheet._merges[i].model;
      mergeCells.push({
        row: top - 1,
        col: left - 1,
        rowspan: bottom - top + 1,
        colspan: right - left + 1
      });
    };

    // 将数据加载到HyperFormula
    hot = new Handsontable(document.getElementById('hot'), {
      // 数据
      data: hotData,
      colHeaders: true,
      rowHeaders: true,
      language: 'zh-CN',
      readOnly: true,
      width: '100%',
      height: 'calc(100% - 25px)',
      //handsontable的许可证
      licenseKey: 'non-commercial-and-evaluation',
      // 行高
      rowHeights: function (index) {
        if (sheet.getRow(index + 1).height) {
          // exceljs获取的行高不是像素值,事实上,它是23px - 13.8 的一个映射。所以需要将它转化为像素值
          return sheet.getRow(index + 1).height * (23 / 13.8);
        }
        return 23; // 默认
      },
      // 列宽
      colWidths: function (index) {
        if (sheet.getColumn(index + 1).width) {
          // exceljs获取的列宽不是像素值,事实上,它是81px - 8.22 的一个映射。所以需要将它转化为像素值
          return sheet.getColumn(index + 1).width * (81 / 8.22);
        }
        return 81; // 默认
      },
      // 自定义单元格样式
      cells: function (row, col, prop) {
        const cellProperties = {};
        const cellStyle = sheet.getCell(row + 1, col + 1).style;

        if (JSON.stringify(cellStyle) !== "{}") {
          // console.log(row+1, col+1, cellStyle);
          for (let key in cellStyle) {
              cellProperties[key] = cellStyle[key];
          }
        }
        return { ...cellProperties, renderer: "customStylesRenderer" };
      },
      // 合并单元格
      mergeCells: mergeCells
    });
    //8.将handsontable实例渲染到页面上
    hot.render();
  });

  // 根据主题和明暗度themeId获取颜色
  function getThemeColor(themeJson, themeId, tint) {
    let color = "";
    const themeColorScheme =themeJson["a:theme"]["a:themeElements"]["a:clrScheme"];
    switch (themeId) {
      case 0:
        color = themeColorScheme["a:lt1"]["a:sysClr"]["_lastClr"];
        break;
      case 1:
        color = themeColorScheme["a:dk1"]["a:sysClr"]["_lastClr"];
        break;
      case 2:
        color = themeColorScheme["a:lt2"]["a:srgbClr"]["_val"];
        break;
      case 3:
        color = themeColorScheme["a:dk2"]["a:srgbClr"]["_val"];
        break;
      default:
        color = themeColorScheme[`a:accent${themeId - 3}`]["a:srgbClr"]["_val"];
        break;
    }
    // 根据tint修改颜色深浅
    color = "#" + color;
    const colorObj = Color(color);
    if (tint) {
      if (tint > 0) {
        // 淡色
        color = colorObj.lighten(tint).hex();
      } else {
        // 深色
        color = colorObj.darken(Math.abs(tint)).hex();
      }
    }
    return color;
  }
  
  // 获取颜色
  function getColor(obj, themeJson) {
    if ("argb" in obj) {
      // 标准色
      // rgba格式去掉前两位: FFFF0000 -> FF0000
      return "#" + obj.argb.substring(2);
    } else if ("theme" in obj) {
      // 主题颜色
      if ("tint" in obj) {
        return getThemeColor(themeJson, obj.theme, obj.tint);
      } else {
        return getThemeColor(themeJson, obj.theme, null);
      }
    }
  }

  // 设置边框
  function setBorder(style) {
      let borderStyle = "solid";
      let borderWidth = "1px";
      switch (style) {
        case "thin":
          borderWidth = "thin";
          break;
        case "dotted":
          borderStyle = "dotted";
          break;
        case "dashDot":
          borderStyle = "dashed";
          break;
        case "hair":
          borderStyle = "solid";
          break;
        case "dashDotDot":
          borderStyle = "dashed";
          break;
        case "slantDashDot":
            borderStyle = "dashed";
            break;
        case "medium":
          borderWidth = "2px";
          break;
        case "mediumDashed":
          borderStyle = "dashed";
          borderWidth = "2px";
          break;
        case "mediumDashDotDot":
          borderStyle = "dashed";
          borderWidth = "2px";
          break;
        case "mdeiumDashDot":
          borderStyle = "dashed";
          borderWidth = "2px";
          break;
        case "double":
          borderStyle = "double";
          break;
        case "thick":
          borderWidth = "3px";
          break;
        default:
          break;
    }
    // console.log(borderStyle, borderWidth);
    return [borderStyle, borderWidth];
  }
</script>

如下图所示:

在这里插入图片描述

通过以上代码,我们成功将Excel文件中的数据、样式、合并单元格等信息加载到了Handsontable中,并渲染到了页面上。

对于单元格中带有公式的,按照以上代码会出现问题,就是带有公式的单元格会渲染成"[object Object]",如下图所示:

请添加图片描述

所以我们需要对公式进行处理,将公式替换为对应的值。请看以下处理公式的方法。

在遍历单元格时我们可以打印看下读取到单元格的数据具体是什么样的,然后根据具体情况进行处理,如下图所示:

在这里插入图片描述
由图可以看到带有公式的单元格是一个对象,这就是带有公式的单元格渲染后是"[object Object]"的原因。解决方法就是将带有公式的单元格替换为对应的值,请看以下代码:

worksheet.eachRow({ includeEmpty: true }, function (row, rowNumber) {
  const row_values = row.values.slice(1); // 获取行数据,并排除第一列为null的数据
  const newRowValue = [...row_values];
  newRowValue.forEach(function(item, index) {
    if(Object.prototype.toString.call(item) === "[object Object]") {
      if(item.formula) {
        // 如果是公式,则保留公式,否则将结果作为值
        newRowValue[index] = item.formula.includes("=") ? item.formula : "=" + item.result;
      }
    };
  })
  sheetData.push(newRowValue);
});
通过处理后,带有公示的单元格不在渲染为"[object Object]“,但是会渲染出具体使用了什么公式,如:”=SUM(B1:B2)“会渲染为”=2",如下图所示:

在这里插入图片描述
解决方法请看以下代码:

// 定义HyperFormula 公式配置配置
const hyperformulaInstance = HyperFormula.buildEmpty({
  licenseKey: 'internal-use-in-handsontable'
});
hot = new Handsontable(document.getElementById('hot'), {
  // 数据
  data: hotData,
  colHeaders: true,
  rowHeaders: true,
  language: 'zh-CN',
  readOnly: true,
  // 开启公式
  formulas: {
    engine: hyperformulaInstance,
    sheetName: "Sheet1",
  },
  width: '100%',
  height: 'calc(100% - 25px)',
  //handsontable的许可证
  licenseKey: 'non-commercial-and-evaluation',
})
我们开启公式后,带有公示的单元格会渲染为对应的值,如下图所示:

在这里插入图片描述
至此,我们成功将Excel文件中的数据、样式、合并单元格等信息加载到了Handsontable中,并渲染到了页面上。

使用Handsontable导出Excel文件,请看以下代码:
// 将handsontable实例导出为excel文件
document.getElementById('export').addEventListener('click', function () {
  var exportData = Handsontable.helper.createEmptySpreadsheetData(100, 100);
  hot.getData().forEach(function (row, rowIndex) {
    row.forEach(function (cell, colIndex) {
      exportData[rowIndex][colIndex] = cell;
    });
  });
  var workbook = XLSX.utils.book_new();
  var worksheet = XLSX.utils.aoa_to_sheet(exportData);
  XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
  XLSX.writeFile(workbook, 'export.xlsx');
});

此处贴出整体代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>基于Handsontable.js + Excel.js实现表格预览功能</title>
  <!-- handsontable的css文件 https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.css -->
  <link rel="stylesheet" href="./lib/handsontable.full.min.css">
  <style>
    * {
        margin: 0;
        padding: 0;
    }

    html,
    body {
        width: 100%;
        height: 100%;
    }
  </style>
</head>

<body>
  <input type="file" id="file">
  <button id="btn">预览</button>
  <button id="export">导出</button>
  <!-- handsontable的容器 -->
  <div id="hot"></div>
  <!-- https://cdn.jsdelivr.net/npm/color-js -->
  <script src="./lib/color-js.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js -->
  <script src="./lib/handsontable.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/handsontable/dist/languages/zh-CN.js -->
  <script src="./lib/zh-CN.js"></script>
  <!-- https://cdnjs.cloudflare.com/ajax/libs/hyperformula/1.4.0/hyperformula.min.js -->
  <script src="./lib/hyperformula.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/exceljs/dist/exceljs.min.js -->
  <script src="./lib/exceljs.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js -->
  <script src="./lib/xlsx.full.min.js"></script>
  <!-- https://cdn.jsdelivr.net/npm/fxparser@1.0.0/dist/fxparser.min.js -->
  <script src="./lib/fxparser.min.js"></script>
    
  <script>
      //在html中使用excel+handsontable实现excel预览功能
      window.onload = function () {
        //1.引入handsontable的css和js文件
        //3.获取excel文件
        var hot; //handsontable实例
        var themeJson; //主题json
        var sheet; //当前sheet
        var Color = net.brehaut.Color; //引入color-js库
        
        // 自定义渲染器函数
        function customRenderer(hotInstance, td, row, column, prop, value, cellProperties) {
            Handsontable.renderers.TextRenderer(hotInstance, td, row, column, prop, value, cellProperties);
            // 填充样式
            if ("fill" in cellProperties) {
              // 背景颜色
              if ("fgColor" in cellProperties.fill && cellProperties.fill.fgColor) {
                td.style.background = getColor(
                  cellProperties.fill.fgColor,
                  themeJson
                );
              }
            }
            // 字体样式
            if ("font" in cellProperties) {
              // 加粗
              if ("bold" in cellProperties.font && cellProperties.font.bold) {
                td.style.fontWeight = "700";
              }
              // 字体颜色
              if ("color" in cellProperties.font && cellProperties.font.color) {
                td.style.color = getColor(cellProperties.font.color, themeJson);
              }
              // 字体大小
              if ("size" in cellProperties.font && cellProperties.font.size) {
                td.style.fontSize = cellProperties.font.size + "px";
              }
              // 字体类型
              if ("name" in cellProperties.font && cellProperties.font.name) {
                td.style.fontFamily = cellProperties.font.name;
              }
              // 字体倾斜
              if ("italic" in cellProperties.font && cellProperties.font.italic) {
                td.style.fontStyle = "italic";
              }
              // 下划线
              if (
                "underline" in cellProperties.font &&
                cellProperties.font.underline
              ) {
                // 其实还有双下划线,但是双下划綫css中没有提供直接的设置方式,需要使用额外的css设置,所以我也就先懒得弄了
                td.style.textDecoration = "underline";
                // 删除线
                if ("strike" in cellProperties.font && cellProperties.font.strike) {
                  td.style.textDecoration = "underline line-through";
                }
              } else {
                // 删除线
                if ("strike" in cellProperties.font && cellProperties.font.strike) {
                  td.style.textDecoration = "line-through";
                }
              }
            }
            // 对齐
            if ("alignment" in cellProperties) {
              if ("horizontal" in cellProperties.alignment) {
                // 水平
                // 这里我直接用handsontable内置类做了,设置成类似htLeft的样子。
                //(handsontable)其实至支持htLeft, htCenter, htRight, htJustify四种,但是其是它还有centerContinuous、distributed、fill,遇到这几种就会没有效果,也可以自己设置,但是我还是懒的弄了,用到的时候再说吧
                const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() +cellProperties.alignment.horizontal.slice(1);
                td.classList.add(`ht${name}`);
              }
              if ("vertical" in cellProperties.alignment) {
                // 垂直
                // 这里我直接用handsontable内置类做了,设置成类似htTop的样子。
                const name =
                cellProperties.alignment.vertical.charAt(0).toUpperCase() +
                cellProperties.alignment.vertical.slice(1);
                td.classList.add(`ht${name}`);
              }
            }
            // 边框
            if ("border" in cellProperties) {
                if ("left" in cellProperties.border && cellProperties.border.left) {
                  // 左边框
                  const [borderWidth, borderStyle] = setBorder(
                    cellProperties.border.left.style
                  );
                  let color = "";
                  // console.log(row, column, borderWidth, borderStyle);
                  if (cellProperties.border.left.color) {
                    color = getColor(cellProperties.border.left.color, themeJson);
                  }
                  td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
                }
                if ("right" in cellProperties.border && cellProperties.border.right) {
                  // 左边框
                  const [borderWidth, borderStyle] = setBorder(
                    cellProperties.border.right.style
                  );
                  // console.log(row, column, borderWidth, borderStyle);
                  let color = "";
                  if (cellProperties.border.right.color) {
                    color = getColor(cellProperties.border.right.color, themeJson);
                  }
                  td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
                }
                if ("top" in cellProperties.border && cellProperties.border.top) {
                  // 左边框
                  const [borderWidth, borderStyle] = setBorder(
                    cellProperties.border.top.style
                  );
                  let color = "";
                  // console.log(row, column, borderWidth, borderStyle);
                  if (cellProperties.border.top.color) {
                    color = getColor(cellProperties.border.top.color, themeJson);
                  }
                  td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
                }
                if ("bottom" in cellProperties.border && cellProperties.border.bottom) {
                  // 左边框
                  const [borderWidth, borderStyle] = setBorder(
                    cellProperties.border.bottom.style
                  );
                  let color = "";
                  // console.log(row, column, borderWidth, borderStyle);
                  if (cellProperties.border.bottom.color) {
                    color = getColor(cellProperties.border.bottom.color, themeJson);
                  }
                  td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
                }
            }
        }

        // 在 Handsontable 初始化之前注册渲染器
        Handsontable.renderers.registerRenderer('customStylesRenderer', customRenderer);
          
        // 点击预览
        document.getElementById('btn').addEventListener('click', async function () {
          var hotData = null; // Handsontable 数据
          var file = document.getElementById('file').files[0]; // 获取文件对象
          const workbook = new ExcelJS.Workbook(); // 创建一个工作簿对象
          await workbook.xlsx.load(file); // 加载Excel文件
          const worksheet = workbook.getWorksheet(1); // 获取第一个工作表
          sheet = worksheet; // 将工作表赋值给全局变量

          // 遍历工作表中的所有行(包括空行)
          const sheetData = [];
          worksheet.eachRow({ includeEmpty: true }, function (row, rowNumber) {
              const row_values = row.values.slice(1); // 获取行数据,并排除第一列为null的数据
              const newRowValue = [...row_values];
              newRowValue.forEach(function(item, index) {
                  if(Object.prototype.toString.call(item) === "[object Object]") {
                      if(item.formula) {
                          // 如果是公式,则保留公式,否则将结果作为值
                          newRowValue[index] = item.formula.includes("=") ? item.formula : "=" + item.result;
                      }
                  };
              })
              sheetData.push(newRowValue);
          });
          // 将数据赋值给Handsontable
          hotData = sheetData; 

          // 将主题xml转换成json
          const themeXml = workbook._themes.theme1;
          const options = {
              ignoreAttributes: false,
              attributeNamePrefix: "_",
          };
          const parser = new XMLParser(options);
          const json = parser.parse(themeXml);
          themeJson = json

          // 获取合并的单元格
          const mergeCells = [];
          for (let i in worksheet._merges) {
              const { top, left, bottom, right } = worksheet._merges[i].model;
              mergeCells.push({
                  row: top - 1,
                  col: left - 1,
                  rowspan: bottom - top + 1,
                  colspan: right - left + 1,
              });
          };

          // 定义HyperFormula配置
          const hyperformulaInstance = HyperFormula.buildEmpty({
            licenseKey: 'internal-use-in-handsontable'
          });
          // 将数据加载到HyperFormula
          hot = new Handsontable(document.getElementById('hot'), {
              // 数据
              data: hotData,
              colHeaders: true,
              rowHeaders: true,
              language: 'zh-CN',
              readOnly: true,
              // 公式
              formulas: {
                  engine: hyperformulaInstance,
                  sheetName: "Sheet1",
              },
              width: '100%',
              height: 'calc(100% - 25px)',
              //handsontable的许可证
              licenseKey: 'non-commercial-and-evaluation',
              // 行高
              rowHeights: function (index) {
                  if (sheet.getRow(index + 1).height) {
                      // exceljs获取的行高不是像素值,事实上,它是23px - 13.8 的一个映射。所以需要将它转化为像素值
                      return sheet.getRow(index + 1).height * (23 / 13.8);
                  }
                  return 23; // 默认
              },
              // 列宽
                colWidths: function (index) {
                  if (sheet.getColumn(index + 1).width) {
                  // exceljs获取的列宽不是像素值,事实上,它是81px - 8.22 的一个映射。所以需要将它转化为像素值
                  return sheet.getColumn(index + 1).width * (81 / 8.22);
                }
                return 81; // 默认
              },
              // 自定义单元格样式
              cells: function (row, col, prop) {
                  const cellProperties = {};
                  const cellStyle = sheet.getCell(row + 1, col + 1).style;

                  if (JSON.stringify(cellStyle) !== "{}") {
                      // console.log(row+1, col+1, cellStyle);
                      for (let key in cellStyle) {
                          cellProperties[key] = cellStyle[key];
                      }
                  }
                  return { ...cellProperties, renderer: "customStylesRenderer" };
              },
            // 合并单元格
              mergeCells: mergeCells
            });
            //将handsontable实例渲染到页面上
            hot.render();
          });
          
        // 将handsontable实例导出为excel文件
        document.getElementById('export').addEventListener('click', function () {
          var exportData = Handsontable.helper.createEmptySpreadsheetData(100, 100);
          hot.getData().forEach(function (row, rowIndex) {
              row.forEach(function (cell, colIndex) {
                exportData[rowIndex][colIndex] = cell;
              });
          });
          var workbook = XLSX.utils.book_new();
          var worksheet = XLSX.utils.aoa_to_sheet(exportData);
          XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
          XLSX.writeFile(workbook, 'export.xlsx');
        });
          
        // 根据主题和明暗度themeId获取颜色
        function getThemeColor(themeJson, themeId, tint) {
            let color = "";
            const themeColorScheme = themeJson["a:theme"]["a:themeElements"]["a:clrScheme"];
            switch (themeId) {
              case 0:
                color = themeColorScheme["a:lt1"]["a:sysClr"]["_lastClr"];
                break;
              case 1:
                color = themeColorScheme["a:dk1"]["a:sysClr"]["_lastClr"];
                break;
              case 2:
                color = themeColorScheme["a:lt2"]["a:srgbClr"]["_val"];
                break;
              case 3:
                color = themeColorScheme["a:dk2"]["a:srgbClr"]["_val"];
                break;
              default:
                color = themeColorScheme[`a:accent${themeId - 3}`]["a:srgbClr"]["_val"];
                break;
            }
            // 根据tint修改颜色深浅
            color = "#" + color;
            const colorObj = Color(color);
            if (tint) {
              if (tint > 0) {
                // 淡色
                color = colorObj.lighten(tint).hex();
              } else {
                // 深色
                color = colorObj.darken(Math.abs(tint)).hex();
              }
          }
          return color;
        }
          
        // 获取颜色
        function getColor(obj, themeJson) {
          if ("argb" in obj) {
            // 标准色
            // rgba格式去掉前两位: FFFF0000 -> FF0000
            return "#" + obj.argb.substring(2);
          } else if ("theme" in obj) {
            // 主题颜色
            if ("tint" in obj) {
              return getThemeColor(themeJson, obj.theme, obj.tint);
            } else {
              return getThemeColor(themeJson, obj.theme, null);
            }
          }
        }

        // 设置边框
        function setBorder(style) {
          let borderStyle = "solid";
          let borderWidth = "1px";
          switch (style) {
            case "thin":
              borderWidth = "thin";
              break;
            case "dotted":
              borderStyle = "dotted";
              break;
            case "dashDot":
              borderStyle = "dashed";
              break;
            case "hair":
              borderStyle = "solid";
              break;
            case "dashDotDot":
              borderStyle = "dashed";
              break;
            case "slantDashDot":
              borderStyle = "dashed";
              break;
            case "medium":
              borderWidth = "2px";
              break;
            case "mediumDashed":
              borderStyle = "dashed";
              borderWidth = "2px";
              break;
            case "mediumDashDotDot":
              borderStyle = "dashed";
              borderWidth = "2px";
              break;
            case "mdeiumDashDot":
              borderStyle = "dashed";
              borderWidth = "2px";
              break;
            case "double":
              borderStyle = "double";
              break;
            case "thick":
              borderWidth = "3px";
              break;
            default:
              break;
          }
          // console.log(borderStyle, borderWidth);
          return [borderStyle, borderWidth];
        }
      }
  </script>
</body>

</html>

参考文献:
前端实现(excel)xlsx文件预览

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值