700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > React(hook)基于antd Table实现可拖拽调整列宽的表格

React(hook)基于antd Table实现可拖拽调整列宽的表格

时间:2020-03-12 13:34:07

相关推荐

React(hook)基于antd Table实现可拖拽调整列宽的表格

React基于antd Table实现可拖拽调整列宽的表格

前言

在日常的工作开发中,会经常的用到表格,进行数据展示,但是数据的长度不一,初始设置的表格列宽不一定符合数据的展示要求,故有时需要手动的进行表格列宽的调整。

用过antd的童鞋,都知道antd的表格并不支持列拖拽功能;那么在react+antd的项目中如何实现可拖动表格呢?

实现功能

1:表格列宽初始自动分配、列宽总和不能超过容器宽度(无横向滚动条,公司项目特殊需求)

2:当容器宽度变化时,保持当前列宽的分配比例,等比缩小

3:拖动过程中不进行列宽的调整,只有释放之后再进行列宽调整

代码实现

拖拽功能实现基础:react-resizable

目录结构: useTableCol.tsx: 处理表格列的宽度计算等相关逻辑

import { useMemoizedFn, useSafeState } from 'ahooks';import type { ColumnType } from 'antd/es/table/interface';import { useEffect, useRef, useCallback } from 'react';const useTableCol = (wrapperWidth: number | undefined, columns: ColumnType<any>[]) => {const [isInit, setInit] = useSafeState<boolean>(false);// 保存每一列宽度的百分比,用来当容器的宽度变化时,计算每列的宽度const titleWidthMapRef = useRef<{ titleWidthMap: Record<string, number> | undefined }>({ titleWidthMap: undefined });// 每一列的宽度转换成数字之后的列配置const [tableShowColumns, setTableShowColumns] = useSafeState<ColumnType<any>[]>([]);// 初始时,将传入的表格配置数据进行转换// 将百分比、字符串格式的宽度配置转换成对应的数字宽度// 并根据容器的宽度做自适应const getTableNumberWidthCol = useMemoizedFn(() => {let resultTableColumesList: ColumnType<any>[] = [];if (wrapperWidth && columns) {// TODO: 筛选出所有显示的列const showCols = columns.filter((col) => col);const newColumesList = showCols.map((col) => {const { width } = col;const newCol = { ...col };// 当配置了width属性,且width为字符串类型时,计算具体的宽度值if (width && typeof width === 'string') {newCol.width = width.endsWith('%') ? (wrapperWidth * parseFloat(width)) / 100 : parseFloat(width);}return newCol;});// 表格总的宽度const totalWidth = newColumesList.filter((item) => typeof item.width === 'number').reduce((sum, current) => sum + Number(current.width), 0);// 查找出未配置宽度的列const noWidthColumes = newColumesList.filter((col) => !col.width);// 如果存在未配置宽度的列,则将容器未分配的宽度,等分给未分配宽度的列if (noWidthColumes.length > 0) {const otherWidth = wrapperWidth - totalWidth;if (otherWidth > 0) {// 为了简单,向下取整,并将差值放到最后一列const commonWidth = Math.floor(otherWidth / noWidthColumes.length);const resultColumes = newColumesList.map((col) => {if (!col.width) {// 最后一个未配置宽度的列宽取差值if (col.title === noWidthColumes[noWidthColumes.length - 1].title) {col.width = otherWidth - commonWidth * (noWidthColumes.length - 1);} else {// 非最后一个未配置宽度的列,则取均值的向下取整值col.width = commonWidth;}}return col;});resultTableColumesList = resultColumes;} else {// 存在未分配宽度的列,但是列的已分配宽度大于容器宽度,此处正常情况下不应出现// 若出现了此情况,则给无列宽的列都分配60px的宽度,其他有列宽的需要同等缩小const needWidth = 60 * noWidthColumes.length + Math.abs(otherWidth);const showColWithWidth = newColumesList.length - noWidthColumes.length;if (showColWithWidth > 0) {const averageWidth = Math.floor(needWidth / showColWithWidth);const lastWidth = needWidth - averageWidth * (showColWithWidth - 1);const resultColumes = newColumesList.map((col) => {if (!col.width) {// 最后一个未配置宽度的列宽取差值if (col.title === noWidthColumes[noWidthColumes.length - 1].title) {col.width = lastWidth;} else {// 非最后一个未配置宽度的列,则取均值的向下取整值col.width = averageWidth;}}return col;});resultTableColumesList = resultColumes;}}} else {const otherWidth = totalWidth - wrapperWidth;const averageWidth = Math.floor(otherWidth / newColumesList.length);const lastWidth = otherWidth - averageWidth * (newColumesList.length - 1);const resultColumes = newColumesList.map((col, index) => {if (index !== newColumesList.length - 1) {return { ...col, width: Number(col.width) - averageWidth };}return { ...col, width: Number(col.width) - lastWidth };});resultTableColumesList = resultColumes;}}return resultTableColumesList;});// 更新列宽占容器百分比的方法,若表格列支持拖拽,则需提供给拖拽方法,每次拖拽结束后,更新值const updateTitleWidthMap = useCallback((result: Record<string, number>) => {titleWidthMapRef.current.titleWidthMap = result;},[titleWidthMapRef],);// 将数字列宽所占百分比保存下来,用以当容器的宽度变更时,做自适应处理const setTitleWidthMapMethod = useMemoizedFn((colList: ColumnType<any>[], allWidth?: number) => {if (allWidth) {const result: Record<string, number> = {};colList.forEach(({ width }, index) => {result[`_${index}`] = parseFloat(((width as number) / allWidth).toFixed(2));});updateTitleWidthMap(result);}});// 此useEffect为第一次执行表格渲染时,生成对应的列配置useEffect(() => {// 初始化时,根据配置项,设置表格列的宽度,并记录对应百分比if (wrapperWidth && !isInit) {const resultTableCol = getTableNumberWidthCol();setTitleWidthMapMethod(resultTableCol, wrapperWidth);setTableShowColumns(resultTableCol);setInit(true);}}, [isInit,wrapperWidth,tableShowColumns,setInit,setTableShowColumns,getTableNumberWidthCol,setTitleWidthMapMethod,]);// 当容器宽度变化时,根据每列所占的比例,重新结算列宽useEffect(() => {if (wrapperWidth && isInit) {setTableShowColumns((oldColumns) => {const result: ColumnType<any>[] = [];const titleWidthMap = titleWidthMapRef?.current?.titleWidthMap;oldColumns.forEach((col, index) => {const pervent = titleWidthMap?.[`_${index}`];result.push({...col,width: wrapperWidth * pervent!,});});const totalWidth = result.reduce((sum, cur) => sum + parseFloat(`${cur.width!}`), 0);result[result.length - 1].width = wrapperWidth + parseFloat(`${result[result.length - 1].width!}`) - totalWidth;return result;});}}, [isInit, wrapperWidth, titleWidthMapRef, setTableShowColumns]);return {tableShowColumns,isInit,setTitleWidthMapMethod,} as const;};export default useTableCol;

useResizeTableCol.tsx:将表格的列转成可拖拽列,增加相关方法

import { useMemoizedFn, useSafeState } from 'ahooks';import type { ColumnType } from 'antd/lib/table';import { useState, useEffect } from 'react';import useTableCol from './useTableCol';const useResizeTableCol = (wrapperWidth: number | undefined, tableRef: any, columns: ColumnType<any>[]) => {const [colIsInit, setColInit] = useSafeState<boolean>(false);const [tableColumns, setTableColumns] = useState<ColumnType<any>[]>(columns);const { tableShowColumns, isInit, setTitleWidthMapMethod } = useTableCol(wrapperWidth, columns);const handleResize = useMemoizedFn((index: number) => (e: any, { size }: any) => {e.stopImmediatePropagation();if (tableRef.current) {const widthList = [...(tableRef.current as HTMLElement).querySelectorAll('.ant-table-thead th.react-resizable'),].map((th) => {return (th as HTMLElement).getBoundingClientRect().width;});setTableColumns((col) => {const nextColumns = [...col];const { width: oldWidth } = nextColumns[index];// 此次平移的宽度const offsetWidth = size.width - Number(oldWidth || 0);// 当前列得宽度const currentWidth = widthList[index] + offsetWidth;const nextWidth = widthList[index + 1] - offsetWidth;// 左移,当前宽度小于42if (currentWidth < 42) {widthList[index] = 42;widthList[index + 1] = nextWidth - 42 + currentWidth;} else if (nextWidth < 42) {// 右移,下一列得宽度小于42widthList[index] = currentWidth - 42 + nextWidth;widthList[index + 1] = 42;} else {widthList[index] = currentWidth;widthList[index + 1] = nextWidth;}console.log(widthList);const resultColumns = nextColumns.map((nextCol, _index) => ({...nextCol,width: widthList[_index],onHeaderCell:_index !== nextColumns.length - 1? () => ({width: widthList[_index],onResize: handleResize(_index),}): undefined,}));setTitleWidthMapMethod(resultColumns, wrapperWidth);return resultColumns;});}});useEffect(() => {if (isInit) {setTableColumns(tableShowColumns.map((col, index) => ({...col,onHeaderCell:index !== tableShowColumns.length - 1? () => ({width: col.width,onResize: handleResize(index),}): undefined,})),);setColInit(true);}}, [tableShowColumns, isInit, setTableColumns, handleResize, setColInit]);return {colIsInit,tableColumns,} as const;};export default useResizeTableCol;

ResizeableTitle.tsx:自定义可拖动的表头th

import React, { useMemo, useState } from 'react';import type { ResizeCallbackData } from 'react-resizable';import { Resizable } from 'react-resizable';const ResizeableTitle: React.FC<any> = (props) => {const { width, className, children, onResize, style = {}, ...resetProps } = props;const [offset, setOffset] = useState<number>(0);const [nextWidth, setNextWidth] = useState<number>(58);const getTranslateX = useMemo(() => {if (offset >= nextWidth + 42) {return nextWidth - 42;}return offset;}, [offset, nextWidth]);if (className?.includes('ant-table-selection-column')) {return (<th className={className} {...resetProps}>{children}</th>);}// console.log(props);if (onResize) {return (<Resizablewidth={width + offset}height={0}handle={<spanclassName={`react-resizable-handle ${offset ? 'active' : ''}`}style={{ transform: `translateX(${getTranslateX}px)` }}onClick={(e) => {e.stopPropagation();e.preventDefault();}}/>}// onResizeStart={() => (this.resizing = true)}onResizeStop={(...arg: any[]) => {setOffset(0);onResize(...arg);}}onResizeStart={(e: any) => {const _nextWidth = e.target.parentNode.nextSibling.getBoundingClientRect().width;setNextWidth(_nextWidth);}}onResize={(e: any, { size }: ResizeCallbackData) => {const currentOffset = size.width - width;if (currentOffset > nextWidth - 42) {setOffset(nextWidth - 42);} else {setOffset(currentOffset);}}}draggableOpts={{enableUserSelectHack: true,minConstraints: [width - 42, 0],maxConstraints: [width + nextWidth, 0],}}><th className={className} style={{ ...style, width: width + 'px' }} {...resetProps}><divstyle={{ width: width + 'px' }}className="ofs-table-cell-wrapper"title={typeof children.join('') === 'string' ? children.join('') : ''}><div className="ofs-table-cell">{children}</div></div></th></Resizable>);}return (<th className={className} style={{ ...style, width: width + 'px' }}><divstyle={{ width: width + 'px' }}className="ofs-table-cell-wrapper"title={typeof children.join('') === 'string' ? children.join('') : ''}><div {...resetProps} className="ofs-table-cell">{children}</div></div></th>);};export default ResizeableTitle;

index.less:表格样式

.react-resizable {position: relative;}.react-resizable-handle {position: absolute;z-index: 999;bottom: 0;right: 0;width: 2px;height: 100%;cursor: col-resize;&.active::before {position: absolute;top: 0;bottom: 0;left: 50%;height: 1000px;border-left: 2px solid #d0d0d0;content: '';}}.ant-table-wrapper {position: relative;overflow: hidden;}.ofs-table-row {display: flex;flex-direction: row;width: 100%;overflow: hidden;}.ofs-table-cell-wrapper {width: 100%;overflow: hidden;}.ofs-table-cell {padding: 0 5px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.ant-table-thead > tr > th,.ant-table-tbody > tr > td,.ant-table tfoot > tr > th,.ant-table tfoot > tr > td {padding: 16px 0px;}.ant-table-thead > tr > th:last-child span.react-resizable-handle {display: none;}.ant-table-thead {.ant-table-cell-ellipsis {overflow: visible;& > div {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;word-break: keep-all;}}}

index.tsx:表格实现

import React, { useEffect, useRef, useState } from 'react';import { Table } from 'antd';import ResizeableTitle from './components/ResizeableTitle';import type { ColumnType } from 'antd/lib/table/interface';import './index.less';import useResizeTableCol from './hooks/useResizeTableCol';import { useSize } from 'ahooks';const columes = [{title: 'Full Name',width: '10%',dataIndex: 'name',key: 'name',},{title: 'Age',width: '10%',dataIndex: 'age',key: 'age',},{ title: 'Column 1', dataIndex: 'address', ellipsis: true, width: '10%', key: '1' },{ title: 'Column 2', dataIndex: 'address', ellipsis: true, width: '10%', key: '2' },{ title: 'Column 3', dataIndex: 'address', ellipsis: true, width: '10%', key: '3' },{ title: 'Column 4', dataIndex: 'address', ellipsis: true, width: '10%', key: '4' },{ title: 'Column 5', dataIndex: 'address', ellipsis: true, width: '20%', key: '5' },{ title: 'Column 6', dataIndex: 'address', ellipsis: true, width: '20%', key: '6' },{ title: 'Column 7', dataIndex: 'address', ellipsis: true, width: 100, key: '7' },{ title: 'Column 8', dataIndex: 'address', ellipsis: true, width: 100, key: '8' },{title: 'aa',key: 'operation',ellipsis: true,width: 100,// fixed: 'right',render: () => <a>action</a>,},];const data = [{key: '1',name: 'John Brown',age: 32,address: 'New York Park',},{key: '2',name: 'Jim Green',age: 40,address: 'London Park',},];const AntdTableTest: React.FC = () => {const tableRef = useRef(null);const tableWrapperSize = useSize(tableRef);const [wrapperWidth, setWrapperWidth] = useState<number>();const { colIsInit, tableColumns } = useResizeTableCol(wrapperWidth, tableRef, columes);useEffect(() => {console.log(tableWrapperSize);if (tableWrapperSize) {setWrapperWidth(tableWrapperSize.width);}}, [tableRef, tableWrapperSize]);return (<><div ref={tableRef}>{colIsInit ? (<TablerowSelection={{type: 'checkbox',}}columns={tableColumns as ColumnType<any>[]}components={{header: {cell: ResizeableTitle,},}}dataSource={data}/>) : null}</div></>);};export default AntdTableTest;

PS:

1:此实现只是demo,在具体项目中,可基于antd的Table进行二次封装,更便于项目中使用;

2:在具体项目中,需考虑的场景更复杂,比如当有纵向滚动条时,宽度的计算需去除滚动条的宽度(将wrapperWidth减去滚动条宽度即可实现);

3:如有问题欢迎留言

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。