当前位置: 首页 > 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.cnnetsun.cn/news/2991557.html

相关文章:

  • .NET 高级开发 | 设计、实现一个事件总线框架
  • Vscode 使用Copilot拓展接入deepseek v4
  • YC最新判断:下一代大公司,可能不是卖软件的
  • 一个实验搞懂 Docker 和 K8s 怎么配合
  • 基于JAX的函数式时序预测:Chronax库的核心原理与实践指南
  • 3000米浮空智联·200平方公里演训全域虚实透明监测与自愈通信一体化系统
  • 非正式同行评审:动机、实践与平台挑战
  • AI超算一体机选择指南
  • 3步解锁ComfyUI换脸魔法:从新手到专家的AI艺术之旅
  • 3步掌握抖音内容下载:从单视频到批量采集的高效实践
  • VMware Workstation Pro 17 免费激活终极指南:1000+密钥与完整使用教程
  • Windows Cleaner完整指南:3分钟掌握C盘清理终极方案
  • 系统架构设计师-标准化知识体系与标准代号速记指南(终章)
  • IPSec原理与应用课程调研报告
  • 5步搭建个人云游戏平台:Sunshine开源游戏串流服务器完全指南
  • OpenClaw个人智能体工作流搭建实战指南
  • paperxie 毕业论文智能写作:拆解四阶分步创作体系,消解本科硕博全阶段文稿创作焦虑
  • 原来低价礼盒的新疆特产质量竟然有保证?
  • Windows右键菜单大扫除:ContextMenuManager让你的桌面操作告别混乱
  • AI应用开发的生产级能力断层诊断:从RAG到LangChain落地的五大硬门槛
  • 3步解锁Jable视频下载:浏览器插件与本地下载器的完美协作
  • 基于LangChain实现OpenAI Functions风格Tool Calling智能助手
  • Fourtune_ML_CTF_Challenge
  • 【置顶干货】博主介绍,各类系统源码领取途径
  • 凸松弛紧密度分析:割多面体、度量多面体与椭球体的体积比较
  • React Navigation 核心原理与工程实践指南
  • 移动设备远程控制风险剖析与防御实战:从漏洞利用到企业安全管控
  • JavaScript错误处理三界:哪些能catch,哪些必须绕过
  • 听书APP哪个好用?帆书、喜马拉雅、微信读书、番茄畅听适合不同需求
  • Redux在2024:状态契约、RTK Query与现代React分层实践