前言
在日常开发中,数据表格扮演着至关重要的角色。它以结构化的形式展现信息,使数据清晰易懂,开发者基于此类表格可以对其进行拓展和复用,本篇文章我们将循序渐进地介绍如何构建一个功能完善、易于使用的表格组件,并探讨其背后的设计理念和最佳实践
阅读之前,你可能需要了解以下技术栈:TS + React + Antd ProComponents
是什么?
data-table
(数据表格)是一种常见的用户界面组件,用于展示和操作数据集合,由于后端的数据与前端某些表格深度绑定,所以需要通过此类数据表格实现对应的诉求。
一个完整的数据功能表格通常有以下常用功能
-
数据展示:以表格的形式展示数据集,其中数据通常按行和列组织。
-
动态数据绑定:能够绑定到动态数据源,实时展示数据变化。
-
排序:允许用户根据一列或多列对数据进行排序。
-
筛选:提供筛选器,使用户可以基于特定条件筛选数据。
-
分页:对于大量数据,提供分页功能以便用户可以逐页浏览。
-
交互操作:支持用户对数据进行操作,如编辑、删除、添加等。
-
自定义列定义:允许开发者自定义表格的每一列,包括列的标题、数据索引、宽度、格式化等。
-
响应式设计:能够适应不同的屏幕尺寸和设备。
-
状态管理:跟踪和展示数据的状态,如是否被选中、是否处于活动状态等。
-
工具栏:可能包括额外的工具栏,提供导出、打印、自定义列可见性等高级功能。
为什么我们会需要如此复杂的功能表格?或者说我们如何写好一个表格?
为什么?
参考类似JavaScript中继承的概念,通过定义一个基类(或基组件)来设定通用的属性和方法,然后根据特定需求对其进行扩展,后续对其属性进行拓展
设计数据表格的一些关键因素有以下几点:
-
设计一个基类数据表格,使其包含通用的功能和属性,以便在不同的场景下复用
-
将数据表格分解为多个模块或组件,如表头、表体、分页器等,每个模块负责特定的功能
-
允许通过配置来定义表格的行为和外观,而不是硬编码,增加了灵活性并简化了扩展
-
将通用的属性和方法封装在基类中,如数据加载、行选择、单元格格式化等
-
集成数据验证和错误处理机制,确保数据的准确性和完整性
-
管理好不同版本的数据表格,确保向后兼容,减少升级的复杂性
基于实际情况和表格数据类型,使得我们系统偏向于使用数据表格的输出形式
怎么做?
说了这么多,如何规范实现一个data-table
?
先来看看antd提供的高级表格组件(Antd ProComponents-ProTable:https://pro-components.antdigital.dev/components/table)
ProTable 的诞生是为了解决项目中需要写很多 table 的样板代码的问题,所以在其中做了封装了很多常用的逻辑。这些封装可以简单的分类为预设行为与预设逻辑。
设计方向很简单,基于antd提供的如此便捷的组件,我们可以通过上层传递的参数来达到表格多态效果,提高表格的可用性和灵活性
下面代码中的ProTable是隶属于公司的sz-components库中,这个库包含了antd的ProTable,并对其做了修改
以近期迭代的实际场景为例:
在resource.types中定义了该模块下的ts types类型
将后端文档中的表格结构统一存放在TableData类型中,用于限制和提示相关类型结构
将与枚举相关的类型值存放在Enum中
接着我们创建相关的data-table,来看看代码结构
export const ResourceExerciseDataTable: React.FC<DataTableProps<IResourceExercise.TableData>> = props => {
const { AuthorityMap, ManualActivationMap, ProductSalesMap, StateMap } = IResourceExercise.Enum;
const { hideInSearch, hideInTable, extraColumns, ...rest } = props;
const { isUcc, dataSourceFrom } = useAppVariable();
const { packageExerciseApi } = useAppApi();
const columns = useMemo(() => {
const cols: ProColumns<IResourceExercise.TableData>[] = [
{
dataIndex: 'packageName',
title: '习题包名称',
ellipsis: true,
fixed: 'left',
width: 140,
},
{
dataIndex: 'versionDate',
title: '资源包版本',
hideInSearch: true,
width: 120,
render: (text, record) => {
const { versionDate, versionNum } = record;
return <div>{formatVersion({ versionDate, versionNum }).version}</div>;
},
},
{
dataIndex: 'permission',
title: '可用权限',
valueEnum: AuthorityMap,
width: 80,
},
{
dataIndex: 'needActive',
title: '需手动开通后使用',
valueEnum: ManualActivationMap,
width: 120,
},
{
dataIndex: 'asProduct',
title: '是否作为产品售卖',
valueEnum: ProductSalesMap,
width: 120,
},
{
dataIndex: 'exerciseCount',
title: '总包含题目数',
hideInSearch: true,
width: 120,
},
{
dataIndex: 'state',
title: '状态',
fieldProps: {
mode: 'multiple',
},
valueEnum: StateMap,
width: 80,
},
{
dataIndex: 'createUserName',
title: '版本作者',
hideInSearch: true,
ellipsis: true,
width: 120,
},
{
dataIndex: 'createTime',
title: '创建时间',
ellipsis: true,
valueType: 'dateTime',
searchType: 'dateRange',
width: 120,
},
{
dataIndex: 'firstShelfTime',
title: '首次上架时间',
ellipsis: true,
valueType: 'dateTime',
searchType: 'dateRange',
width: 120,
},
{
dataIndex: 'shelfTime',
title: '最新上架时间',
ellipsis: true,
valueType: 'dateTime',
searchType: 'dateRange',
width: 120,
},
];
return DataTableUtils.resolve(cols, {
hideInTable,
hideInSearch,
extraColumns,
});
}, [isUcc, dataSourceFrom, extraColumns, hideInSearch, hideInTable]);
const itemValueToNum = (list: string[]) => list.map(item => Number(item));
/**
* 获取习题包分页列表
* @param param
* @returns
*/
const handleTableSearch = async (param: any) => {
const data = await packageExerciseApi.getExerciseList(
Object.assign({}, omit(param, 'state', 'createTime', 'firstShelfTime', 'shelfTime', 'permission', 'needActive', 'asProduct'), {
pageNo: param.current,
permissionList: param.permission ? itemValueToNum([param.permission]) : [],
needActiveList: param.needActive ? itemValueToNum([param.needActive]) : [],
asProductList: param.asProduct ? itemValueToNum([param.asProduct]) : [],
stateList: param.state,
...timeCollect(param),
}),
);
return {
data: data.records,
total: data.total,
success: true,
};
};
return (
<ProTable
scroll={{ x: '100%', y: window.innerHeight * 0.6 }}
search={{ labelWidth: 100 }}
{...rest}
queryKey={[IResourceExercise.key, 'table']}
rowKey="id"
columns={columns}
request={rest.dataSource ? undefined : rest.request ?? handleTableSearch}
/>
);
};
首先限制Props类型为DataTableProps<IResourceExercise.TableData>以便上层更好控制表格参数
表头部分使用固定写法是因为:数据展示及数据查询条件与后端接口强关联,如果接口层发生数据或功能模块发生变化,需要调整当前data-table
接着通过DataTableUtils中的表格函数对外层的参数做处理,来看看resolve函数的写法
static resolve<T>(
list: ProColumns<T>[],
options?: {
hideInSearch?: string[];
hideInTable?: string[];
extraColumns?: ProColumns<T>[];
},
): ProColumns<T>[] {
const { hideInSearch, hideInTable, extraColumns } = options ?? {};
let cols = list.concat();
cols.forEach(col => {
if (hideInSearch && hideInSearch.length > 0) {
if (hideInSearch.includes(col.dataIndex as any)) {
col.hideInSearch = true;
}
}
if (hideInTable && hideInTable.length > 0) {
if (hideInTable.includes(col.dataIndex as any)) {
col.hideInTable = true;
}
}
});
if (extraColumns) {
cols = cols.concat(extraColumns);
}
cols.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
return cols.map(item => omit(item, ['index']));
}
hideInSearch, hideInTable, extraColumns分别代表上级对表格搜索栏,表格表头,列数据的控制,其中参数引入了index的概念,通过index来控制在哪一列中插入新的列,打个比方:在data-table中使用index对表格进行排序,思考下面的ProColumns代码
[{
dataIndex: 'packageName',
title: '习题包名称',
ellipsis: true,
fixed: 'left',
width: 140,
index: 1,
},
{
dataIndex: 'versionDate',
title: '资源包版本',
hideInSearch: true,
width: 120,
index: 2,
render: (text, record) => {
const { versionDate, versionNum } = record;
return <div>{formatVersion({ versionDate, versionNum }).version}</div>;
},
},
{
dataIndex: 'permission',
title: '可用权限',
valueEnum: AuthorityMap,
width: 80,
index: 3
}]
如果我想在第二列后面插入一行,只需要通过extraColumns参数传入列的index>2&& <3的数字即可,思考以下代码
{
dataIndex: 'line',
title: '新插入的列',
width: 140,
index: 2.1,
}
以上代码可以在资源包和权限中间插入新的列
说完resolve函数,接下来就是表格自带的request函数,request是表格提供的请求函数,在第一次加载时可以通过该函数将分页,工具栏过滤等参数通过调用该函数将参数格式化,但是一般推荐在search.transform中将搜索值进行格式化
ProTable提供一个params参数,传入该参数可以拓展request函数的参数,方便在外层维护使用
最后需要分享的是queryKey的概念,这个是在公共组件封装的一个用法,其借鉴的是react-query的queryKey的概念,使用该字段可以使表格数据对请求函数(request)深度绑定,在其他地方使用时可以直接使用以下代码实现表格数据刷新的效果
const { refetch } = ProTable.useApi()
refetch([IResourceExercise.key, 'table'])
总结
以上就是文章全部内容了,本文详细介绍了如何构建一个功能完善、易于使用的data-table(数据表格)组件,并探讨了其设计理念和最佳实践。感谢看到最后,如果觉得有所帮助,还望三连支持一下,感谢
相关文章:
https://procomponents.ant.design/components/table
GitHub - TanStack/query: 🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.