企业级的大数据量的 UI 展现,后台管理系统的3个核心场景

  1. Table 表格:支持虚拟滚动、列锁定、树状展开、内置的排序和过滤支持等
  2. Tree 树:动态加载节点数据、支持节点的拖拽、支持节点多选框、可内置于下拉框等等。
  3. Form 表单:表单状态管理、布局系统、丰富的验证机制等功能。可以一站式满足表单的开发需求

让不懂技术的人也能够参与开发,比如产品和运营,设计
ToB端页面,大部分界面以表格进行展示,以承载数据信息为主;结构简单,分类清晰,便于用户浏览
中后台业务表单很多也很重要,沉淀思考方案,把这个议题聊透,汇总出一个 Table的最佳实践。

帮助用户快速了解并简单分析信息的差异性、关联性等
整体界面需要复用度高,拓展性强
表格中各列内容相对独立,可根据业务需求或用户关注点的不同进行自定义调整
使用一种前端框架进行开发,保持视觉和代码一致性

数据量大,承载了核心业务
逻辑交互复杂,质量失控
拖拽生成表单、复杂自定义组件,输出 schema
快速配置简洁 JSON,输出基于 Antd 的表单
抽象出一些业务常见的表单

表格特性

常见的表格功能

  1. 固定表头 Header
    1. sticky 固定左侧列
    2. 固定右侧列
  2. 行选择
    1. 调整样式,如列颜色等
  3. 点击触发操作(比如加载数据)
  4. 导出当前数据
  5. 按列过滤
  6. 搜索
  7. 排序
    1. 表头上下箭头排序
    2. 拖拽行排序,交换行顺序
    3. 拖拽列顺序
  8. 绑定和展示数据(比如 http 查询的结果数据)
  9. 表格编辑
    1. 重命名列
    2. 编辑当前行
  10. 分页

表格技术点

  1. 大数据筛选排序优化
  2. Canvas Grid
    1. canvas实现 Table,性能好
    2. 快速滚动不会白屏,按照帧渲染
  3. DOM,AgGrid
    1. div实现 Table,容易定制开发
    2. 对象池技术
    3. 虚拟DOM,快速滚动会白屏
  4. 多人协同
    1. OT算法 https://github.com/Operational-Transformation/ot.js

table4.x

不支持的组合 expandedRowRendercolumn.fixed
展开行不能喝固定列一起使用
展开行不应该和固定列产生冲突。展开行作为单独一列,也应该独立于滚动之外。
从性能考虑,展开行只有在用户点击展开的时候进行渲染。这就导致了我们无法知道是否渲染方法会返回空元素。
v4 添加 rowExpandable 属性,用于判断是否该行支持展开;展开相关属性全部收入 expandable 属性中。

Table 的背景颜色正式提出了一个 @table-bg 变量
在 v4 中,Table 会跟随该变量设置背景色,不会再出现背景被透出来的情况。
v4 全选当前页”替换成了“选择全部”。并将他们抽取成 Table 的两个静态属性。便于用户选择与组合:
v4 templateAreas 属性简化用户的布局设置:

  1. templateAreas: [
  2. [name, address, address],
  3. [name, building, no]
  4. ]

v4 summary 属性支持。在 Table 中直接渲染一个 tfoot:用作总结栏
表格导出参考
https://github.com/adazzle/react-data-grid

table3.x

在 v3 中,左侧固定列和右侧固定列分别是两个独立的 Table:
position: sticky 实现元素相对于容器的粘附效果
rc-resize-observer ,通过 ResizeObserver 我们获得了更好的性能以及更准确的监听时机。
hideDefaultSelections 的属性。会在勾选框边上添加一个下拉框并提供两个默认功能:

  • 全选当前页面
  • 反选当前页面
  • 单独开启这个属性并不会有效果,它还有一个条件是你得设置 selections 且至少有一个选项

