npm install handsontable @handsontable/react
import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.css';
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n';
import React, { useEffect, useRef, useState } from 'react';
registerLanguageDictionary(zhCN);
const EditTable: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const [tableData, setTableData] = useState<any>([
[
'厂内',
'产品/应用',
null,
null,
'软件',
null,
null,
'算法',
null,
null,
'应用工程',
null,
null,
'机械',
null,
null,
'研发总计\r\n(人天)',
'应用总计\r\n(人天)',
'产品+算法',
'厂内\r\n总投入',
],
[
null,
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
null,
null,
null,
null,
],
[
'开发阶段',
'1',
'7',
'7',
'2',
'10',
'20',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'27',
'0',
'',
'',
],
[
'厂内调试阶段',
'',
'',
'0',
'2',
'15',
'30',
'',
'',
'0',
'2',
'35',
'70',
'',
'',
'0',
'30',
'70',
'',
'',
],
['合计', '', '', '7', '', '', '50', '', '', '0', '', '', '70', '', '', '0', '57', '70', '', ''],
[
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
' 57,000 ',
' 49,000 ',
'',
'106000',
],
[
'厂外',
'产品',
null,
null,
'软件',
null,
null,
'算法',
null,
null,
'应用工程',
null,
null,
'机械设计',
null,
null,
'研发总计\r\n(人天)',
'应用总计\r\n(人天)',
'产品+算法',
'厂外\r\n总投入',
],
[
null,
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
'人数',
'时间',
'小计\r\n(人天)',
null,
null,
null,
null,
],
[
'SETUP阶段',
'1',
'5',
'5',
'1',
'15',
'15',
'0',
'0',
'0',
'2',
'35',
'70',
'0',
'0',
'0',
'20',
'70',
' ',
'',
],
[
'技术达标阶段',
'1',
'5',
'5',
'1',
'40',
'40',
'0',
'0',
'0',
'1',
'45',
'45',
'0',
'0',
'0',
'45',
'45',
'',
'',
],
[
'验收阶段',
'1',
'0',
'0',
'1',
'7',
'7',
'0',
'0',
'0',
'1',
'30',
'30',
'',
'',
'0',
'7',
'30',
'',
'',
],
[
'合计',
'',
'',
'10',
'',
'',
'62',
'',
'',
'0',
'',
'',
'145',
'',
'',
'0',
'72',
'145',
'',
'',
],
[
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
' 72,000 ',
' 101,500 ',
'0',
'173500',
],
]);
useEffect(() => {
if (containerRef.current) {
let hot = new Handsontable(containerRef.current, {
data: tableData,
rowHeaders: true,
colHeaders: true,
mergeCells: [
{
row: 0,
col: 0,
rowspan: 2,
colspan: 1,
},
{
row: 0,
col: 4,
rowspan: 1,
colspan: 3,
},
{
row: 0,
col: 7,
rowspan: 1,
colspan: 3,
},
{
row: 0,
col: 10,
rowspan: 1,
colspan: 3,
},
{
row: 0,
col: 13,
rowspan: 1,
colspan: 3,
},
{
row: 0,
col: 16,
rowspan: 2,
colspan: 1,
},
{
row: 0,
col: 17,
rowspan: 2,
colspan: 1,
},
{
row: 0,
col: 18,
rowspan: 2,
colspan: 1,
},
{
row: 0,
col: 19,
rowspan: 2,
colspan: 1,
},
{
row: 6,
col: 0,
rowspan: 2,
colspan: 1,
},
{
row: 6,
col: 1,
rowspan: 1,
colspan: 3,
},
{
row: 6,
col: 4,
rowspan: 1,
colspan: 3,
},
{
row: 6,
col: 7,
rowspan: 1,
colspan: 3,
},
{
row: 6,
col: 10,
rowspan: 1,
colspan: 3,
},
{
row: 6,
col: 13,
rowspan: 1,
colspan: 3,
},
{
row: 6,
col: 16,
rowspan: 2,
colspan: 1,
},
{
row: 6,
col: 17,
rowspan: 2,
colspan: 1,
},
{
row: 6,
col: 18,
rowspan: 2,
colspan: 1,
},
{
row: 6,
col: 19,
rowspan: 2,
colspan: 1,
},
{
row: 0,
col: 1,
rowspan: 1,
colspan: 3,
},
],
height: 'auto',
licenseKey: 'non-commercial-and-evaluation', // 非商业用途的免费license
afterChange: (changes, source) => {
if (source === 'edit') {
const updatedData = hot.getData();
console.log('Updated Data:', updatedData);
setTableData(updatedData);
}
},
afterMergeCells: function (mergedCellRange) {
const mergedCells = this.getPlugin('mergeCells').mergedCellsCollection.mergedCells;
console.log('Current Merged Cells:', mergedCells);
},
afterColumnResize: function (newSize, column) {
console.log(`列 ${column} 的新宽度: ${newSize}px`);
// 获取所有列宽
const allColWidths = [];
for (let i = 0; i < this.countCols(); i++) {
allColWidths.push(this.getColWidth(i));
}
console.log('所有列宽:', allColWidths);
},
colWidths: [92, 50, 50, 62, 50, 50, 62, 50, 50, 62, 50, 50, 62, 50, 50, 62, 80, 75, 94, 61],
manualColumnResize: true,
manualRowResize: true,
contextMenu: true,
language: 'zh-CN', // 添加中文语言配置
cells: function (row, col) {
const className = 'htCenter htMiddle'; // 默认样式
// 为特定行设置背景色
if (row === 0 || row === 1 || row === 6 || row === 7) {
return {
className,
renderer: function (instance, td) {
Handsontable.renderers.TextRenderer.apply(this, arguments);
td.style.backgroundColor = '#1890ff'; // 浅蓝色背景
td.style.color = '#fff';
},
};
}
// 开发阶段-小计(人天)计算
if (row === 2 && col === 3) {
return {
className,
type: 'numeric',
readOnly: true, // 禁止编辑
renderer: function (instance, td) {
// 计算前两行对应列的和
const value =
Number(instance.getDataAtCell(2, 1)) * Number(instance.getDataAtCell(2, 2));
// @ts-ignore
td.textContent = value;
td.style.backgroundColor = '#f5f5f5'; // 浅灰色背景
td.style.textAlign = 'right'; // 浅灰色背景
tableData[row][col] = value;
setTableData([...tableData]);
},
};
}
// 厂内调试阶段-小计(人天)计算
if (row === 3 && col === 3) {
return {
className,
type: 'numeric',
readOnly: true, // 禁止编辑
renderer: function (instance, td) {
// 计算前两行对应列的和
const value =
Number(instance.getDataAtCell(3, 1)) * Number(instance.getDataAtCell(3, 2));
// @ts-ignore
td.textContent = value;
td.style.backgroundColor = '#f5f5f5'; // 浅灰色背景
td.style.textAlign = 'right'; // 浅灰色背景
tableData[row][col] = value;
setTableData([...tableData]);
},
};
}
// 合计-小计(人天)计算
if (row === 4 && col === 3) {
return {
className,
type: 'numeric',
readOnly: true, // 禁止编辑
renderer: function (instance, td) {
// 计算前两行对应列的和
const value =
Number(instance.getDataAtCell(2, 3)) + Number(instance.getDataAtCell(3, 3));
// @ts-ignore
td.textContent = value;
td.style.backgroundColor = '#f5f5f5'; // 浅灰色背景
td.style.textAlign = 'right'; // 浅灰色背景
tableData[row][col] = value;
setTableData([...tableData]);
},
};
}
return { className };
},
});
}
}, []);
return <div ref={containerRef} />;
};
export default EditTable;