Bootstrap

React之antdesign官网虚拟列表(Table组件)二次封装

1. 功能介绍、建议、官网链接

  1. 建议:走一遍官网的虚拟列表(最好走通)
  2. 官网虚拟列表功能都有;但是!官网虚拟列表里面跟之前表格 .ant-table-tbody 相关的功能都无了,因为这块官网是整体替换了 .ant-table-tbody 这一级的所有内容
  3. 添加功能:
    (1) 全选与单选功能
    (2)滚动加载功能(虚拟列表级滚动加载)
    (3)定位某一行
    (4)表格字体闪烁功能
    (5)可对表头进行隐藏或显示配置,表格内部内容进行宽度适配
    (6)高度设置(这块可能得靠你自己再去深度研究一下这块了)
  4. react-window官网网站需要翻墙才能去访问哦
  5. ant design 官网表格
  6. 观赏地址(需越墙)

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;
}

体验地址

;