1. 功能介绍、建议、官网链接
- 建议:走一遍官网的虚拟列表(最好走通)
- 官网虚拟列表功能都有;但是!官网虚拟列表里面跟之前表格
.ant-table-tbody
相关的功能都无了,因为这块官网是整体替换了.ant-table-tbody
这一级的所有内容 - 添加功能:
(1) 全选与单选功能
(2)滚动加载功能(虚拟列表级滚动加载)
(3)定位某一行
(4)表格字体闪烁功能
(5)可对表头进行隐藏或显示配置,表格内部内容进行宽度适配
(6)高度设置(这块可能得靠你自己再去深度研究一下这块了) - react-window官网网站需要翻墙才能去访问哦
- ant design 官网表格
- 观赏地址(需越墙)
2. 效果图
3. 依赖下载
npm i react-window
npm i rc-resize-observer
4. 二次封装虚拟列表组件 LhVirtualTable 代码(virtualTable/components/LhVirtualTable/index.tsx)
import { Table } from 'antd';
import styles from './index.less'
import ResizeObserver from 'rc-resize-observer';
import React, { useCallback, useEffect, useRef, useState } from 'react';
// @ts-ignore
import { VariableSizeGrid as Grid } from 'react-window';
// 获取可滚动节点的父级节点
const findNeedNode: any = (node: any, key: string = 'ant-table-body') => {
if (node?.className === key) return node;
if (node?.children) {
let needNode: any = [];
[...node.children].forEach((child: any) => {
needNode.push(findNeedNode(child, key))
})
return needNode;
}
}
const getByteLen = (val: string) => {
let len = 0;
for (let i = 0; i < val.length; i++) {
let a = val.charAt(i);
if (a.match(/[^\x00-\xff]/ig) != null) {
len += 2;
} else {
len += 1;
}
}
return len;
}
const LhVirtualTable = (props: any) => {
const { dataSource, columns, scroll } = props;
const [tableWidth, setTableWidth] = useState(0);
/* 修改 --------------------------------------------------------------------------------------------------------------*/
/**
* lh__pagination @param {object: {pageSize: {number},pageNum: {number},total: {number}}};
* @describe {pageSize: '每页条数'}
* @describe {pageNum: '处于第几页'}
* @describe {total: '数据总数'}
* lh__onScrollBottom @param { Function }
* @describe 触底回调函数,注意只有数据总数变更了后才会再进行下一步
* lh__scrollIndex @param { number }
* @describe 快速跳转到表格内的第几行
*/
const { lh__pagination, lh__onScrollBottom, lh__scrollIndex } = props;
// 滚动加载
const [oldDataSourceLen, setOldDataSourceLen] = useState<any>(0)
const tableRef = useRef<any>(null)
const _onScroll: any = (res: { scrollLeft: number, scrollTop: number }) => {
const { scrollTop } = res;
const dom = findNeedNode(tableRef.current, 'virtual-grid')?.flat(Infinity)[0];
const clientHeight = dom?.children[0]?.clientHeight
if (!clientHeight) return;
// 这个为滚动条的高度
const scrollBarHeight = 16;
if ((clientHeight - scroll.y) + scrollBarHeight > scrollTop + 1 + lh__pagination.pageNum) return;
const { pageSize, pageNum, total } = lh__pagination;
const all = pageSize * pageNum;
if (all > total) return;
// 表格置空不能有滚动加载
if (dataSource.length < 1) return;
// bug: 这块有个问题,如果后端返回列表数据接口报错后或者超时后,就无法进行滚动分页。
if (oldDataSourceLen === dataSource.length) return;
setOldDataSourceLen(dataSource.length)
// 通知父组件
lh__onScrollBottom({ ...lh__onScrollBottom, pageNum: pageNum + 1 });
}
// -----
// 表头添加与消失后兼容表内数据
const [gridKey, setGridKey] = useState(0)
const [mergedColumns, setMergedColumns] = useState([]);
useEffect(() => {
// 没有宽度的个数
const widthColumnCount = columns!.filter(({ width }: any) => !width).length;
// 没有宽度且被隐藏的个数
const wCFilterHidden = columns!.filter(({ hidden, width }: any) => hidden && !width).length;
// 已经使用的宽度
const usedWidth = columns!.reduce((pre: number, next: { width: number, hidden: boolean }) => {
// 如果隐藏则不计算宽度
if (next.hidden) return pre
return next.width ? pre + +next.width : pre;
}, 0)
setMergedColumns(columns.filter((v: { hidden: boolean }) => !v.hidden)!.map((column: any) => {
if (column.width) {
// 判断以哪种宽度去计算最终宽度(单个宽度的总值选那个)
const widthP = +column.width;
if (widthColumnCount === 0) return { ...column, width: (widthP / usedWidth) * tableWidth }
return wCFilterHidden < 1 ? { ...column, width: widthP } : { ...column, width: (widthP / usedWidth) * tableWidth };
}
return {
...column,
width: Math.floor((tableWidth - usedWidth) / widthColumnCount),
};
}))
// 通知虚拟列表组件进行宽度更新
setGridKey(gridKey + 1)
}, [columns, tableWidth])
// -----
/* -------------------------------------------------------------------------------------------------------------- */
// 虚拟列表 antdesign 官网
// const { columns, scroll } = props;
// const [tableWidth, setTableWidth] = useState(0);
// const widthColumnCount = columns!.filter(({ width }: any) => !width).length;
// const mergedColumns = columns!.map((column: any) => {
// if (column.width) {
// return column;
// }
// return {
// ...column,
// width: Math.floor(tableWidth / widthColumnCount),
// };
// });
const gridRef = useRef<any>();
const [connectObject] = useState<any>(() => {
const obj = {};
Object.defineProperty(obj, 'scrollLeft', {
get: () => {
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft;
}
return null;
},
set: (scrollLeft: number) => {
if (gridRef.current) {
gridRef.current.scrollTo({ scrollLeft });
}
},
});
return obj;
});
const resetVirtualGrid = () => {
gridRef.current?.resetAfterIndices({
columnIndex: 0,
shouldForceUpdate: true,
});
};
useEffect(() => resetVirtualGrid, [tableWidth]);
// ------
// scroll定位
const scrollPosition = (index: number) => {
const dom = findNeedNode(tableRef.current, 'virtual-grid')?.flat(Infinity)[0];
gridRef.current.scrollToItem({ rowIndex: index });
const scrollDom = dom.children[0]
// gridRef.current.scrollTo({ scrollTop: 1200})
setTimeout(() => {
console.log(scrollDom.children, 'scrollDom.children')
for (let i = 0; i < scrollDom.children.length; i++) {
const node = scrollDom.children[i];
node.style.background = 'transparent';
if (node?.id?.includes(`lh-${index}`)) {
node.style.background = 'rgba(10, 177, 205, .6)';
}
}
// scrollDom.children.forEach((node: any) => {
// node.style.background = 'transparent';
// if (node?.id?.includes(`lh-${index}`)) {
// node.style.background = 'rgba(10, 177, 205, .6)';
// }
// })
}, 0);
}
useEffect(() => {
scrollPosition(lh__scrollIndex)
}, [lh__scrollIndex])
// -----
const renderVirtualList = useCallback((rawData: object[], { scrollbarSize, ref, onScroll }: any) => {
ref.current = connectObject;
const totalHeight = rawData.length * 54;
return (
<Grid
ref={gridRef}
className="virtual-grid"
key={gridKey}
columnCount={mergedColumns.length}
columnWidth={(index: number) => {
const { width } = mergedColumns[index];
return totalHeight > scroll!.y! && index === mergedColumns.length - 1
? (width as number) - scrollbarSize - 1
: (width as number);
}}
height={scroll!.y as number}
rowCount={rawData.length}
rowHeight={(index: number) => {
const width = tableRef.current?.clientWidth;
const baseNumber = 24 * ((+(width > (1920 / 2)) + 1))
const row = rawData[index];
if (!row) return;
const max = Object.values(row).reduce((pre, next) => {
let len = getByteLen(`${next}`)
if (pre > len) return pre
else return len
}, 0)
return + ((Math.ceil(max / baseNumber) * 24).toFixed(2))
}}
width={tableWidth}
onScroll={(res: { scrollLeft: number }) => {
const { scrollLeft } = res;
onScroll({ scrollLeft });
// 虚拟列表实现
_onScroll(res)
}}
>
{({
columnIndex,
rowIndex,
style,
}: {
columnIndex: number;
rowIndex: number;
style: React.CSSProperties;
}) => {
const index = rowIndex;
const column = (mergedColumns as any)[columnIndex];
const record = (rawData[index] as any)
const text = record[column.dataIndex]
return (
<div
id={`lh-${rowIndex}-${columnIndex}`}
key={`lh-${rowIndex}-${columnIndex}`}
className={['virtual-table-cell', columnIndex === mergedColumns.length - 1 ? 'virtual-table-cell-last' : ''].join(' ')}
style={{ ...style, display: 'flex', alignItems: 'center' }}
onContextMenu={(event) => {
event.stopPropagation();
event.preventDefault();
// onContextMenuTable?.(record, event);
}}
>
<div style={{ width: '100%', textAlign: column.align || 'left', lineHeight: '18px' }}>
{
column.render ? column.render(text, record, index) : `${rowIndex}-${columnIndex}`
}
</div>
</div>
)
}}
</Grid>
);
}, [connectObject, mergedColumns]);
return <>
<ResizeObserver
onResize={({ width }) => {
setTableWidth(width);
}}
>
<Table
{...props}
className={styles['virtual-table']}
columns={mergedColumns.filter((v: any) => !v.hidden)}
ref={tableRef}
pagination={false}
components={{
body: renderVirtualList,
}}
/>
</ResizeObserver>
</>
}
export default LhVirtualTable
5. 使用 virtualTable/index.tsx
import { Checkbox, Button, Popover } from 'antd';
import { useState, useEffect } from 'react';
import LhVirtualTable from './components/LhVirtualTable'
import styles from './index.less'
// @ts-ignore
import _ from 'lodash';
// service为模仿 服务端返回数据;具体数据你找后台;
const dataName = ['涨', '车', '轴', '走', '周', '凤', '胡', '晶', '京', '梅', '韦', '小', '字', '陈', '程', '测', '就', '当', '费', '飞', '矿', '况', '李', '刘', '成', '龙', '于', '巷', '港', '翔']
const serviceArr: any = []
const service = (param: any) => {
const { pageNow, pageSize } = param
const createData = (arr: any) => {
const random = (number: any) => Math.floor(Math.random() * number).toFixed(0)
const nameFn = () =>
Array.from(
new Array(+random(3) < 2 ? 2 : 3),
() => dataName[+random(dataName.length)]
).join('')
const data = Array.from(new Array(pageSize), () => nameFn()).map(
(name, i) => ({ name, checked: false, key: i, len: new Array(+random(20) + 1).fill('').map(v => name).join(',') })
)
arr.push(...data)
}
createData(serviceArr)
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: serviceArr.slice(pageSize * (pageNow - 1), pageSize * pageNow),
total: 300000
})
}, 500)
})
}
// =================================================================================
const colorChange = (t: any) => {
const colorList = ['#f00', '#0f0', '#00f', '#0ff', '#f60', '#f0f'];
const random = +(Math.random() * +colorList.length - 1).toFixed(0)
return <span
className={styles['animation_twinkle']}
style={{
color: colorList[random]
}}
>
{t}
</span>
};
const VirtualTable = () => {
// Usage
const [columns, setColumns] = useState<any>([
// 虚拟列表多选功能
{
title: '',
width: 30,
align: 'center',
render: (text: any, record: any) => <Checkbox
style={{ marginLeft: '6px' }}
checked={selectedRowKeys.includes(record.key)}
indeterminate={selectedRowKeys.includes(record.key)}
onChange={(e) => {
if (e.target.checked) {
selectedRowKeys.push(record.key);
} else {
const i = selectedRowKeys.findIndex((v: any) => v === record.key);
selectedRowKeys.splice(i, 1)
}
setSelectedRowKeys(_.cloneDeep(rowSelection.selectedRowKeys))
}}
></Checkbox>
},
{ title: 'A', dataIndex: 'key', width: 150, render: (t: any) => colorChange(t) },
{ title: 'B', dataIndex: 'key', render: (t: any) => colorChange(t) },
{ title: 'C', dataIndex: 'key', render: (t: any) => colorChange(t) },
{ title: 'D', dataIndex: 'key', render: (t: any) => colorChange(t) },
{ title: 'E', dataIndex: 'key', width: 200, render: (t: any) => colorChange(t) },
{ title: 'F', dataIndex: 'key', width: 100, render: (t: any) => colorChange(t) },
{ title: 'len', dataIndex: 'len', width: 400, render: (t: any) => colorChange(t) },
]);
const [data, setData] = useState<any>([])
// const data = Array.from({ length: 10 }, (_, key) => ({ key, checked: Math.random() > 0.5 }));
// 表格多选框的配置
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const rowSelection = {
selectedRowKeys,
// columnWidth: !flag.current ? '40px' : '0',
hideSelectAll: false,
onChange: (newSelectedRowKeys: React.Key[]) => {
console.log(newSelectedRowKeys, 'newSelectedRowKeys')
setSelectedRowKeys(_.cloneDeep(newSelectedRowKeys))
},
}
// --------------- 表格多选框的配置
// 滚动加载功能
const [loading, setLoading] = useState(false)
const [lh__pagination, setLh__pagination] = useState({
pageSize: 1000,
pageNum: 1,
total: 0
})
const param = {
pageNow: lh__pagination.pageNum,
pageSize: lh__pagination.pageSize
}
const getList = (lh__pagination: any) => {
setLoading(true)
service(param)
.then(({ data: ds, total }: any) => {
data.push(...ds)
lh__pagination.total = total;
setLh__pagination(_.cloneDeep(lh__pagination))
setData(_.cloneDeep(data))
// .....
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
getList(lh__pagination)
}, [])
// scroll定位
const [lh__scrollIndex, setLh__scrollIndex] = useState(0)
const scrollPosition = () => {
setLh__scrollIndex(+(Math.random() * data.length).toFixed(0))
}
// ---------------
// 配置表头展示
const PopoverContent = () => {
const hiddenTitle = columns.filter((v: any) => _.isString(v.title)).map((item: any) => (!item.hidden && item.title) || '');
const plainOptions = columns.filter((v: any) => _.isString(v.title)).map((item: any) => item.title);
const onChange = (e: Array<any>) => {
columns.forEach((item: any) => {
item.hidden = !e.includes(item.title)
})
setColumns(_.cloneDeep([...columns]))
}
return <>
<Checkbox.Group
className='flex-column'
options={plainOptions}
defaultValue={hiddenTitle}
style={{ width: '100px' }}
onChange={onChange} />
</>
}
// ---------------
const marginRight10 = {
marginRight: '10px'
}
return <>
<Button style={{ ...marginRight10 }} type='primary' onClick={() => scrollPosition()}>进行scroll随机定位</Button>
<Popover placement="bottom" content={PopoverContent} trigger="click">
<Button type='primary'>对表头进行选择</Button>
</Popover>
<LhVirtualTable
columns={columns}
dataSource={data}
loading={loading}
scroll={{ y: 300, x: '100vw' }}
lh__scrollIndex={lh__scrollIndex}
lh__pagination={lh__pagination}
lh__onScrollBottom={(pa: any) => {
const pag = _.cloneDeep({ ...lh__pagination, ...pa })
setLh__pagination(pag)
getList(pag)
}}
/>
</>
}
export default VirtualTable
6. 布灵布灵效果样式文件 virtualTable/index.less
@keyframes twinkle {
0% {}
50% {
color: #000;
}
}
.animation_twinkle {
animation: twinkle 1s infinite;
}