企业级的大数据量的 UI 展现,后台管理系统的3个核心场景
- Table 表格:支持虚拟滚动、列锁定、树状展开、内置的排序和过滤支持等
- Tree 树:动态加载节点数据、支持节点的拖拽、支持节点多选框、可内置于下拉框等等。
- Form 表单:表单状态管理、布局系统、丰富的验证机制等功能。可以一站式满足表单的开发需求
让不懂技术的人也能够参与开发,比如产品和运营,设计
ToB端页面,大部分界面以表格进行展示,以承载数据信息为主;结构简单,分类清晰,便于用户浏览
中后台业务表单很多也很重要,沉淀思考方案,把这个议题聊透,汇总出一个 Table的最佳实践。
帮助用户快速了解并简单分析信息的差异性、关联性等
整体界面需要复用度高,拓展性强
表格中各列内容相对独立,可根据业务需求或用户关注点的不同进行自定义调整
使用一种前端框架进行开发,保持视觉和代码一致性
数据量大,承载了核心业务
逻辑交互复杂,质量失控
拖拽生成表单、复杂自定义组件,输出 schema
快速配置简洁 JSON,输出基于 Antd 的表单
抽象出一些业务常见的表单
表格特性
常见的表格功能
- 固定表头 Header
- sticky 固定左侧列
- 固定右侧列
- 行选择
- 调整样式,如列颜色等
- 点击触发操作(比如加载数据)
- 导出当前数据
- 按列过滤
- 搜索
- 排序
- 表头上下箭头排序
- 拖拽行排序,交换行顺序
- 拖拽列顺序
- 绑定和展示数据(比如 http 查询的结果数据)
- 表格编辑
- 重命名列
- 编辑当前行
- 分页
表格技术点
- 大数据筛选排序优化
- Canvas Grid
- canvas实现 Table,性能好
- 快速滚动不会白屏,按照帧渲染
- DOM,AgGrid
- div实现 Table,容易定制开发
- 对象池技术
- 虚拟DOM,快速滚动会白屏
- 多人协同
table4.x
不支持的组合 expandedRowRender和
column.fixed
展开行不能喝固定列一起使用
展开行不应该和固定列产生冲突。展开行作为单独一列,也应该独立于滚动之外。
从性能考虑,展开行只有在用户点击展开的时候进行渲染。这就导致了我们无法知道是否渲染方法会返回空元素。
v4 添加 rowExpandable
属性,用于判断是否该行支持展开;展开相关属性全部收入 expandable
属性中。
Table 的背景颜色正式提出了一个 @table-bg
变量
在 v4 中,Table 会跟随该变量设置背景色,不会再出现背景被透出来的情况。
v4 全选当前页”替换成了“选择全部”。并将他们抽取成 Table 的两个静态属性。便于用户选择与组合:
v4 templateAreas
属性简化用户的布局设置:
templateAreas: [
[name, address, address],
[name, building, no]
]
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封装
import React, { memo } from 'react'
import { Table } from 'antd'
function TableList (props) {
const {
className='',
thead,
data,
rowKey = 'id',
type,
loading,
scroll,
selected,
pagination, // 默认带分页
rowChange, // null 没有单选或者复选列
paginationChange, // 分页改变
children
} = props
// rowSelection 选择表格行
const rowSelection = {
type, // 默认 'checkbox'
selectedRowKeys: selected, // 默认选中的
onChange: (rowKey, row) => rowChange({ rowKey, row }),
getCheckboxProps: record => {
return {
defaultChecked: record.id === 1, // 默认选中的
disabled: record.id === 4, // 禁用的
}
},
}
// 分页
function setPagination(data, paginationChange) {
const {
current,
pageSize,
total,
count,
} = data
return {
current,
pageSize,
total,
showQuickJumper: true,
onShowSizeChange(page, limit) {
console.log('show size', page, limit) // 向父级传递事件
paginationChange && paginationChange({ page, limit })
},
onChange(page, limit) {
console.log('page', page, limit)
paginationChange && paginationChange({ page, limit })
},
showTotal() {
return `共有 ${count} 条`
}
}
}
// 设置行属性
function onRow ({record, index}) {
return {
onClick: event => {
// console.log('onrow', record, index)
}, // 点击行
onDoubleClick: event => {
console.log('onrow', record, index)
},
onContextMenu: event => { },
onMouseEnter: event => { }, // 鼠标移入行
onMouseLeave: event => { },
}
}
const attr = {
className,
rowKey,
loading,
scroll,
bordered: true,
columns: thead,
dataSource: tbody,
pagination: pagination && setPagination(data, paginationChange),
rowSelection: type && rowSelection,
}
return (
<section>
<Table
onRow={ (record, index) => onRow({record, index}) }
/>
{ children }
</section>
)
}
export default TableList
onChange
fucntion App() {
const onChange = (pagination, filters, sorter) => {
// const { current, pageSize } = pagination
console.log(pagination, filters, sorter)
}
return (
<Table
onChange={onChange}
/>
)
}
pagination, filters
pagination
import { injectIntl } from 'react-intl';
@injectIntl
class App extends PureComponent {}
const { intl, onChange} = this.props;
const attr = {
rowKey: 'id',
loading,
dataSource,
columns,
pagination: {
total,
current,
pageSize,
showSizeChanger: true,
showQuickJumper: true,
showTotal: value => intl.formatMessage(locales.total, { value }),
// value 是搜索框的值
onShowSizeChange: (current, pageSize) => onChange({ value, current, pageSize }),
onChange: (current, pageSize) => onChange({ value, current, pageSize }),
},
};
hooks onChange优化
import { FormattedMessage } from 'react-intl';
function TableList() {
const attr = {
rowKey: 'id',
loading,
columns,
dataSource,
onChange: (pagination, filters) => {
const { current, pageSize } = pagination
props.onChange({ value, current, pageSize, filters });
},
pagination: {
total,
current,
pageSize,
showSizeChanger: true,
showQuickJumper: true,
showTotal: value =>
<FormattedMessage id="table.pagination.total" values={{ value }} />,
},
};
return <Table {...attr} />
}
memo的用法
function propsEqual(prevProps, nextProps) {
// true 不会触发 render,false会重新 render
return JSON.stringify(prevProps.value) === JSON.stringify(nextProps.value)
}
export default memo(TableList, propsEqual);