table4.x封装

  1. import React, { memo } from 'react'
  2. import { Table } from 'antd'
  3. function TableList (props) {
  4. const {
  5. className='',
  6. thead,
  7. data,
  8. rowKey = 'id',
  9. type,
  10. loading,
  11. scroll,
  12. selected,
  13. pagination, // 默认带分页
  14. rowChange, // null 没有单选或者复选列
  15. paginationChange, // 分页改变
  16. children
  17. } = props
  18. // rowSelection 选择表格行
  19. const rowSelection = {
  20. type, // 默认 'checkbox'
  21. selectedRowKeys: selected, // 默认选中的
  22. onChange: (rowKey, row) => rowChange({ rowKey, row }),
  23. getCheckboxProps: record => {
  24. return {
  25. defaultChecked: record.id === 1, // 默认选中的
  26. disabled: record.id === 4, // 禁用的
  27. }
  28. },
  29. }
  30. // 分页
  31. function setPagination(data, paginationChange) {
  32. const {
  33. current,
  34. pageSize,
  35. total,
  36. count,
  37. } = data
  38. return {
  39. current,
  40. pageSize,
  41. total,
  42. showQuickJumper: true,
  43. onShowSizeChange(page, limit) {
  44. console.log('show size', page, limit) // 向父级传递事件
  45. paginationChange && paginationChange({ page, limit })
  46. },
  47. onChange(page, limit) {
  48. console.log('page', page, limit)
  49. paginationChange && paginationChange({ page, limit })
  50. },
  51. showTotal() {
  52. return `共有 ${count} 条`
  53. }
  54. }
  55. }
  56. // 设置行属性
  57. function onRow ({record, index}) {
  58. return {
  59. onClick: event => {
  60. // console.log('onrow', record, index)
  61. }, // 点击行
  62. onDoubleClick: event => {
  63. console.log('onrow', record, index)
  64. },
  65. onContextMenu: event => { },
  66. onMouseEnter: event => { }, // 鼠标移入行
  67. onMouseLeave: event => { },
  68. }
  69. }
  70. const attr = {
  71. className,
  72. rowKey,
  73. loading,
  74. scroll,
  75. bordered: true,
  76. columns: thead,
  77. dataSource: tbody,
  78. pagination: pagination && setPagination(data, paginationChange),
  79. rowSelection: type && rowSelection,
  80. }
  81. return (
  82. <section>
  83. <Table
  84. onRow={ (record, index) => onRow({record, index}) }
  85. />
  86. { children }
  87. </section>
  88. )
  89. }
  90. export default TableList

onChange

  1. fucntion App() {
  2. const onChange = (pagination, filters, sorter) => {
  3. // const { current, pageSize } = pagination
  4. console.log(pagination, filters, sorter)
  5. }
  6. return (
  7. <Table
  8. onChange={onChange}
  9. />
  10. )
  11. }

pagination, filters
image.png

pagination

  1. import { injectIntl } from 'react-intl';
  2. @injectIntl
  3. class App extends PureComponent {}
  4. const { intl, onChange} = this.props;
  5. const attr = {
  6. rowKey: 'id',
  7. loading,
  8. dataSource,
  9. columns,
  10. pagination: {
  11. total,
  12. current,
  13. pageSize,
  14. showSizeChanger: true,
  15. showQuickJumper: true,
  16. showTotal: value => intl.formatMessage(locales.total, { value }),
  17. // value 是搜索框的值
  18. onShowSizeChange: (current, pageSize) => onChange({ value, current, pageSize }),
  19. onChange: (current, pageSize) => onChange({ value, current, pageSize }),
  20. },
  21. };

hooks onChange优化

  1. import { FormattedMessage } from 'react-intl';
  2. function TableList() {
  3. const attr = {
  4. rowKey: 'id',
  5. loading,
  6. columns,
  7. dataSource,
  8. onChange: (pagination, filters) => {
  9. const { current, pageSize } = pagination
  10. props.onChange({ value, current, pageSize, filters });
  11. },
  12. pagination: {
  13. total,
  14. current,
  15. pageSize,
  16. showSizeChanger: true,
  17. showQuickJumper: true,
  18. showTotal: value =>
  19. <FormattedMessage id="table.pagination.total" values={{ value }} />,
  20. },
  21. };
  22. return <Table {...attr} />
  23. }
  24. memo的用法
  25. function propsEqual(prevProps, nextProps) {
  26. // true 不会触发 render,false会重新 render
  27. return JSON.stringify(prevProps.value) === JSON.stringify(nextProps.value)
  28. }
  29. export default memo(TableList, propsEqual);