React 可拖拽列宽 + 点击行选中 ProTable 封装笔记

发布时间:2026/6/23 11:39:27
React 可拖拽列宽 + 点击行选中 ProTable 封装笔记 整体思路把功能拆成两部分解耦列宽拖拽核心逻辑独立封装可调整表头组件无业务侵入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.FCResizableTitleProps (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.DispatchReact.SetStateActionTableColumnType[] ) { 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 Recordstring, any, U extends Recordstring, any Recordstring, any, ValueType text ProTablePropsT, U, ValueType { enableRowSelect?: boolean; // 是否开启点击选中 selectedRowKeys?: React.Key[]; // 外部受控选中key onSelectedChange?: (keys: React.Key[], rows: T[]) void; // 选中变化回调 multiple?: boolean; // 是否多选 }; const MyProTableInner T extends Recordstring, any, U extends Recordstring, any Recordstring, any, ValueType text ( props: MyProTablePropsT, U, ValueType, ref: React.ForwardedRefMyProTableRef ) { const { enableRowSelect true, selectedRowKeys, onSelectedChange, multiple false, rowKey id as keyof T, columns [], ...restProps } props; // 内部选中状态非受控模式 const [innerKeys, setInnerKeys] useStateReact.Key[]([]); const finalKeys selectedRowKeys ?? innerKeys; // 列宽拖拽状态 const [renderColumns, setRenderColumns] useStateany[](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 ( ProTableT, 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 Recordstring, any, U extends Recordstring, any Recordstring, any, ValueType text ( props: MyProTablePropsT, U, ValueType { ref?: React.ForwardedRefMyProTableRef } ) React.ReactElement; export default MyProTable;样式补充全局加一行即可选中行高亮样式在全局global.less中添加.table-row-selected { background-color: rgba(22, 119, 255, 0.1) !important; }