基于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文件预览

相关推荐
小曲曲7 分钟前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
----云烟----4 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言