实现背景:
客户要求手机端H5页面实现用表格展示预算信息,要求表格固定表头和首列与第二列,原项目使用antd-design-mobile,没有表格组件,故自写于此。
实现功能:
可配置的固定左边几列,或者固定右边几列
具体代码:
目录结构:
normalTable.js代码如下:
import React from 'react';
class NormalTable extends React.Component {
constructor(props) {
super(props);
this.state = {
height: document.documentElement.clientHeight,
};
}
render(){
const {dataSource,columns,width,height,headerFixed,scrollRef,className,scrollClassName}=this.props;
return (
<div className={className?`${className} table-content`:'table-content'}>
<div className={'table-header'} style={width?{width:width}:null}>
<table className={'fixed-table'}>
<thead>
<tr>
{
columns ? columns.map((item, index) => {
return <th key={'columns_'+index+1} width={item.width?item.width:''} className={item.columnClass?item.columnClass:''} style={item.width&&item.fixed?{maxWidth:item.width}:null}>
<span className={'header-column'}>{item.title}</span>
</th>;
}) : null
}
</tr>
</thead>
</table>
</div>
<div className={scrollClassName?scrollClassName+' table-body':'table-body'} style={headerFixed?{width:width,height:(height-41)+'px',overflowY:'auto'}:{width:width}} ref={scrollRef?scrollRef:null}>
<table className={'fixed-table'}>
<tbody>
{
dataSource && columns ? dataSource.map((dataItem, dataIndex) => {
return <React.Fragment key={dataIndex}>
<tr key={'data_'+dataIndex}>
{
columns.map((columnItem, columnIndex) => {
return <td key={columnIndex} width={columnItem.width?columnItem.width:null} className={columnItem.columnClass?columnItem.columnClass:null}>
{
columnItem.render ? columnItem.render(dataItem[columnItem.dataIndex]) :
<span>{dataItem[columnItem.dataIndex]}</span>
}
</td>;
})
}
</tr>
</React.Fragment>;
}) : null
}
</tbody>
</table>
</div>
</div>
)
}
}
export default NormalTable
index.js 代码如下:
import React from 'react';
// import { Flex, List, InputItem, Button, Toast } from 'antd-mobile';
import './index.less';
import NormalTable from './normalTable';
import {syncScroller} from '@/utils/index';
/*从父组件传入数据必须满足:
columns中必须有:
1、title字段,用来显示表头名称
2、dataIndex字段,需要根据此字段来显示当前单元格对应的是哪个字段
3、滚动哪列根据column中传过来的fixed字段判断,如果是'left'则固定在左边,如果是‘right’则滚动到右边,有个缺点是此处固定的列必须是column的前几列否则会挡住后面表格中内容
*/
class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
width: 0,
documentHeight: 0,
leftList: [],
rightList: [],
leftStyle: null,
};
}
componentDidMount() {
const { columns, bodyFixed, tableHeight } = this.props;
let width = 0;
let leftList = [];
let rightList = [];
columns.forEach((item) => {
if (item.width) {
width = width+item.width;
}
if (bodyFixed) {
if (item.fixed === 'left') {
leftList.push(item);
}
if (item.fixed === 'right') {
rightList.push(item);
}
}
});
let documentWidth = document.documentElement.offsetWidth;
if (documentWidth < width) {
this.setState({
width: width,
});
} else {
this.setState({
width: '100%',
});
}
if (bodyFixed) {
this.setState({
leftList: leftList,
rightList: rightList,
},()=>{
if(this.tableMain&&this.leftFixed){
syncScroller([this.leftFixed,this.tableMain],this.changeScroll,tableHeight-41)
}
if(this.tableMain&&this.rightFixed){
syncScroller([this.rightFixed,this.tableMain],this.changeScroll,tableHeight-41)
}
});
}
}
changeScroll=()=>{
this.props.refreshData();
};
render() {
const { width, leftList,rightList } = this.state;
const { headerFixed, className, tableHeight, bodyFixed } = this.props;
return (
<div className={`${className ? className : ''} table-container`}>
{
headerFixed ? <div className={'table-scroll'} style={{ height: tableHeight+'px' }}>
<NormalTable {...this.props} height={tableHeight} width={width} scrollRef={ref => this.tableMain = ref} scrollClassName={'main-scroll'}/>
</div> : <NormalTable {...this.props} className={'normal-table'}/>
}
{
bodyFixed ? <React.Fragment>
{
leftList.length > 0 ? <div className={'left-fixed'} style={this.state.leftStyle}>
<NormalTable {...this.props} columns={leftList} headerFixed={true}
height={tableHeight} from={'left'} scrollRef={ref => this.leftFixed = ref}
className={'left-table'} scrollClassName={'left-scroll'} />
</div> : null
}
{
rightList.length > 0 ? <div className={'right-fixed'}>
<NormalTable {...this.props} columns={rightList} height={tableHeight} headerFixed={true} from={'right'} scrollRef={ref => this.rightFixed = ref}
className={'right-table'}/>
</div> : null
}
</React.Fragment> : null
}
</div>);
}
}
export default Table;
index.less 如下:
:global{
.table-container {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
line-height: 20px;
height: 100%;
position: relative;
.table-scroll {
width: 100%;
height: 100%;
overflow: auto;
overflow-x: hidden;
.table-header {
position: absolute;
top: 0;
z-index: 3;
table {
min-width: 100%;
}
}
.table-body {
position: relative;
top: 42px;
}
}
.left-fixed, .right-fixed {
position: absolute;
top: 0;
z-index: 3;
overflow: hidden;
border-radius: 0;
transition: box-shadow 0.3s ease;
.tableContent, table {
width: auto;
}
}
.left-fixed {
left: 0;
//box-shadow: 6px 0 6px -4px rgba(0, 0, 0, 0.15);
}
.right-fixed {
right: 0;
//box-shadow: -6px 0 6px -4px rgba(0, 0, 0, 0.15);
}
.table-content {
position: relative;
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
.table-body {
transition: opacity 0.3s;
}
}
.normal-table{
overflow-y: auto;
}
table {
width: 100%;
text-align: left;
border-radius: 4px 4px 0 0;
border-collapse: separate;
border-spacing: 0;
background-color: #fff;
thead,
tbody {
> tr {
transition: all 0.3s, height 0s;
}
}
thead > tr > th {
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
text-align: left;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
transition: background 0.3s ease;
}
thead > tr > th, tbody > tr > td {
padding: 10px;
line-height: 20px;
overflow-wrap: break-word;
}
tbody > tr > td {
border-bottom: 1px solid #eee;
white-space: nowrap;
}
thead > tr:first-child > th:first-child {
border-top-left-radius: 4px;
}
thead > tr:first-child > th:last-child {
border-top-right-radius: 4px;
}
thead > tr > th .headerColumn {
display: inline-block;
max-width: 100%;
vertical-align: top;
}
.expended-row {
&:hover {
background: #fbfbfb;
}
td > .table-wrapper {
margin: -10px -10px -11px;
}
}
}
.fixed-table {
table-layout: fixed;
}
.left-table,.right-table {
.table-body::-webkit-scrollbar { /*滚动条整体样式*/
width:0;
height:0
}
.table-body::-webkit-scrollbar-thumb { /*滚动条里面小方块*/
width:0;
height:0
}
.table-body::-webkit-scrollbar-track { /*滚动条里面轨道*/
width:0;
height:0
}
}
}
}
index.js中引用的syncScroller方法,此方法是为了设置两个表格的滚动条滚动一致,代码如下:
//滚动条同步
const syncScroller = function (nodeList,callback,height) {
let nodes = Array.prototype.filter.call(nodeList, item => item instanceof HTMLElement)
let max = nodes.length
if (!max || max === 1) return
let sign = 0; // 用于标注
nodes.forEach((ele, index) => {
ele.addEventListener('scroll', function () { // 给每一个节点绑定 scroll 事件
if (!sign) { // 标注为 0 时 表示滚动起源
sign = max - 1;
let top = this.scrollTop
let left = this.scrollLeft
for (let node of nodes) { // 同步所有除自己以外节点
if (node == this) continue;
node.scrollTo(left, top);
}
if(ele.className.indexOf('main-scroll')>=0){
if (this.scrollHeight-this.scrollTop === height) {
callback();
}
}
} else{
-- sign; // 其他节点滚动时 标注减一
}
});
});
}
调用此组件实例如下:
import React from 'react';
import { NavBar,Toast,Picker,List } from 'antd-mobile';
import { connect } from 'dva';
import router from 'umi/router';
import Loader from '@/components/Loader/Loader';
import Table from '@/components/Table/index';
import { bigPageSize } from '@/utils/config';
import { yearArray,filterMoneyFormat } from '@/utils/index';
import ReactDOM from 'react-dom';
const yearData=yearArray();
class YearBudget extends React.Component {
constructor(props) {
super(props);
this.state = {
year: [new Date().getFullYear()],
height: 0,//表格可滚动区域高度
};
}
goBack = () => {
router.goBack();
};
componentDidMount() {
const hei = document.documentElement.clientHeight-ReactDOM.findDOMNode(this.search).offsetTop-ReactDOM.findDOMNode(this.search).clientHeight-ReactDOM.findDOMNode(this.priceRemark).clientHeight-20;
setTimeout(() => {
this.setState({
height: hei,
});
}, 600);
}
changeDate=(year)=>{
this.setState({
year:year
},()=>{
this.getData(1);
})
}
getData = (pageIndex) => {
const { dispatch, yearBudget } = this.props;
const { userCode } = yearBudget;
dispatch({
type: 'yearBudget/getYearBudget',
payload:{
pageIndex: pageIndex,
pageSize: bigPageSize,
year: this.state.year[0],
userCode: userCode
}
})
};
//滚动到底部后加载下页数据
refreshData = () => {
const { currentPage, totalData } = this.props.yearBudget;
let hasMore = Math.ceil(totalData / bigPageSize);
if (currentPage >= hasMore) {
Toast.info('已经是最后一页')
return false;
} else {
this.getData(currentPage+1);
}
};
render() {
const { yearBudget,loading } = this.props;
const {dataList,budgetData,refreshing}=yearBudget
let loadingStatus = loading.effects['global/getUserInfo']||loading.effects['global/getMenu'] ||loading.effects['yearBudget/getYearBudget'] ;
const columns=[
{
title:"成本中心",
dataIndex: 'budgetDepName',
fixed:'left',//如果为'right'则会在右边固定
width:100,
},
{
title:"预算项目",
dataIndex: 'budgetItemName',
fixed:'left',//设置预算项目列滚动
width:110,
},
{
title:"批复金额",
dataIndex: 'monthBudget',
width:150,
columnClass:'money-text',
render:function(money) {
return <div className={'text-right'}>{filterMoneyFormat(money)}</div>
}
},
{
title:"剩余可用预算",
dataIndex: 'allowUseBudget',
width:150,
columnClass:'money-text',
render:function(money) {
return <div className={'text-right'}>{filterMoneyFormat(money)}</div>
}
},
]
return (
<div className={'page-container'}>
<NavBar
ref={el => this.navBar = el}
mode="light"
icon={<span className={`iconfont icon-houtui back-btn`} onClick={() => this.goBack()}/>}
>年度可用预算余额表</NavBar>
{
loadingStatus ?
<Loader fullScreen={true} size={'large'} animating={loadingStatus}/> : null
}
<div className={'content'} ref={el => this.search = el}>
<List style={{ backgroundColor: 'white' }}>
<Picker
cols={1}
data={yearData}
value={this.state.year}
onChange={this.changeDate}
>
<List.Item arrow="horizontal">年度</List.Item>
</Picker>
</List>
</div>
<div className={'price-remark'} ref={el => this.priceRemark = el}>注:金额单位为万元</div>
<div className={'budget-content'} style={{height:this.state.height}}>
{
this.state.height>0?<Table columns={columns} dataSource={dataList} tableLayout={'fixed'} tableHeight={this.state.height} headerFixed={true} bodyFixed={true} refreshData={this.refreshData}/>:null
}
</div>
</div>
);
}
}
export default connect(({ loading,yearBudget }) => ({
loading,
yearBudget
}))(YearBudget);