整体思路
把功能拆成两部分解耦:
- 列宽拖拽核心逻辑:独立封装可调整表头组件,无业务侵入
- ProTable 业务封装:集成列宽拖拽 + 点击行选中 + 选中状态受控/非受控 + 暴露清空选中方法
两个文件配合使用,开箱即用,支持 TypeScript,兼容 ProTable 所有原生属性。
二、列宽拖拽表头封装(ResizableTitle.tsx)
这是列宽拖拽的核心,基于原生 th 实现鼠标按下、移动、抬起的完整拖拽逻辑,最小宽度限制 80px,右侧有拖拽触发区,体验接近 Excel。
import React, { useState, useCallback } from 'react'; // 表格列配置类型 export interface TableColumnType { width?: number; title?: React.ReactNode; dataIndex?: string; key?: string; [key: string]: any; } // 表头组件 interface ResizableTitleProps { width?: number; onResize?: (width: number) => void; [key: string]: any; } const ResizableTitle: React.FC<ResizableTitleProps> = (props) => { const { width, onResize, ...restProps } = props; const [isResizing, setIsResizing] = useState(false); // 鼠标按下开始拖拽 const handleMouseDown = useCallback((e: React.MouseEvent) => { const thRect = e.currentTarget.getBoundingClientRect(); // 只在右侧 10px 区域触发拖拽 const isOnEdge = e.clientX > thRect.right - 10; if (!isOnEdge) return; e.preventDefault(); setIsResizing(true); const startX = e.clientX; const startWidth = thRect.width; // 拖拽中实时更新宽度 const handleMouseMove = (moveEvent: MouseEvent) => { const diff = moveEvent.clientX - startX; const newWidth = Math.max(80, startWidth + diff); onResize?.(newWidth); }; // 松开鼠标结束拖拽 const handleMouseUp = () => { setIsResizing(false); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, [onResize]); return ( <th {...restProps} onMouseDown={handleMouseDown} style={{ width, position: 'relative', paddingRight: '10px', cursor: isResizing ? 'col-resize' : undefined, userSelect: 'none', }} > {/* 拖拽触发区域 */} <span style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '10px', cursor: 'col-resize', backgroundColor: 'transparent', }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'rgba(22, 119, 255, 0.1)'; }} onMouseLeave={(e) => { if (!isResizing) { e.currentTarget.style.backgroundColor = 'transparent'; } }} /> {props.children} </th> ); }; // 注入到 ProTable 表头 export const components = { header: { cell: ResizableTitle, }, }; // 处理列配置,绑定拖拽回调 export const getMergeColumns = ( columns: TableColumnType[], setColumns: React.Dispatch<React.SetStateAction<TableColumnType[]>> ) => { return columns.map((col, index) => ({ ...col, onHeaderCell: (column: TableColumnType) => ({ width: column.width, onResize: (newWidth: number) => { setColumns((prev: TableColumnType[]) => { const next = [...prev]; next[index] = { ...next[index], width: newWidth, }; return next; }); }, }), })); }; export default ResizableTitle;核心要点
- 拖拽只触发在表头右侧 10px 区域,不影响正常点击
- 最小宽度 80px,防止列被缩没
- 鼠标悬浮拖拽区有淡蓝色提示,体验更好
- 对外暴露
components和getMergeColumns供 ProTable 集成
三、ProTable 业务封装(MyProTable.tsx)
在 ProTable 基础上集成:
- 列宽拖拽
- 点击行选中(支持单选/多选)
- 选中状态支持外部受控 / 内部非受控
- 暴露
clearSelected方法清空选中 - 搜索栏按钮顺序调整(查询在前,重置在后)
- 完全兼容 ProTable 原有属性
import { ProTable, type ProTableProps } from '@ant-design/pro-components'; import React, { forwardRef, useImperativeHandle, useState } from 'react'; import { components, getMergeColumns } from '../ResizableTitle'; // 暴露给父组件的方法 export interface MyProTableRef { clearSelected: () => void; } // 扩展 ProTable 属性 export type MyProTableProps< T extends Record<string, any>, U extends Record<string, any> = Record<string, any>, ValueType = 'text' > = ProTableProps<T, U, ValueType> & { enableRowSelect?: boolean; // 是否开启点击选中 selectedRowKeys?: React.Key[]; // 外部受控选中key onSelectedChange?: (keys: React.Key[], rows: T[]) => void; // 选中变化回调 multiple?: boolean; // 是否多选 }; const MyProTableInner = < T extends Record<string, any>, U extends Record<string, any> = Record<string, any>, ValueType = 'text' >( props: MyProTableProps<T, U, ValueType>, ref: React.ForwardedRef<MyProTableRef> ) => { const { enableRowSelect = true, selectedRowKeys, onSelectedChange, multiple = false, rowKey = 'id' as keyof T, columns = [], ...restProps } = props; // 内部选中状态(非受控模式) const [innerKeys, setInnerKeys] = useState<React.Key[]>([]); const finalKeys = selectedRowKeys ?? innerKeys; // 列宽拖拽状态 const [renderColumns, setRenderColumns] = useState<any[]>(columns); const resizeColumns = getMergeColumns(renderColumns, setRenderColumns as any); // 选中变化统一处理 const handleChange = (keys: React.Key[], rows: T[]) => { if (selectedRowKeys === undefined) setInnerKeys(keys); onSelectedChange?.(keys, rows); }; // 获取行唯一 key const getRowKey = (record: T): React.Key => { if (typeof rowKey === 'function') return rowKey(record); return record[rowKey] as React.Key; }; // 点击行触发选中 const handleClick = (record: T) => { if (!enableRowSelect) return; const key = getRowKey(record); let newKeys: React.Key[]; if (multiple) { // 多选:切换当前行选中状态 newKeys = finalKeys.includes(key) ? finalKeys.filter((k) => k !== key) : [...finalKeys, key]; } else { // 单选:只保留当前行或清空 newKeys = finalKeys.includes(key) ? [] : [key]; } // 匹配选中行数据 const selectedRows = newKeys .map((k) => restProps.dataSource?.find((item) => getRowKey(item) === k)) .filter((item): item is T => !!item); handleChange(newKeys, selectedRows); }; // 暴露方法给父组件 useImperativeHandle(ref, () => ({ clearSelected: () => handleChange([], []), })); return ( <ProTable<T, U, ValueType> {...restProps} rowKey={rowKey} columns={resizeColumns as any} components={components} // 注入可拖拽表头 onRow={(record) => ({ ...restProps.onRow?.(record), onClick: () => handleClick(record), // 绑定点击行事件 })} rowClassName={(record, index, indent) => { const key = getRowKey(record); const isSelected = finalKeys.includes(key); let customClass = ''; // 兼容外部传入的 className if (typeof restProps.rowClassName === 'function') { customClass = restProps.rowClassName(record, index, indent); } else if (typeof restProps.rowClassName === 'string') { customClass = restProps.rowClassName; } return isSelected ? `table-row-selected ${customClass}` : customClass; }} // 搜索栏:查询按钮在前,重置按钮在后 search={{ ...restProps.search, optionRender: (_searchConfig, _formProps, dom) => { if (!dom || dom.length < 2) return dom; const [resetBtn, submitBtn] = dom; return [submitBtn, resetBtn]; }, }} /> ); }; // 转发 ref,支持泛型 const MyProTable = forwardRef(MyProTableInner) as < T extends Record<string, any>, U extends Record<string, any> = Record<string, any>, ValueType = 'text' >( props: MyProTableProps<T, U, ValueType> & { ref?: React.ForwardedRef<MyProTableRef> } ) => React.ReactElement; export default MyProTable;样式补充(全局加一行即可)
选中行高亮样式,在全局global.less中添加:
.table-row-selected { background-color: rgba(22, 119, 255, 0.1) !important; }