尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

React 可拖拽列宽 + 点击行选中 ProTable 封装笔记
📅 发布时间:2026/6/23 11:48:11

整体思路

把功能拆成两部分解耦:

  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,防止列被缩没
  • 鼠标悬浮拖拽区有淡蓝色提示,体验更好
  • 对外暴露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; }

相关新闻

  • 如何设计一个可自我修复与自我迭代的 AI Agent Harness Engineering 系统:核心机制与工程拆解
  • Microsoft Agent Framework - 对 Agent 进AOP(Middleware)编程
  • 深度拆解:从零构建生产级 Multi-Agent 驾驭层(Harness)全景架构

最新新闻

  • 3步搞定!让Windows电脑变身AirPlay 2无线投屏接收器
  • 终极免费Verilog仿真工具:Icarus Verilog完整使用指南
  • 深入解析ANSI C标准库:数学函数与文件I/O的核心原理与实战避坑
  • 3分钟掌握开源Verilog仿真:Icarus Verilog完整实战指南
  • ESP32-C2在Arduino-ESP32中的技术实现与生态整合策略
  • 深入解析NXP LS2088A SEC Job Ring架构与中断处理机制

日新闻

  • Arduino-ESP32项目深度解析:解锁隐藏芯片支持与架构演进
  • 2026年 系统窗厂家/品牌推荐榜单:隔音系统窗+高端系统门窗的核心优势与选购指南 - 品牌发掘
  • NVBench:首个双语非言语发声语音合成评测基准详解与实践

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号