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

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

Handsontable官方文档

一、开发前的准备引入相关依赖库
html 复制代码
<!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>
二、编写页面布局
html 复制代码
<body>
  <input type="file" id="file">
  <button id="btn">预览</button>
  <button id="export">导出</button>
</body>
三、编写预览核心代码
javascript 复制代码
<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]"的原因。解决方法就是将带有公式的单元格替换为对应的值,请看以下代码:

javascript 复制代码
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",如下图所示:

解决方法请看以下代码:

javascript 复制代码
// 定义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文件,请看以下代码:
javascript 复制代码
// 将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');
});

此处贴出整体代码

html 复制代码
<!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文件预览

相关推荐
minstbe1 分钟前
AI开发 - 算法基础 递归 的概念和入门(二)汉诺塔问题 递归的应用和使用注意 - Python
开发语言·python·算法
残花月伴10 分钟前
axios
javascript
岁月如歌,青春不败25 分钟前
HMSC联合物种分布模型
开发语言·人工智能·python·深度学习·r语言
言之。32 分钟前
【Java】面试题 并发安全 (1)
java·开发语言
m0_7482345232 分钟前
2025最新版Java面试八股文大全
java·开发语言·面试
chengxuyuan1213_38 分钟前
Python有哪些常用的库
开发语言·python
van叶~39 分钟前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
xiaosannihaiyl241 小时前
Scala语言的函数实现
开发语言·后端·golang
新手小袁_J1 小时前
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigur
java·开发语言·spring·spring cloud·bootstrap·maven·mybatis