实现效果

原来的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>
);
}}
/>

被折叠的 条评论
为什么被折叠?



