news 2026/6/23 11:38:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 11:34:11

.NET 高级开发 | 设计、实现一个事件总线框架

使用事件总线在编写事件总线框架之前&#xff0c;首先了解 Maomi.EventBus 的使用&#xff0c;其示例代码参考 Demo8.Console 项目。创建一个项目&#xff0c;然后通过 nuget 引入 Maomi.EventBus 包。这里我们来模拟用户注册的流程&#xff0c;模拟用户注册流程。假设用户提交…

作者头像 李华
网站建设 2026/6/23 11:23:40

Vscode 使用Copilot拓展接入deepseek v4

1.首先去deepseek开放平台去申请一个API key[注意&#xff1a;申请完成后请立即复制并妥善保存您的key&#xff0c;该信息仅显示一次]2. 在vscode中安装DeepSeek V4 for Copilot Chat 拓展&#xff0c;并启用3. 在 VS Code 中配置 API Key打开命令面板&#xff08;CmdShiftP / …

作者头像 李华
网站建设 2026/6/23 11:21:15

YC最新判断:下一代大公司,可能不是卖软件的

过去几年&#xff0c;AI 创业最常见的方向&#xff0c;是做软件。 比如做一个客服助手、销售助手、律师助手、财务助手&#xff0c;把 AI 接进企业原来的工作流里&#xff0c;让员工效率更高。 但 YC 最近强调的是另一个方向&#xff1a;AI 原生服务公司。 01&#xff5c;什么…

作者头像 李华
网站建设 2026/6/23 11:12:52

一个实验搞懂 Docker 和 K8s 怎么配合

&#x1f5c2;️ 我的项目目录&#xff08;12 个关键文件&#xff09;text/root/message-board/ │ ├── backend/ ← 【Docker 相关】打包后端用的 │ ├── Dockerfile ← 告诉 Docker 怎么打包后端 │ ├── package.jso…

作者头像 李华
网站建设 2026/6/23 11:12:42

基于JAX的函数式时序预测:Chronax库的核心原理与实践指南

1. 项目概述&#xff1a;当函数式编程遇上时序预测如果你正在处理时间序列数据&#xff0c;无论是金融市场的波动、物联网传感器的读数&#xff0c;还是服务器集群的监控指标&#xff0c;你大概率已经体验过传统时序预测库的“甜蜜负担”。它们功能强大&#xff0c;但往往伴随着…

作者头像 李华
网站建设 2026/6/23 11:09:48

3000米浮空智联·200平方公里演训全域虚实透明监测与自愈通信一体化系统

3000米浮空智联200平方公里演训全域虚实透明监测与自愈通信一体化系统技术解析白皮书&#xff08;御衡天地一体化空间智控基座专项版&#xff09;一、方案总述本系统由镜像视界浙江科技有限公司依托企业全栈原生数字孪生、视频孪生底层技术体系独立研制&#xff0c;企业是无感定…

作者头像 李华