import { Spin, Empty } from antd; import React, { useMemo } from react; interface VirtualTableProps { columns: any[]; dataSource: any[]; height: any; rowHeight?: number; rowKey?: any; summary?: (data: any[]) React.ReactNode; loading?: boolean; size?: small | middle | large; bordered?: boolean; } const VirtualTable: React.FCVirtualTableProps (props) { const { columns, dataSource, height, rowHeight 46, rowKey, summary, loading, size small, bordered, } props; // 计算总宽度 const totalWidth useMemo(() { return columns.reduce((sum, col) sum (Number(col.width) || 100), 0); }, [columns]); // 预处理单元格合并信息 - 通过 columns 的 onCell 获取 const cellSpans useMemo(() { const spans: any[][] []; dataSource.forEach((record: any, rowIndex: number) { spans[rowIndex] []; columns.forEach((col, colIndex: number) { if (col.onCell) { const cellProps col.onCell(record, rowIndex, dataSource); if (cellProps) { spans[rowIndex][colIndex] { rowSpan: cellProps.rowSpan ?? 1, colSpan: cellProps.colSpan ?? 1, }; } else { spans[rowIndex][colIndex] { rowSpan: 1, colSpan: 1 }; } } else { spans[rowIndex][colIndex] { rowSpan: 1, colSpan: 1 }; } }); }); return spans; }, [dataSource, columns]); const padding size small ? 4px 8px : 8px; // 渲染表头行 const HeaderRow () { let currentLeft 0; let currentRight 0; return ( tr {columns.map((col, index) { const colIsFixedLeft col.fixed left || col.fixed true; const colIsFixedRight col.fixed right; const width Number(col.width) || 100; let leftPos: number | undefined; let rightPos: number | undefined; let zIndex: number | undefined; if (colIsFixedLeft) { leftPos currentLeft; zIndex 20; currentLeft width; } else if (colIsFixedRight) { rightPos currentRight; zIndex 20; currentRight width; } return ( th key{index} className{ant-table-cell ${col.className || }} style{{ position: colIsFixedLeft || colIsFixedRight ? sticky : static, left: leftPos, right: rightPos, zIndex, backgroundColor: #fafafa, width: width, minWidth: width, height: rowHeight, textAlign: col.align || left, padding: padding, fontWeight: 500, borderBottom: 1px solid #f0f0f0, borderRight: bordered ? 1px solid #f0f0f0 : undefined, boxSizing: border-box, color: rgba(0, 0, 0, 0.85), fontSize: 14px, }} {col.title} /th ); })} /tr ); }; return ( div className{ant-table ant-table-middle ${bordered ? ant-table-bordered : }} style{{ position: relative, border: bordered ? 1px solid #f0f0f0 : undefined, borderRadius: 2, background: #fff, fontSize: 14px, width: 100%, }} {loading ( div style{{ position: absolute, top: 0, left: 0, right: 0, bottom: 0, background: rgba(255,255,255,0.8), display: flex, alignItems: center, justifyContent: center, zIndex: 100, }} span classNameant-spin ant-spin-spinning span classNameant-spin-dot ant-spin-dot-spin i / i / i / i / /span /span span style{{ marginLeft: 8 }} Spin/Spin /span /div )} {/* 暂无数据 */} {dataSource.length 0 ( div style{{ position: absolute, top: 100, left: 0, right: 0, bottom: 100, display: flex, alignItems: center, justifyContent: center, zIndex: 99, }} span style{{ marginLeft: 8 }} Empty image{Empty.PRESENTED_IMAGE_SIMPLE} / /span /div )} div classNameant-table-scroll style{{ minHeight: dataSource.length ? 0 : 277, maxHeight: height, overflow: auto, }} div style{{ minWidth: totalWidth }} {/* 表头 */} div classNameant-table-header style{{ position: sticky, top: 0, zIndex: 20, background: #fafafa, }} table classNameant-table-content style{{ width: totalWidth, tableLayout: fixed }} colgroup {columns.map((col, index) ( col key{index} style{{ width: col.width }} / ))} /colgroup thead classNameant-table-thead HeaderRow / /thead /table /div div classNameant-table-body table classNameant-table-content style{{ width: totalWidth, tableLayout: fixed }} colgroup {columns.map((col, index) ( col key{index} style{{ width: col.width }} / ))} /colgroup tbody classNameant-table-tbody {dataSource.map((record, index) { const key typeof rowKey function ? rowKey(record) : record[rowKey || key]; const colPositions columns.map((col, colIndex) { const width Number(col.width) || 100; const colIsFixedLeft col.fixed left || col.fixed true; const colIsFixedRight col.fixed right; let leftPos: number | undefined; let rightPos: number | undefined; let zIndex: number | undefined; if (colIsFixedLeft) { leftPos columns .slice(0, colIndex) .filter((c) c.fixed left || c.fixed true) .reduce((sum, c) sum (Number(c.width) || 100), 0); zIndex 10; } else if (colIsFixedRight) { rightPos columns .slice(colIndex 1) .filter((c) c.fixed right) .reduce((sum, c) sum (Number(c.width) || 100), 0); zIndex 10; } return { width, leftPos, rightPos, zIndex, isFixed: colIsFixedLeft || colIsFixedRight, }; }); const colsToRender: number[] []; let skipUntil -1; for (let colIndex 0; colIndex columns.length; colIndex) { if (colIndex skipUntil) { continue; } const spanInfo cellSpans[index]?.[colIndex]; if (!spanInfo || spanInfo.rowSpan 0) { continue; } if (spanInfo.colSpan 0) { continue; } colsToRender.push(colIndex); if (spanInfo.colSpan 1) { skipUntil colIndex spanInfo.colSpan; } } return ( tr key{key} classNameant-table-row style{{ height: rowHeight }} {colsToRender.map((colIndex) { const col columns[colIndex]; const spanInfo cellSpans[index]?.[colIndex]; const { colSpan, rowSpan } spanInfo; const cellData record[col.dataIndex]; const pos colPositions[colIndex]; const isFixed pos.isFixed; return ( td key{${col.dataIndex || colIndex}-${index}} colSpan{colSpan} rowSpan{rowSpan} className{col.className || } style{{ position: isFixed ? sticky : static, left: pos.leftPos, right: pos.rightPos, zIndex: pos.zIndex, backgroundColor: #fff, width: colSpan 1 ? undefined : pos.width, minWidth: pos.width, textAlign: col.align || left, borderBottom: 1px solid #f0f0f0, borderRight: bordered ? 1px solid #f0f0f0 : undefined, padding: padding, overflow: hidden, textOverflow: ellipsis, whiteSpace: nowrap, boxSizing: border-box, color: rgba(0, 0, 0, 0.85), fontSize: 14px, }} title{ typeof cellData string || typeof cellData number ? String(cellData) : undefined } {col.render ? col.render(cellData, record, index) : cellData ?? -} /td ); })} /tr ); })} /tbody /table /div {summary dataSource.length 0 ( div style{{ position: sticky, bottom: 0, backgroundColor: #fafafa, zIndex: 15, }} table style{{ width: totalWidth, tableLayout: fixed }} colgroup {columns.map((col, index) ( col key{index} style{{ width: col.width }} / ))} /colgroup tbody tr style{{ height: rowHeight }} {(() { const filteredData dataSource.filter( (item) !item?.isGroupSummary !item?.isAeraSummary !item?.isTotalSummary, ); const summaryCells summary(filteredData); const colPositions columns.map((col, colIndex) { const width Number(col.width) || 100; const colIsFixedLeft col.fixed left || col.fixed true; const colIsFixedRight col.fixed right; let leftPos: number | undefined; let rightPos: number | undefined; let zIndex: number | undefined; if (colIsFixedLeft) { leftPos columns .slice(0, colIndex) .filter((c) c.fixed left || c.fixed true) .reduce((sum, c) sum (Number(c.width) || 100), 0); zIndex 10; } else if (colIsFixedRight) { rightPos columns .slice(colIndex 1) .filter((c) c.fixed right) .reduce((sum, c) sum (Number(c.width) || 100), 0); zIndex 10; } return { width, leftPos, rightPos, zIndex, isFixed: colIsFixedLeft || colIsFixedRight, }; }); let colIndexOffset 0; return React.Children.map(summaryCells, (cell: any) { if (cell cell.type td) { const colSpan cell.props.colSpan || 1; const pos colPositions[colIndexOffset] || {}; colIndexOffset colSpan; return React.cloneElement(cell, { style: { ...cell.props.style, position: pos.isFixed ? sticky : static, left: pos.leftPos, right: pos.rightPos, zIndex: pos.isFixed ? 10 : undefined, backgroundColor: white, width: colSpan 1 ? undefined : pos.width, minWidth: pos.width, textAlign: cell.props.style?.textAlign || left, border: 1px solid #f0f0f0, borderRight: bordered ? 1px solid #f0f0f0 : undefined, padding: padding, overflow: hidden, textOverflow: ellipsis, whiteSpace: nowrap, boxSizing: border-box, color: rgba(0, 0, 0, 0.85), fontSize: 14px, }, }); } return cell; }); })()} /tr /tbody /table /div )} /div /div /div ); }; export default VirtualTable;以上是虚拟列表组件使用时ProTableany scroll{{ x: calc(100vh - 304px) }} sticky{{ offsetHeader: 0 }} columns{recordColumns} actionRef{tableActionRef3} sizesmall pagination{false} request{(){{ .... setVirtualTableData(res.data || []); }}} tableViewRender{(props: any, defaultDom) { return ( div VirtualTable key{JSON.stringify(recordColumns.map((col) col.dataIndex))} // 列变化时 key 改变 columns{columns.filter((col) !col.hideInTable)}//过滤搜索项 dataSource{virtualTableData} height{calc(100vh - 300px)} rowHeight{46} rowKeyid_projectId_areaFrom loading{virtualLoading} sizesmall bordered scrollXmax-content / /div ); }} bordered key{info} rowKey{id_projectId_areaFrom} /