一、背景
由于刚到现在就职的公司接手到公司后台的项目代码的时候,发现系统中大量使用了表格,因为代码经多人参与过,导致有些代码块是通过ul>li标签布局有些地方则是通过div布局等等,导致了代码的严重冗余;简单的表格还好,如果是表格中需要对某些字段进行排序的话,又会大大增加代码的量级和可读性,如图
代码为:
从上图部分截图可以看出,一个表格的代码量大概能占到200行代码,而且每个字段的排序的上下箭头都会增加两个state去进行管理,5个字段需要排序则需要增加10个state,也就意味着每次我们在进入路由组件的时候和进行各种刷新重置操作的时候都需要去处理这10个state等等,还有一个问题就是表格的布局,每个字段所占的宽和高度都是class去设置的,也就意味着,每次产品来告诉你,这个表格需要增加一个字段,你也就得重新去计算所有的字段所占宽度和分配适合的宽度。以上所述的问题违背了程序开发的耦合性和复用性,所以决定封装一个Table组件去解决这个问题。
二、分析需要实现Table的API
1、传入一个数组自动构建出表头以及该表头下这一列的显示的内容,命名为columns,数据类型为array
2、传入表格需要显示的数据源数组,命名为dataSource,数据类型为array
3、缺省状态下,表格需要展示的内容,命名为emptyText,数据类型为string
4、传入表格的分页项,包含当前页码,总页码,以及分页器的点击事件,命名为pagination,数据类型为object
5、传入一个表格每行勾选的配置对象,包含是否使用勾选、勾选的点击事件,便于满足对表格勾选项进行批量处理的需求,命名为rowSelection,数据类型为object
6、传入一个表格行的点击事件,便于满足点击当前行进入详情的需求,命名为onRowClick,数据类型为function
7、传入表格的样式对象,给表格内部的外层包裹容器添加行内样式,命名为style,数据类型为object
8、传入一个className给表格内部的外层包裹容器添加className,可以实现在表格组件外部设置表格的每一列的className,数据类型为string
三、具体实现
1、整体代码
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ListNone from '../ListNone/ListNone';
import Pager from '../Pager/Pager';
import './Table.less';
// API
// columns为表格定义数据格式,title字段为表格标题,dataIndex为传入的数据源中需要显示的字段一致,可以通过render函数来渲染当前列的数据 -> Array// dataSource为数据源 -> Array
// rowSelection列表项是否可选 -> Object | null
// pagination为分页器 -> Object | false
// onRowClick为单行点击事件
export default class Table extends Component {
constructor(props) {
super(props);
this.state = {
rowAllSelect: false,//全选按钮
rowCheck: [],//单项勾选框
rowSelId: [], //选中的id
sortArr: [], //排序标志数组
};
}
static propTypes = {
columns: PropTypes.array.isRequired, //表头名称
dataSource: PropTypes.array.isRequired, //数据列表
emptyText: PropTypes.string, //列表为空时表格缺省状态
pagination: PropTypes.object, //表格分页对象,包含当前页码,总共页数,分页器的点击事件,
rowSelection: PropTypes.object, //表格单选全选的配置对象,onChange为单选全选框的状态改变事件,可以得到选中的列的数据 style: PropTypes.object, //表格的样式对象
isLastNoOp: PropTypes.bool //表格最后一行不需要渲染操作样式
}
static defaultProps = {
dataSource: [],
columns: [],
pagination: {},
emptyText: '暂无相关信息'
};
componentWillReceiveProps(nextProps) {
let { dataSource } = nextProps;
let { rowCheck } = this.state;
let sortArr = [];
if (dataSource != this.props.dataSource) {
dataSource.map((item, i) => {
rowCheck[i] = false;
});
this.props.columns.map(item => {
sortArr.push("");
});
const rowSelId = [];
this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
this.setState({
rowAllSelect: false,//全选按钮
rowCheck,//单项勾选框
rowSelId, //选中的id
sortArr
});
}
}
//单选
goodsChange = (item, index, event) => {
let { rowCheck } = this.state;
let status = false;
let { rowSelId } = this.state;
if (event.target.checked) {
rowCheck[index] = true;
rowSelId.push(item);
} else {
rowCheck[index] = false;
for (let i = 0; i < rowSelId.length; i++) {
if (rowSelId[i].id == item.id) {
rowSelId.splice(i, 1);
break;
}
}
// rowSelId.splice($.inArray(item.id,rowSelId),1);
}
for (let i = 0; i < rowCheck.length; i++) {
if (rowCheck[i]) {
status = true;
} else {
status = false;
break;
}
}
if (status) {
let all = [];
for (let i = 0; i < rowCheck.length; i++) {
all.push(true);
}
rowCheck = all;
}
this.setState({
rowCheck,
rowAllSelect: status,
rowSelId
});
this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
}
//全选 goodsAllChange = (event) => {
var all = [];
var rowSelId = [];
const { dataSource } = this.props;
var status = false;
if (event.target.checked) {
for (var i = 0; i < dataSource.length; i++) {
all.push(true);
rowSelId.push(dataSource[i]);
}
status = true;
} else {
for (var i = 0; i < dataSource.length; i++) {
all.push(false);
rowSelId = [];
}
status = false;
}
this.setState({
rowCheck: all,
rowAllSelect: status,
rowSelId
});
this.props.rowSelection && this.props.rowSelection.onChange(rowSelId);
event.stopPropagation();
}
trClick = (data, event) => {
this.props.onRowClick && this.props.onRowClick(data, event);
}
sort = (item, type, index, event) => {
let { sortArr } = this.state;
for (let i = 0; i < sortArr.length; i++) {
sortArr[i] = "";
}
sortArr[index] = type;
this.setState({
sortArr
});
item.sorter(type, item.dataIndex);
}
//列筛选
colFilter = (item,event) =>{
item.colFilter.eventL(event.target.value);
}
//递归columns的children
retColumns = (columns) => {
const { sortArr, rowAllSelect } = this.state;
const { rowSelection, bordered } = this.props;
return (
{this.convertToRows(columns).map((row,j) =>{
return (
{rowSelection && }
{row.map((item, i) => {
return (
{item.title}
{item.sorter &&
}
{item.colFilter &&
}
)
})}
)
})}
)
}
getAllColumns = (columns) => {
const result = [];
columns.forEach((column) => {
if (column.children) {
result.push(column);
result.push.apply(result, this.getAllColumns(column.children));
} else {
result.push(column);
}
});
return result;
}
convertToRows = (originColumns) => {
let maxLevel = 1;
const traverse = (column, parent) => {
if (parent) {
column.level = parent.level + 1;
if (maxLevel < column.level) {
maxLevel = column.level;
}
}
if (column.children) {
let colSpan = 0;
column.children.forEach((subColumn) => {
traverse(subColumn, column);
colSpan += subColumn.colSpan;
});
column.colSpan = colSpan;
} else {
column.colSpan = 1;
}
};
originColumns.forEach((column) => {
column.level = 1;
traverse(column);
});
const rows = [];
for (let i = 0; i < maxLevel; i++) {
rows.push([]);
}
const allColumns = this.getAllColumns(originColumns);
allColumns.forEach((column) => {
if (!column.children) {
column.rowSpan = maxLevel - column.level + 1;
} else {
column.rowSpan = 1;
}
rows[column.level - 1].push(column);
});
return rows;
};
retRows = (columns, data, index, isLastNoOp, length) =>{
return (
this.getAllColumns(columns).map((col, i) => {
if (col.dataIndex !== undefined) {
if (data[col.dataIndex] !== undefined) {
return ({col.render ? col.render(data[col.dataIndex], data, index) : data[col.dataIndex]} )
} else {
return ( )
}
} else {
if (col.children && col.children.length > 0 ) {
this.retRows(col.children, data);
} else {
let renderEle = "";
if (col.render) {
renderEle = col.render(data, index);
if(isLastNoOp && index === length - 1){
renderEle = "";
}
}
return ({col.render && renderEle} );
}
}
})
);
}
render () {
const {
className,
columns,
dataSource,
pagination,
rowSelection,
style,
emptyText,
isLastNoOp
} = this.props;
const { rowCheck } = this.state;
return (
{/* 表头部分 */}
{this.retColumns(columns)}
{
dataSource.map((data, i) => {
return (
{rowSelection && }
{this.retRows(columns, data, i, isLastNoOp, dataSource.length)}
)
})
}
);
}}复制代码
2、表头部分(thead)
//递归columns的children
retColumns = (columns) => {
const { sortArr, rowAllSelect } = this.state;
const { rowSelection, bordered } = this.props;
return (
<thead className={bordered && 'bordered'}>
{this.convertToRows(columns).map((row,j) =>{
return (
<tr key={j}>
{rowSelection && <th><input type="checkbox" className={rowAllSelect ? 'checked' : ''} checked={rowAllSelect} onChange={this.goodsAllChange} />th>}
{row.map((item, i) => {
return (
<th key={i} {...item} className={bordered && 'bordered'}>
<div className={item.sorter?"marginL":""}>{item.title}div>
{item.sorter &&
<div>
<span className={`sort up${sortArr[i] === 'asc' ? ` top` : ``}`} onClick={this.sort.bind(this, item, "asc", i)}>span>
<span className={`sort down${sortArr[i] === 'desc' ? ` bottom` : ``}`} onClick={this.sort.bind(this, item, "desc", i)}>span>
div>
}
{item.colFilter &&
<span className="colFilter">
<select onChange={this.colFilter.bind(this,item)}>
{
item.colFilter.data.map((filterItem,k) =>{
return (<option value={filterItem.val} key={k}>{filterItem.name}option>)
})
}
select>
span>
}
th>
)
})}
tr>
)
})}
thead>
)
}复制代码
注意:convertToRows函数是为了处理多表头的情况,接收我们传入Table组件的columns,通过递归columns中每一项的children字段并判定每一列的colSpan,最终返回一个新的columns进行表头的渲染
3、表体部分(tbody)
retRows = (columns, data, index, isLastNoOp, length) =>{
return (
this.getAllColumns(columns).map((col, i) => {
if (col.dataIndex !== undefined) {
if (data[col.dataIndex] !== undefined) {
return (<td key={i} width={col.width && col.width}>{col.render ? col.render(data[col.dataIndex], data, index) : data[col.dataIndex]}td>)
} else {
return (<td key={i} width={col.width && col.width}>td>)
}
} else {
if (col.children && col.children.length > 0 ) {
this.retRows(col.children, data);
} else {
let renderEle = "";
if (col.render) {
renderEle = col.render(data, index);
if(isLastNoOp && index === length - 1){
renderEle = "";
}
}
return (<td key={i} width={col.width && col.width}>{col.render && renderEle}td>);
}
}
})
);
} 复制代码
4、表格less部分
.table-box{
table{
width: 100%;
thead{
width: 100%;
background-color: #e8e9ed;
tr{
th{
position: relative;
text-align: center;
vertical-align: middle;
padding: 6px;
// input[type=checkbox]{
// width: 25px;
// height: 25px;
// vertical-align: middle;
// }
div:nth-of-type(1){
display: inline-block;
}
div:nth-of-type(2){
cursor: pointer;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: inline-block;
margin-left: 3px;
.sort{
display: block;
width: 0px;
height: 0px;
border-width: 7px;
border-style:solid;
}
.up{
border-color: transparent transparent #868893 transparent;
margin-bottom: 8px;
}
.down{
border-color: #868893 transparent transparent transparent;
}
.top{
border-color: transparent transparent #eb6767 transparent;
}
.bottom{
border-color: #eb6767 transparent transparent transparent;
}
}
.colFilter{
display: inline-block;
margin-left: 10px;
border: 1px solid #d2d6de;
}
}
.bordered{
border: 1px solid #ccc;
}
.marginL{
margin-left: -10px;
}
}
}
tbody{
width: 100%;
background-color: #FFFFFF;
tr{
width: 100%;
border: 1px solid #e4e4e4;
cursor: pointer;
transition: all .3s ease;
td{
text-align: center;
vertical-align: middle;
padding: 6px;
// input[type=checkbox]{
// width: 25px;
// height: 25px;
// vertical-align: middle;
// }
}
}
tr:hover{
background-color: #F9F9F9;
}
}
}}复制代码
四、使用Table组件
以下是在render函数中的代码
以下是传入的columns的代码(关键)
注意:如需添加表头添加子级表头,只需在columns数组中的其中一项添加children字段,children为一个数组