[特殊字符] 使用 Handsontable 构建一个支持 Excel 公式计算的动态表格

在 Web 应用中,处理表格数据并提供 Excel 级的功能(如公式计算、数据导入导出)一直是个挑战。今天,我将带你使用 React + Handsontable 搭建一个强大的 Excel 风格表格,支持 公式计算Excel 文件导入导出 ,并实现 动态单元格样式

🎯 项目目标

  • ✅ 创建一个可编辑的 Excel 风格表格

  • ✅ 支持 Excel 公式解析 (如 =B1+10

  • ✅ 支持 Excel 文件导入/导出(.xlsx/.xls)

  • ✅ 实现 单元格动态渲染(如公式高亮)

📦 依赖安装

首先,确保你的 React 项目已创建(若没有,可用 npx create-react-app my-app 创建)。然后,安装必要的依赖项:

javascript 复制代码
npm install @handsontable/react handsontable mathjs xlsx react-i18next

📌 核心代码解析

1️⃣ 创建 Excel 风格的 Handsontable 表格

javascript 复制代码
import React, { useState } from 'react';
import { HotTable } from '@handsontable/react';
import { useTranslation } from 'react-i18next';
import * as math from 'mathjs';
import * as XLSX from 'xlsx';
import 'handsontable/dist/handsontable.full.min.css';

const ExcelTable = () => {
  const { t } = useTranslation();
  const [data, setData] = useState([
    [t('name'), t('age'), t('city'), 'Total'],
    ['John', 30, 'New York', '=B1+10'],
    ['Alice', 25, 'London', '=B2*2'],
  ]);
  • useState 初始化数据,支持 公式输入 (如 =B1+10

  • Handsontable 是一个轻量级但功能强大的表格库,支持 Excel 风格的操作


2️⃣ 实现公式计算功能

javascript 复制代码
const calculateFormula = (value, row, col, dataArray) => {
  if (typeof value === 'string' && value.startsWith('=')) {
    try {
      const formula = value.slice(1).replace(/[A-Z]\d+/g, (cell) => {
        const colLetter = cell.match(/[A-Z]/)[0];
        const rowNum = parseInt(cell.match(/\d+/)[0], 10) - 1;
        const colNum = colLetter.charCodeAt(0) - 65;
        return dataArray[rowNum][colNum];
      });
      return math.evaluate(formula);
    } catch (e) {
      return '#ERROR';
    }
  }
  return value;
};
  • 解析 Excel 格式的公式=B1+10

  • 将公式转换为 数学计算表达式 并使用 math.evaluate() 计算结果

  • 错误处理 :如果解析失败,返回 #ERROR


3️⃣ 实现 Excel 文件导入功能

javascript 复制代码
const handleImport = (event) => {
  const file = event.target.files[0];
  const reader = new FileReader();
  reader.onload = (e) => {
    const binaryStr = e.target.result;
    const workbook = XLSX.read(binaryStr, { type: 'binary' });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    const importedData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
    setData(importedData);
  };
  reader.readAsBinaryString(file);
};
  • 用户选择 Excel 文件

  • 解析 Excel 数据 并转换为 JavaScript 数组

  • 更新 Handsontable 的 data 以渲染新数据


4️⃣ 实现 Excel 文件导出功能

javascript 复制代码
const handleExport = () => {
  const ws = XLSX.utils.aoa_to_sheet(data);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
  XLSX.writeFile(wb, 'exported_excel.xlsx');
};
  • 将 Handsontable 数据 转换为 Excel sheet

  • 创建新的 Excel 工作簿

  • 下载 Excel 文件 ,实现 导出功能


5️⃣ 动态单元格渲染(公式高亮)

javascript 复制代码
<HotTable
  data={data}
  rowHeaders={true}
  colHeaders={true}
  contextMenu={true}
  stretchH="all"
  beforeChange={(changes) => {
    changes.forEach(([row, col, , newValue]) => {
      data[row][col] = newValue;
    });
    setData([...data]);
  }}
  afterGetCellMeta={(row, col, cellProperties) => {
    const value = data[row][col];
    if (typeof value === 'string' && value.startsWith('=')) {
      cellProperties.readOnly = false;
      cellProperties.renderer = (instance, td, r, c, prop, val) => {
        const result = calculateFormula(val, r, c, data);
        td.innerHTML = result;
        td.style.backgroundColor = '#e0f7fa'; // 公式单元格高亮
      };
    } else {
      cellProperties.renderer = (instance, td, r, c, prop, val) => {
        td.innerHTML = val;
        td.style.backgroundColor = '#ffffff'; // 普通单元格白色背景
      };
    }
  }}
  cells={(row, col) => {
    const cellProperties = {};
    if (row === 0) {
      cellProperties.className = 'header-cell'; // 表头样式
    }
    return cellProperties;
  }}
  licenseKey="non-commercial-and-evaluation"
/>
  • 检测单元格是否包含公式

  • 动态渲染公式计算结果

  • 高亮公式单元格 以增强用户体验


完整代码

javascript 复制代码
// src/components/ExcelTable.jsx
import React, { useState } from 'react';
import { HotTable } from '@handsontable/react';
import { useTranslation } from 'react-i18next';
import * as math from 'mathjs';
import * as XLSX from 'xlsx';
import 'handsontable/dist/handsontable.full.min.css';

const ExcelTable = () => {
  const { t } = useTranslation();
  const [data, setData] = useState([
    [t('name'), t('age'), t('city'), 'Total'],
    ['John', 30, 'New York', '=B1+10'],
    ['Alice', 25, 'London', '=B2*2'],
  ]);

  // 计算公式
  const calculateFormula = (value, row, col, dataArray) => {
    if (typeof value === 'string' && value.startsWith('=')) {
      try {
        const formula = value.slice(1).replace(/[A-Z]\d+/g, (cell) => {
          const colLetter = cell.match(/[A-Z]/)[0];
          const rowNum = parseInt(cell.match(/\d+/)[0], 10) - 1;
          const colNum = colLetter.charCodeAt(0) - 65;
          return dataArray[rowNum][colNum];
        });
        return math.evaluate(formula);
      } catch (e) {
        return '#ERROR';
      }
    }
    return value;
  };

  // 导入 Excel 文件
  const handleImport = (event) => {
    const file = event.target.files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
      const binaryStr = e.target.result;
      const workbook = XLSX.read(binaryStr, { type: 'binary' });
      const sheetName = workbook.SheetNames[0];
      const sheet = workbook.Sheets[sheetName];
      const importedData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
      setData(importedData);
    };
    reader.readAsBinaryString(file);
  };

  // 导出 Excel 文件
  const handleExport = () => {
    const ws = XLSX.utils.aoa_to_sheet(data);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
    XLSX.writeFile(wb, 'exported_excel.xlsx');
  };

  return (
    <div>
      <div style={{ marginBottom: '10px' }}>
        <input type="file" accept=".xlsx, .xls" onChange={handleImport} />
        <button onClick={handleExport}>Export to Excel</button>
      </div>
      <HotTable
        data={data}
        rowHeaders={true}
        colHeaders={true}
        contextMenu={true}
        stretchH="all"
        beforeChange={(changes) => {
          changes.forEach(([row, col, , newValue]) => {
            data[row][col] = newValue;
          });
          setData([...data]);
        }}
        afterGetCellMeta={(row, col, cellProperties) => {
          const value = data[row][col];
          if (typeof value === 'string' && value.startsWith('=')) {
            cellProperties.readOnly = false;
            cellProperties.renderer = (instance, td, r, c, prop, val) => {
              const result = calculateFormula(val, r, c, data);
              td.innerHTML = result;
              td.style.backgroundColor = '#e0f7fa'; // 公式单元格高亮
            };
          } else {
            cellProperties.renderer = (instance, td, r, c, prop, val) => {
              td.innerHTML = val;
              td.style.backgroundColor = '#ffffff'; // 普通单元格白色背景
            };
          }
        }}
        cells={(row, col) => {
          const cellProperties = {};
          if (row === 0) {
            cellProperties.className = 'header-cell'; // 表头样式
          }
          return cellProperties;
        }}
        licenseKey="non-commercial-and-evaluation"
      />
      <style jsx>{
        .header-cell {
          background-color: #f0f0f0;
          font-weight: bold;
        }
      }</style>
    </div>
  );
};

export default ExcelTable; 

🎉 运行效果

🚀 你现在拥有了一个 功能强大的 Excel 风格表格 ,支持:

公式计算 (自动计算 =B1+10

Excel 文件导入/导出

动态高亮公式单元格

行列可编辑 & 右键菜单操作

💡 总结

通过 Handsontable + mathjs + xlsx ,我们轻松构建了一个 Excel 风格的动态表格 。这一方案适用于 财务管理、数据分析、在线表单应用 等场景,提升了数据处理的灵活性!

🔹 完整代码已上传 GitHub (可在评论区留言获取)

🔹 你对这个 Excel 组件有什么优化建议?欢迎评论交流!

🔥 如果觉得有帮助,别忘了点赞 + 收藏! 🎯

相关推荐
tedcloud1237 小时前
UI-TARS-desktop部署教程:构建AI桌面自动化系统
服务器·前端·人工智能·ui·自动化·github
UXbot10 小时前
AI原型设计工具如何支持团队协作与快速迭代
前端·交互·个人开发·ai编程·原型模式
ZC跨境爬虫11 小时前
跟着MDN学HTML_day_48:(Node接口)
前端·javascript·ui·html·音视频
chatexcel12 小时前
AI知识库教程:基于ChatExcel实现规则文档、Excel数据与业务分析联动
人工智能·excel
红尘散仙12 小时前
一套 Rust 核心,跑通 Tauri + React Native
react native·react.js·rust
PieroPc12 小时前
CAMWATCH — 局域网摄像头监控系统 Fastapi + html
前端·python·html·fastapi·监控
Yana.nice13 小时前
Excel中以当前列的数值作为查找条件,查找匹配的行
excel
巴巴博一13 小时前
2026 最新:Trae / Cursor 一键接入 taste-skill 完整教程(让 AI 前端告别“AI 味”)
前端·ai·ai编程
kyriewen13 小时前
半夜三点线上崩了,AI替我背了锅——用AI排错,五分钟定位三年老bug
前端·javascript·ai编程
kyriewen14 小时前
我让 AI 当了 24 小时全年无休的“毒舌考官”
前端·ci/cd·ai编程