本人之前擅长客户端GDI+,疫情期间偶然了解到Web前端的Canvas画布。突发奇想,遂一发不可收拾,于是就有了这么个产物--基于Canvas+React的高性能Table表格,可咸可甜。
语言:JavaScript、TypeScript、ES6语法;
框架:React;
技术:Canvas画布;
性能:以装载并渲染18列、N行为标准,压测有以下性能指标(附,测试硬盘状态不是太好,08年的老盘)。
1 | 如需自适应列宽,30万行(30万,耗时24S) |
2 | 若无需自适应列宽,可达百万级(30万,耗时5S) |
3 | 自适应列宽模式,14500行,耗时1.5S |
4 | 无自适应列宽,14500行,耗时忽略不计 |
特色:高性能、渲染流畅、操控顺滑、高扩展性(自定义派生列、单元格)、装载灵活、ES6语法、全程面向对象(class)、样式全部属性化(颜色、字体、尺寸、高度……抛弃繁杂的CSS样式表,降低入手门槛、维护复杂度);
目前已经支持的功能:
1、序号列、颜色(矩形、圆形)、进度、复选框、复选框集、按钮集、金额线、普通单元格;
2、右键显示隐藏列;
3、拖拽列索引顺序;
4、拖拽列宽;
5、自适应列宽;
6、数据结构(列表结构、树结构);
7、自定义行、单元格背景色、前景色;
8、自定义选中行、单元格的背景色;
9、焦点行背景色;
10、展开、折叠父子行;
11、表格数据排序;
12、表格内数据查找、定位;
13、数据选择模式(单选、多选--Shift辅助、鼠标滑动拖拽);
14、快捷键:↑(向上选择单元格或行)、↓(向下选择单元格或行)、←(向左选择单元格)、→(向右选择单元格)、PageUp(上翻一个显示范围)、PageDown(下翻一个显示范围)、Home(定位首行)、End(定位末行)、Ctrl+C(复制选中内容)、Ctrl+F(表格内数据查找、定位);
15、表格数据打印(目前针对页边距,还有点小瑕疵,需要手工调整一次页边距。大数据打印,尚且存有性能缺陷);
16、表格数据导出(时间紧迫,目前仅支持Excel);
17、多表头(支持表头合并);
18、单元格合并;
19、表头数据筛选、过滤(类似Excel数据筛选);
20、表格内容动态编辑(目前仅支持文本内容编辑,后续补充支持下拉框、日期框、数字框……);
21、表内,行数据拖拽交换(同级交换、父子级交换);
22、表外,行数据拖拽交换(不同表格之间,拖拽移除、或插入、或交换);
23、自定义右键菜单;
24、行定位(定位可视范围外、未渲染区域的选中行,并主动更新滚动条,使目标行处于可视范围内);
25、丰富的事件行为,总有一款适合您。单元格单击事件、行单击事件、行选择改变事件、表格勾选事件(行、列、单元格)、表格失焦事件、右键上下文选项单击事件……;
列表结构:
树结构:
右键菜单:
显示、隐藏列:
列头数据筛选:
案例一:绑定Column、Row、Cell对象
import React, { useEffect, useState } from 'react';
import ColumnCollection from '@/EniacComponents/Table/Collections/ColumnCollection';
import RowCollection from '@/EniacComponents/Table/Collections/RowCollection';
import TableCore, { IFieldData } from '@/EniacComponents/Table/TableCore';
import Table from '@/EniacComponents/Table/Index';
const PageTableBindColumns = () => {
const [columns, setColumns] = useState(new ColumnCollection());
const [rows, setRows] = useState(new RowCollection());
const BuildColumns = () => {
let loColumns = columns;
if (!loColumns || loColumns.Count === 0) {
let loFieldList: IFieldData[] = [
{ Name: 'colSequenceColumn', Text: '', Type: 'SequenceColumn' },
{ Name: 'colCheckBoxColumn', Text: '', Type: 'CheckBoxColumn' },
{ Name: 'name', Text: '姓名', Type: 'Column' },
{ Name: 'age', Text: '年龄', Type: 'Column' },
{ Name: 'sex', Text: '性别', Type: 'Column' },
{ Name: 'items', Text: '功能清单', Type: 'CheckBoxListColumn' },
{ Name: 'address', Text: '地址', Type: 'Column' },
{ Name: 'assets_b', Text: '资产-本币', Type: 'AmountColumn' },
{ Name: 'assets_y', Text: '资产-原币', Type: 'AmountColumn' },
{ Name: 'controls', Text: '操作列', Type: 'ButtonColumn' },
];
loColumns = new ColumnCollection();
for (let liIndex = 0; liIndex < loFieldList.length; liIndex++) {
let loCulumnData = loFieldList[liIndex];
let loColumn = TableCore.CreateColumn(loCulumnData);
loColumns.Add(loColumn);
}
setColumns(loColumns);
}
return loColumns;
};
const BuildRows = (poColumns: ColumnCollection) => {
if (!rows || rows.length === 0) {
let loDataList = [
{
name: '辛弃疾',
age: 37,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '南宋',
assets_b: 56832.215,
assets_y: 56832.215,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '苏洵',
age: 62,
items: '喝酒,true;文臣,true;武将,false',
sex: '男',
address: '北宋',
assets_b: 65912.32,
assets_y: 65912.32,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '苏轼',
age: 46,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '北宋',
assets_b: 92615.68,
assets_y: 92615.68,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '苏辙',
age: 43,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '北宋',
assets_b: 4689.36,
assets_y: 4689.36,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '欧阳修',
age: 78,
items: '喝酒,true;文臣,true;武将,false',
sex: '男',
address: '北宋',
assets_b: 9956592.36,
assets_y: 9956592.36,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '王维',
age: 66,
items: '喝酒,true;文臣,true;武将,false',
sex: '男',
address: '大唐',
assets_b: 6588.66,
assets_y: 6588.66,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '杜甫',
age: 48,
items: '喝酒,true;文臣,true;武将,false',
sex: '男',
address: '大唐',
assets_b: 6523698.99,
assets_y: 6523698.99,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '李白',
age: 63,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '大唐',
assets_b: 98217365.922,
assets_y: 98217365.922,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '李清照',
age: 53,
items: '喝酒,true;文臣,true;武将,true',
sex: '女',
address: '大宋',
assets_b: 16591.52,
assets_y: 16591.52,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '施耐庵',
age: 56,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '大明',
assets_b: 32336.12,
assets_y: 32336.12,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '曹雪芹',
age: 36,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '大清',
assets_b: 615292.77,
assets_y: 615292.77,
controls: '新增;修改;复制;审核;反审核;删除',
},
{
name: '罗贯中',
age: 68,
items: '喝酒,true;文臣,true;武将,true',
sex: '男',
address: '元末明初',
assets_b: 6668214.53,
assets_y: 6668214.53,
controls: '新增;修改;复制;审核;反审核;删除',
},
];
let loRows = new RowCollection();
loDataList.forEach((loRowData: object) => {
let loRow = TableCore.CreateRow(poColumns, loRowData);
if (loRow) {
loRows.Add(loRow);
}
});
setRows(loRows);
}
};
const BuildTableData = () => {
let loColumns = BuildColumns();
BuildRows(loColumns);
};
useEffect(() => {
BuildTableData();
}, [columns, rows]);
return (
<Table
Width={'100%'}
Height={'100%'}
Properties={{
Draggable: true,
ShowCheckBoxColumn: true,
ShowSequenceColumn: true,
ContextMenuVisible: true,
ColumnHeight: 40,
}}
DataSource={null}
Items={{ Columns: columns, Rows: rows }}
></Table>
);
};
export default PageTableBindColumns;
案例二:绑定数据源、事件的使用
import React, { useEffect, useState } from 'react';
import Table from '@/EniacComponents/Table/Index';
import ToolStripView from '../Components/ToolStrip/ToolStripView';
import { IFieldData } from '@/EniacComponents/Table/TableCore';
const PageTableBindFields = () => {
const [fieldList, setFieldList] = useState([] as IFieldData[]);
const [dataList, setDataList] = useState([] as object[]);
const BuildFieldList = () => {
if (!fieldList || fieldList.length === 0) {
let loFieldList: IFieldData[] = [
{ Name: 'name', Text: '姓名', Type: 'Column' },
{ Name: 'age', Text: '年龄', Type: 'Column' },
{ Name: 'items', Text: '功能清单', Type: 'CheckBoxListColumn' },
{ Name: 'sex', Text: '性别', Type: 'Column' },
{ Name: 'address', Text: '地址', Type: 'Column' },
];
setFieldList(loFieldList);
}
};
const BuildDataList = () => {
if (!dataList || dataList.length === 0) {
let loDataList: any[] = [
{ name: '辛弃疾', age: 37, items: '新增;修改;复制;删除;', sex: '男', address: '南宋' },
{ name: '苏洵', age: 62, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏轼', age: 46, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏辙', age: 43, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '欧阳修', age: 78, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '王维', age: 66, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '杜甫', age: 48, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李白', age: 63, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李清照', age: 53, items: '新增;修改;复制;删除;', sex: '女', address: '大宋' },
{ name: '施耐庵', age: 56, items: '新增;修改;复制;删除;', sex: '男', address: '大明' },
{ name: '曹雪芹', age: 36, items: '新增;修改;复制;删除;', sex: '男', address: '大清' },
{ name: '罗贯中', age: 68, items: '新增;修改;复制;删除;', sex: '男', address: '元末明初' },
{ name: '辛弃疾', age: 37, items: '新增;修改;复制;删除;', sex: '男', address: '南宋' },
{ name: '苏洵', age: 62, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏轼', age: 46, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏辙', age: 43, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '欧阳修', age: 78, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '王维', age: 66, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '杜甫', age: 48, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李白', age: 63, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李清照', age: 53, items: '新增;修改;复制;删除;', sex: '女', address: '大宋' },
{ name: '施耐庵', age: 56, items: '新增;修改;复制;删除;', sex: '男', address: '大明' },
{ name: '曹雪芹', age: 36, items: '新增;修改;复制;删除;', sex: '男', address: '大清' },
{ name: '罗贯中', age: 68, items: '新增;修改;复制;删除;', sex: '男', address: '元末明初' },
{ name: '辛弃疾', age: 37, items: '新增;修改;复制;删除;', sex: '男', address: '南宋' },
{ name: '苏洵', age: 62, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏轼', age: 46, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏辙', age: 43, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '欧阳修', age: 78, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '王维', age: 66, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '杜甫', age: 48, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李白', age: 63, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李清照', age: 53, items: '新增;修改;复制;删除;', sex: '女', address: '大宋' },
{ name: '施耐庵', age: 56, items: '新增;修改;复制;删除;', sex: '男', address: '大明' },
{ name: '曹雪芹', age: 36, items: '新增;修改;复制;删除;', sex: '男', address: '大清' },
{ name: '罗贯中', age: 68, items: '新增;修改;复制;删除;', sex: '男', address: '元末明初' },
{ name: '辛弃疾', age: 37, items: '新增;修改;复制;删除;', sex: '男', address: '南宋' },
{ name: '苏洵', age: 62, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏轼', age: 46, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏辙', age: 43, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '欧阳修', age: 78, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '王维', age: 66, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '杜甫', age: 48, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李白', age: 63, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李清照', age: 53, items: '新增;修改;复制;删除;', sex: '女', address: '大宋' },
{ name: '施耐庵', age: 56, items: '新增;修改;复制;删除;', sex: '男', address: '大明' },
{ name: '曹雪芹', age: 36, items: '新增;修改;复制;删除;', sex: '男', address: '大清' },
{ name: '罗贯中', age: 68, items: '新增;修改;复制;删除;', sex: '男', address: '元末明初' },
{ name: '辛弃疾', age: 37, items: '新增;修改;复制;删除;', sex: '男', address: '南宋' },
{ name: '苏洵', age: 62, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏轼', age: 46, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '苏辙', age: 43, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '欧阳修', age: 78, items: '新增;修改;复制;删除;', sex: '男', address: '北宋' },
{ name: '王维', age: 66, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '杜甫', age: 48, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李白', age: 63, items: '新增;修改;复制;删除;', sex: '男', address: '大唐' },
{ name: '李清照', age: 53, items: '新增;修改;复制;删除;', sex: '女', address: '大宋' },
{ name: '施耐庵', age: 56, items: '新增;修改;复制;删除;', sex: '男', address: '大明' },
{ name: '曹雪芹', age: 36, items: '新增;修改;复制;删除;', sex: '男', address: '大清' },
{ name: '罗贯中', age: 68, items: '新增;修改;复制;删除;', sex: '男', address: '元末明初' },
];
setDataList(loDataList);
}
};
useEffect(() => {
BuildFieldList();
BuildDataList();
}, [fieldList, dataList]);
const table_CellClick = (e: any) => {
console.log('table_CellClick', e.Cell.Text);
};
const table_RowClick = (e: any) => {
console.log('table_RowClick', e.Row);
};
const table_SelectionChanged = (e: any) => {
console.log('table_SelectionChanged', e.Row);
};
const table_CheckStateChanged = (e: any) => {
if (e.Cell) {
console.log('勾选单元格', e.Cell);
} else if (e.Row) {
console.log('勾选行', e.Row);
} else if (e.Column) {
console.log('勾选列', e.Column);
}
};
const table_ContextItemClick = (e: any) => {
console.log('table_ContextMenuItemClick', e.Item);
};
const table_GotFocus = (e: any) => {
console.log('table_GotFocus', '获得焦点');
};
const table_LostFocus = (e: any) => {
console.log('table_LostFocus', '失去焦点');
};
return (
<div style={{ width: '100%', height: '100%' }}>
<div>
<ToolStripView ItemStyle={'ImageBeforeText'}></ToolStripView>
</div>
<div style={{ width: '100%', height: 'calc(100% - 43px)' }}>
<Table
Width={'100%'}
Height={'100%'}
Properties={{
AllowDragRow: true,
ShowCheckBoxColumn: true,
ShowSequenceColumn: true,
ContextMenuVisible: true,
CustomContextItems: [
{ Name: 'CustomItem1', Text: '自定义选项1号' },
{ Name: 'CustomItem2', Text: '自定义选项2号' },
],
}}
DataSource={{ FieldList: fieldList, DataList: dataList }}
Items={null}
OnCellClick={table_CellClick}
OnRowClick={table_RowClick}
OnSelectionChanged={table_SelectionChanged}
OnCheckStateChanged={table_CheckStateChanged}
OnContextItemClick={table_ContextItemClick}
OnGotFocus={table_GotFocus}
OnLostFocus={table_LostFocus}
></Table>
</div>
</div>
);
};
export default PageTableBindFields;
本人对GDI+、Canvas相当痴迷,并小有成就。欢迎咨询、洽谈,相互学习、互相进步。