当前位置: 首页 > news >正文

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

整体思路

把功能拆成两部分解耦:

  1. 列宽拖拽核心逻辑:独立封装可调整表头组件,无业务侵入
  2. 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,防止列被缩没
  • 鼠标悬浮拖拽区有淡蓝色提示,体验更好
  • 对外暴露componentsgetMergeColumns供 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; }
http://www.jsqmd.com/news/1067201/

相关文章:

  • 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录
  • Microsoft Agent Framework - 对 Agent 进AOP(Middleware)编程
  • 如何设计一个可自我修复与自我迭代的 AI Agent Harness Engineering 系统:核心机制与工程拆解
  • 【HHO栅格地图路径规划】多策略改进的哈里斯鹰算法MHHO移动机器人栅格地图路径规划【含Matlab源码 15654期】
  • 从“不可能三角”到模块化突围:2026年区块链开发的技术范式转型
  • 深度拆解:从零构建生产级 Multi-Agent 驾驭层(Harness)全景架构
  • 那个写稿的行业,完了
  • aws-waf-token 亚马逊waf盾逆向分析
  • Ubuntu如何卸載LibreOfflice
  • 他40岁,身价5万欧,一夜涨粉500万——这才是世界杯存在的意义
  • Insilico与SK生物制药达成25亿美元AI神经免疫领域合作
  • 环保行业选择 TDengine:环境监测数据的国产时序数据库实践
  • 财务操作日志自动审计与异常告警,智能体保障安全:2026年企业级数智化审计架构深度解析
  • 为什么90%的企业AI项目会失败?7层能力建设架构告诉你答案
  • AI原生上下文学习正在淘汰传统微调——SITS 2026 ICL协议发布后,你的模型还剩多少有效上下文窗口?
  • 多智能体辩论为什么有效?这篇 arXiv 论文给出了“隐藏锚点“的数学证明
  • 福州高端整木定制怎么选?6 家品牌实测对比,避坑必看
  • Redis 8 大数据类型完整实战场景
  • 断尺问题:戴德金分割现实悖论
  • 国产BIM神器!翻模+BIM咨询全流程提速
  • 从大语言模型到具身智能的范式跃迁
  • 怎么去除甲醛又快又好?科学方法+靠谱产品,一步到位
  • 如何高效监控AI配额:Antigravity Cockpit的终极配置指南
  • 大数据专业考公岗位多吗,可报考哪些机关单位
  • 企业AI项目为什么总是失败-七层架构缺失才是根因
  • 二分查找解题
  • 信托制物业缴费模式的数智化落地实践与技术架构
  • 埃拉托斯特尼算法(埃氏筛)【简单】
  • Java 转大模型开发:团队协作中的使用边界
  • 好久不见,甚是想念