React基于antd Table实现可拖拽调整列宽的表格
实现功能1:表格列宽初始自动分配、列宽总和不能超过容器宽度(无横向滚动条,公司项目特殊需求)2:当容器宽度变化时,保持当前列宽的分配比例,等比缩小3:拖动过程中不进行列宽的调整,只有释放之后再进行列宽调整效果图见目录结构:在这里插入代码片useTableCol.tsx: 处理表格列的宽度计算等相关逻辑import { useMemoizedFn, useSafeState } from 'ahoo
·
实现功能
1:表格列宽初始自动分配、列宽总和不能超过容器宽度(无横向滚动条,公司项目特殊需求)
2:当容器宽度变化时,保持当前列宽的分配比例,等比缩小
3:拖动过程中不进行列宽的调整,只有释放之后再进行列宽调整
效果图见
目录结构:
在这里插入代码片
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;
// 左移,当前宽度小于42
if (currentWidth < 42) {
widthList[index] = 42;
widthList[index + 1] = nextWidth - 42 + currentWidth;
} else if (nextWidth < 42) {
// 右移,下一列得宽度小于42
widthList[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 (
<Resizable
width={width + offset}
height={0}
handle={
<span
className={`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}>
<div
style={{ 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' }}>
<div
style={{ 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 ? (
<Table
rowSelection={{
type: 'checkbox',
}}
columns={tableColumns as ColumnType<any>[]}
components={{
header: {
cell: ResizeableTitle,
},
}}
dataSource={data}
/>
) : null}
</div>
</>
);
};
export default AntdTableTest;
更多推荐
已为社区贡献4条内容
所有评论(0)