react 自写组件实现:固定列与表头的表格

实现背景:
客户要求手机端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);

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值