antdpro-ProTable组件分页自动在底部,默认10条数据,会自动增加滚动条

实现效果

在这里插入图片描述
原来的protable的组件,因为我的ui给我的图查询和表格之间没有间隙
而且跟查询框也有左右布局的,索性我直接把查询组件也抽出来放进去,就是processedQueryFilter的部分,如果这个不用定制的话,直接去掉就行
在这里插入图片描述
我的表格底部也被我重写了一遍,因为ui要求分页左边要添加内容,所以分页的逻辑也夹在了组件框里

代码实现

import React, {
  ReactNode,
  useRef,
  useState,
  useCallback,
  useEffect,
  isValidElement,
  cloneElement,
} from 'react';
import type { ProColumns, ProTableProps, ProFormInstance } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { Pagination, PaginationProps } from 'antd';
import { getTableScroll } from './utils';

export interface TablePageProps<
  T extends Record<string, any> = any,
  U extends Record<string, any> = any,
> extends Omit<ProTableProps<T, U>, 'tableRender'> {

  /**
   * 表格列配置
   */
  columns: ProColumns<T>[];

  /**
   * 数据请求函数
   */
  request: (params: any) => Promise<{
    data: T[];
    success: boolean;
    total: number;
  }>;

  /**
   * 查询组件配置
   */
  queryFilter?: ReactNode;

  /**
   * 表格行 key
   */
  rowKey: string | ((record: T) => string);

  /**
   * 表格类名
   */
  tableClassName?: string;

  /**
   * 表格滚动高度
   */
  scrollY?: string;

  /**
   * 查询过滤器的 ref
   */
  queryFilterRef?: React.RefObject<HTMLDivElement>;

  /**
   * 表格容器的 ref
   */
  countRef?: React.RefObject<HTMLDivElement>;

  paginationLeft?: ReactNode;

  newToolBarRender?: () => ReactNode;
}

/**
 * 表格页面组件
 * 包含 ProTable 和查询过滤器
 */
