react 滑动分页表格封装

79 阅读2分钟

ScrollTable基本介绍

  • 滑动底部进行分页(用Observer实现),支持render

  • 支持参数:

    • columns:列属性【Array】,每列支持的属性如下:
      {
          hide:false// 是否隐藏该列
          field:'name'// 字段名称
          headerName: '姓名',// 列名
          width:200,// 单元格宽度
          align:'center',	// 文本排列
          renderCell: (params)=><span>{params.value}--{params.row.id}</span> //渲染函数
      }
      
    • rows:数据源【Array】
    • tableHeight:表格体高度【Number】
    • rowHeight:每行高度【Number】
    • onScrollEnd:滑动底部时触发的回调函数【Function】

效果展示

bandicam 2022-08-19 14-59-19-333.gif

源码

index.js
helper.js
style.less

index.js

import PropTypes from 'prop-types';
import { useMemo, createRef, useEffect } from 'react';
import { getHeaderRow, getBodyRows, getColumnsProps } from './helper';
import './style.less';

let observer=null
const ScrollTable = ({ columns, rows, tableHeight = window.innerHeight - 120, rowHeight = 42, onScrollEnd }) => {
   const tbodyRef = createRef();   //获取表格体dom,用于注册观察者
   const lastRowRef = createRef(); //获取表尾隐藏dom,用于注册观察者
   const columnsProps = getColumnsProps(columns); // 获取每个field的属性,用于后面处理单元格样式
   const headRows = getHeaderRow(columns); // 表格头部 jsx element
   const bodyRows = getBodyRows(columns, rows, columnsProps, rowHeight); // 表格体 jsx element
   const bodyHeight = `${tableHeight}px`; // 表格体高度

   // 判断内容高度是否大于容器高度
   const isScroll = useMemo(() => {
      if (rows) {
         const rowCount = rows.length;
         return tableHeight < rowCount * rowHeight;
      }
      return false;
   }, [rows, tableHeight, rowHeight]);

   // 注册观察者
   useEffect(() => {
      const observerCallback = (entries) => {
         const [entry] = entries;
         if (!entry.isIntersecting || !entry) return;
         if (onScrollEnd) {
            onScrollEnd();  // 表格容器与表尾交叉时,及滑动到底部时,执行回调
         }
      };
       
      // 如果出现滚动了就注册观察者
      if (isScroll) {
         if (!observer && lastRowRef && tbodyRef) {
            observer = new IntersectionObserver(observerCallback, { root: tbodyRef.current, threshold: 0.8 });
            observer.observe(lastRowRef.current);
         }
      }
   }, [isScroll, lastRowRef, tbodyRef, onScrollEnd]);
 
   useEffect(()=>()=>{
      observer=null
   },[])

   return (
      <table className="scroll-table">
         <thead>{headRows}</thead>
         {rows && rows.length > 0 ? (
            <tbody ref={tbodyRef} style={{ height: bodyHeight }}>
               {bodyRows}
               <tr ref={lastRowRef} style={{ height: '2px', opacity: 0 }}>
                  <td>end</td>
               </tr>
            </tbody>
         ) : (
            <tbody>
               <tr style={{ height: bodyHeight, display: 'flex', justifyContent: 'center' }}>
                  <td>no rows</td>
               </tr>
            </tbody>
         )}
      </table>
   );
};

ScrollTable.propTypes = {
   columns: PropTypes.arrayOf(
      PropTypes.shape({
         hide:PropTypes.bool,  // 是否隐藏该列
         field: PropTypes.string.isRequired, // 字段
         headerName: PropTypes.string, // 列名称
         width: PropTypes.number, // 列宽
         align: PropTypes.string,// 文本布局(只限于文本类型)
         renderCell: PropTypes.func// 渲染其他组件
      })
   ).isRequired,
   rows: PropTypes.array.isRequired, // 数据源
   tableHeight: PropTypes.number, // 表格高度
   rowHeight: PropTypes.number,// 行高
   onScrollEnd: PropTypes.func// 碰到底部时的回调函数
};

export default ScrollTable;

helper.js