const TablePage = <T extends Record<string, any> = any, U extends Record<string, any> = any>({
  columns,
  request,
  rowKey,
  tableClassName = 'common-table',
  queryFilter,
  paginationLeft,
  rowSelection,
  newToolBarRender,
  ...restProTableProps
}: TablePageProps<T, U>) => {
  const countRef = useRef<HTMLDivElement>(null);
  const queryFilterRef = useRef<HTMLDivElement>(null);
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
  const rafTimerRef = useRef<number | null>(null);
  // 设置成0,防止页面抖动
  const [scrollY, setScrollY] = useState('0');

  // 查询参数
  const queryParamsRef = useRef({});

  // 计算并更新表格高度的函数,使用防抖优化
  const updateTableHeight = useCallback(() => {
    // 清除之前的 RAF,避免重复计算
    if (rafTimerRef.current) {
      cancelAnimationFrame(rafTimerRef.current);
    }

    rafTimerRef.current = requestAnimationFrame(() => {
      if (!countRef.current) return;

      const height = getTableScroll({
        ref: countRef,
        // extraHeight: tabList?.length ? 88 : 72,
      });
      setScrollY(height);

      // 同步设置 ant-table-body 的高度
      const tableBody = countRef.current.querySelector('.ant-table-body') as HTMLElement;
      if (tableBody) {
        tableBody.style.height = height;
      }
    });
  }, []);

  // 初始化和清理 ResizeObserver
  useEffect(() => {
    // 初始化时计算一次,延迟执行确保 DOM 已渲染
    const initTimer = requestAnimationFrame(() => {
      updateTableHeight();
    });

    // 使用原生 ResizeObserver 监听 queryFilter 的尺寸变化
    // 延迟执行确保 queryFilterRef 已经挂载
    const observerTimer = requestAnimationFrame(() => {
      if (queryFilterRef.current && typeof ResizeObserver !== 'undefined') {
        resizeObserverRef.current = new ResizeObserver(() => {
          // ResizeObserver 的回调已经由浏览器优化,不需要额外防抖
          updateTableHeight();
        });

        resizeObserverRef.current.observe(queryFilterRef.current);
      }
    });

    // 监听窗口大小变化
    const handleWindowResize = () => {
      updateTableHeight();
    };
    window.addEventListener('resize', handleWindowResize);

    // 清理函数
    return () => {
      cancelAnimationFrame(initTimer);
      cancelAnimationFrame(observerTimer);
      if (rafTimerRef.current) {
        cancelAnimationFrame(rafTimerRef.current);
      }
      if (resizeObserverRef.current) {
        resizeObserverRef.current.disconnect();
      }
      window.removeEventListener('resize', handleWindowResize);
    };
  }, [updateTableHeight]);

  // 获取 actionRef 用于控制分页
  const { actionRef } = restProTableProps;
  // 创建 formRef 用于管理查询表单
  const formRef = useRef<ProFormInstance>();

  // 获取默认分页大小
  const defaultPageSize =
    (restProTableProps.pagination &&
      typeof restProTableProps.pagination === 'object' &&
      restProTableProps.pagination.defaultPageSize) ||
    10;

  const [paginationState, setPaginationState] = useState<{
    current: number;
    pageSize: number;
    total: number;
  }>({
    current: 1,
    pageSize: defaultPageSize,
    total: 0,
  });

  // 处理查询和重置逻辑
  const handleQueryFinish = useCallback(
    (values: any) => {
      queryParamsRef.current = values;
      // 重置到第一页
      setPaginationState(prev => ({
        ...prev,
        current: 1,
      }));
      // 重新加载数据,ProTable 会自动将表单值传递给 request
      if (actionRef && 'current' in actionRef && actionRef.current) {
        actionRef.current.reload();
      }
    },
    [actionRef],
  );

  const handleQueryReset = useCallback(() => {
    queryParamsRef.current = {};
    // 重置到第一页
    setPaginationState(prev => ({
      ...prev,
      current: 1,
    }));
    // 重新加载数据,ProTable 会自动将表单值传递给 request
    if (actionRef && 'current' in actionRef && actionRef.current) {
      actionRef.current.reload();
    }
  }, [actionRef]);

  // 处理 queryFilter,自动注入 onFinish 和 onReset
  const processedQueryFilter = React.useMemo(() => {
    if (!queryFilter) return queryFilter;

    // 如果是 QueryFilterWrapper,自动注入查询和重置逻辑
    if (isValidElement(queryFilter)) {
      const props = queryFilter.props as any;
      const elementType = (queryFilter.type as any)?.displayName || (queryFilter.type as any)?.name;
      // 判断是否是 QueryFilterWrapper(通过组件名或 props 特征)
      const isQueryFilterWrapper =
        elementType === 'QueryFilterWrapper' ||
        (props?.leftActions !== undefined &&
          props?.onFinish === undefined &&
          props?.onReset === undefined);

      if (isQueryFilterWrapper) {
        return cloneElement(queryFilter as React.ReactElement<any>, {
          form: formRef.current,
          onFinish: (values: any) => {
            // 先执行用户自定义的 onFinish
            if (props?.onFinish) {
              props.onFinish(values);
            }
            // 再执行默认的查询逻辑
            handleQueryFinish(values);
          },
          onReset: () => {
            // 先执行用户自定义的 onReset
            if (props?.onReset) {
              props.onReset();
            }
            // 再执行默认的重置逻辑
            handleQueryReset();
          },
        });
      }
    }

    return queryFilter;
  }, [queryFilter, handleQueryFinish, handleQueryReset]);

  return (
    <ProTable<T, U>
      {...restProTableProps}
      formRef={formRef}
      columns={columns}
      request={async (params, sort, filter) => {
        const result = await request({ ...params, ...(queryParamsRef.current || {}) });
        // 更新分页状态
        setPaginationState({
          current: params.current || 1,
          pageSize: params.pageSize || paginationState.pageSize,
          total: result.total || 0,
        });
        return result;
      }}
      params={
        {
          current: paginationState.current,
          pageSize: paginationState.pageSize,
        } as unknown as U
      }
      rowKey={rowKey}
      tableClassName={tableClassName}
      search={false}
      scroll={{ x: 1200, y: scrollY }}
      pagination={false}
      tableRender={(props, dom, domlist) => {
        // 从 ProTable 的 props 中获取分页信息(作为备用)
        const { pagination: proPagination } = props;
        // 由于设置了 pagination={false},proPagination 可能是 false,需要类型检查
        const proTotal =
          (proPagination && typeof proPagination === 'object' && proPagination.total) ||
          paginationState.total;
        const proCurrent =
          (proPagination && typeof proPagination === 'object' && proPagination.current) ||
          paginationState.current;
        const proPageSize =
          (proPagination && typeof proPagination === 'object' && proPagination.pageSize) ||
          paginationState.pageSize;

        // 合并分页配置
        const paginationConfig: PaginationProps = {
          current: proCurrent,
          pageSize: proPageSize,
          total: proTotal,
          defaultPageSize: 10,
          size: 'small',
          showSizeChanger: true,
          showQuickJumper: true,
          showTotal: (tot: number) => `${tot}`,
          pageSizeOptions: ['10', '20', '50', '100'],
          ...(restProTableProps.pagination || {}),
        };

        // 处理分页变化
        const handlePaginationChange = (page: number, size?: number) => {
          const newPageSize = size || proPageSize;
          // 更新分页状态
          setPaginationState({
            current: page,
            pageSize: newPageSize,
            total: proTotal,
          });
          // 通过 actionRef 重新加载数据
          if (actionRef && 'current' in actionRef && actionRef.current) {
            actionRef.current.reload();
          }
        };

        return (
          <div className="wrapper" ref={queryFilterRef}>
            <div>{processedQueryFilter}</div>
          {newToolBarRender ? newToolBarRender() : <div>{domlist.toolbar}</div>}
            <div ref={countRef}>{domlist.table}</div>
            <div className="pagination-container">
              <span className="pagination-left">{paginationLeft}</span>
              <Pagination
                {...paginationConfig}
                onChange={handlePaginationChange}
                onShowSizeChange={handlePaginationChange}
              />
            </div>
          </div>
        );
      }}
      rowSelection={
        rowSelection
          ? {
            alwaysShowAlert: false,
            ...(rowSelection || {}),
          }
          : false
      }
    />
  );
};

export default TablePage;

重点是 updateTableHeight方法和resize的坚听

util

import type { RefObject } from 'react';

/**
 * 获取表格滚动高度的参数接口
 */
export interface GetTableScrollParams {

  /**
   * 额外的高度(表格底部的内容高度,默认为74)
   */
  extraHeight?: number;

  /**
   * Table所在的组件的ref
   */
  ref?: RefObject<HTMLElement>;

}

/**
 * 获取第一个表格的可视化高度
 * @param params - 配置参数
 * @param params.extraHeight - 额外的高度(表格底部的内容高度 Number类型,默认为74)
 * @param params.ref - Table所在的组件的ref
 * @returns 计算后的表格高度字符串,格式为 calc(100vh - xxxpx)
 */
export function getTableScroll({ extraHeight, ref }: GetTableScrollParams = {}): string {
  // 默认底部分页56 + 边距16
  const finalExtraHeight = extraHeight ?? 72;

  let tHeader: Element | null = null;
  if (ref?.current) {
    tHeader = ref.current.getElementsByClassName('ant-table-thead')[0];
  } else {
    tHeader = document.getElementsByClassName('ant-table-thead')[0];
  }

  // 表格内容距离顶部的距离
  let tHeaderBottom = 0;
  if (tHeader) {
    tHeaderBottom = Math.ceil(tHeader.getBoundingClientRect().bottom);
  }

  // 窗体高度-表格内容顶部的高度-表格内容底部的高度
  const height = `calc(100vh - ${tHeaderBottom + finalExtraHeight}px)`;

  // 空数据的时候表格高度保持不变,暂无数据提示文本图片居中
  if (ref?.current) {
    const placeholder = ref.current.getElementsByClassName('ant-table-placeholder')[0] as HTMLElement | undefined;
    if (placeholder) {
      placeholder.style.height = height;
    }
  }

  return height;
}

样式代码

.tableContainer {
  width: 100%;
  height: 100%;
  background: #fff;
  border-radius: 4px;
  overflow: hidden;

  .ant-pro-page-container-warp {
    padding: 0 16px;
  }
  .ant-tabs-top > .ant-tabs-nav {
    margin: 0;
  }
  .ant-pro-card .ant-pro-card-body {
    padding-inline: 0;
    padding-block: 0;
  }
  .ant-pro-table-list-toolbar-container {
    padding-block-start: 0;
    padding-inline: 16px;
  }
}

.wrapper {
  :global {
    .ant-pro-card {
      border-top-left-radius: 0;
      border-top-right-radius: 0;
    }
  }
}

.pagination-container {
  display: flex;
  justify-content: space-between;
  padding: 0 16px;
  align-items: center;
}

.pagination-left {
  color: rgba(0, 0, 0, 0.6);
}

使用

  <TablePage<API.Sample.SampleItem, API.Sample.SampleListParams>
        actionRef={actionRef}
        rowKey="id"
        tableClassName="common-table-nopadding"
        options={false}
        columns={columns}
        request={async (params) => {
          const result = await getList({
            sceneName: params.sceneName,
            current: params.current || 1,
            pageSize: params.pageSize || 8,
          });
          return {
            data: result.rows,
            success: true,
            total: result.total,
          };
        }}
        rowSelection={{
          preserveSelectedRowKeys: true,
          onChange: (_, selectedRows) => {
            setSelectedRows(selectedRows);
          },
        }}
        queryFilter={
          <QueryFilterWrapper
            onReset={() => {
              actionRef.current?.reload();
            }}
            onFinish={() => {
              actionRef.current?.reload();
            }}
            oneLine
          >
            <ProFormText
              name="sceneName"
              label="名称"
              placeholder="输入关键词进行搜索"
              fieldProps={{
                allowClear: false,
              }}
            />
          </QueryFilterWrapper>
        }
        paginationLeft={
          <div>{selectedRows.length > 0 ? `已选 ${selectedRows.length} 项信息` : ''}</div>
        }
        newToolBarRender={() => {
          return (
            <div className={styles.toolbar}>
              <Space>
                <Button key="import" type="primary" onClick={handleImport}>
                  导入
                </Button>
              </Space>
              {!!selectedRows.length && (
                <Button
                  key="batchDelete"
                  type="primary"
                  danger
                  icon={<DeleteOutlined />}
                  onClick={handleBatchDelete}
                >
                  批量删除
                </Button>
              )}
            </div>
          );
        }}
      />
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值