// 获取filed的属性,用于后面单元格样式渲染
export const getColumnsProps = (columns) => {
    const props = {};
    columns.forEach((item) => {
       Reflect.set(props, item.field, { ...item });
    });
    return props;
 };
 
 // 表格头
 export const getHeaderRow = (columns) => {
   // 表格头单元格
   const HeaderCell = (params) => {
      const { field, headerName, width, align } = params;
      const widthStyle = width ? { width } : { flex: 1 };
      return (
         <th key={field} style={{ textAlign: align || 'left', ...widthStyle }}>
            {headerName || field}
         </th>
      );
   };
   
   return  <tr key="header">
      {columns?.map((item) =>!item.hide &&HeaderCell(item)  )}
   </tr>
 }
 

 const getFields = (columns) => columns.filter((item) => !item.hide).map((item) => item.field);
 // 表格体-行
 const bodyRowElement = (...args) => {
    const [fields, row, rowIdx, columnsProps, style] = args;
    const rowKey = `${row.id}-row${rowIdx}`;
    return (
       <tr key={rowKey} style={style}>
          {fields.map((field, fieldIdx) => {
             const cellKey = `${rowKey}-cell${fieldIdx}`;

             const props = columnsProps[field];             
             const { width, renderCell, align } = props;
             const cellWidth = width ? { width } : { flex: 1 };
 
             return (
                <td key={cellKey} style={{ textAlign: align || 'left', ...cellWidth }}>
                   {renderCell ? renderCell({ row: { ...row }, value: row[field] }) : row[field]}
                </td>
             );
          })}
       </tr>
    );
 };
 
//  表格体
 export const getBodyRows = (...agrs) => {
    const [columns, dataSource, columnsProps, rowHeight] = agrs;
    const fields = getFields(columns); // 获取需要遍历的字段
    const style = { height: `${rowHeight}px` };

    // 对于每一行,传入要渲染的字段fields,行数据,行索引,列属性(用于渲染每个单元格),行样式
    return dataSource.map((row, rowIdx) => bodyRowElement(fields, row, rowIdx, columnsProps, style)); 
 };

style.less

.scroll-table {
   display: table;
   border-collapse: collapse;
   border-spacing: 0;
   width: 100%;
   thead {
      min-height: 56px;
      max-height: 56px;
      line-height: 56px;
      display: flex;
      align-items: center;
      th {
         font-weight: 500;
         cursor: pointer;
      }
      margin-right: 6px;
   }
   tbody {
      display: block;
      width: 100%;
      overflow: auto;
      &::-webkit-scrollbar {
         width: 6px;
      }
      &::-webkit-scrollbar-thumb {
         border-radius: 20px;
         background: rgba(0, 0, 0, 0.3);
      }
   }
   tr {
      display: flex;
      align-items: center;
      box-align: center;
      width: 100%;
      border-bottom: 1px solid rgba(224, 224, 224, 1);
      &:last-child td,
      &:last-child th {
         border: 0;
      }
   }
   td,
   th {
      display: 'flex';
      text-align: left;
      padding-left: 10px;
      white-space: nowrap;
      align-items: 'center';
   }
}

使用示例

import React from "react"
import { useState } from 'react';
import ScrollTable from "./scrollTable"

const initRows=[
   {id:1,name:'sds',des:'是否is地方很多事覅滑动i'},
   {id:2,name:'rfs',des:'fdgdgjpo'},
   {id:3,name:'dvv',des:'发的是啥地方和'},
   {id:4,name:'dwgv',des:'房间号山东发货时'},
   {id:5,name:'er2c',des:'范德萨发生了疯狂'},
]


function uuid() {
	var s = [];
	var uuidData = "0123456789abcdefghijklmnopqrstuvwxyz";
	var uuidDataLength = uuidData.length;
	for (var i = 0; i < 36; i++) {
		s[i] = uuidData.substr(Math.floor(Math.random() * uuidDataLength), 1);
	}
	var uuid = s.join("");
	return uuid;
}


export default function App() {
   const [rows,setRows] = useState(initRows)
   const columns=[
      {
         field: 'id',
         headerName: 'id',
         hide:true
      },
      {
         field: 'name',
         headerName: '名称',
      },
      {
         field: 'des',
         headerName: '描述',
      }
   ]
   
   return <ScrollTable
      tableHeight={200}
      columns={columns}
      rows={rows}
      onScrollEnd={()=>{
         setRows((prev)=>[...prev,...[
            {id:uuid(),name:'sds',des:'是否is地方很多事覅滑动i'},
            {id:uuid(),name:'rfs',des:'fdgdgjpo'},
            {id:uuid(),name:'dvv',des:'发的是啥地方和'},
            {id:uuid(),name:'dwgv',des:'房间号山东发货时'},
            {id:uuid(),name:'er2c',des:'范德萨发生了疯狂'},
         ]])
      }}
   />
}