基于 React 实现无限滚动表格

1,247 阅读4分钟

文字的无限滚动

无限滚动效果

为了实现无限滚动的视觉效果,需要准备两段相同的文本,并让第二段文本的头部衔接在第一段文本的尾部;同时,为两段文本设置相同的滚动动画。

当第一段文本滚动到尾部时,如果能让第一段文本的位置瞬间移动回头部,并将第一段文本的头部内容替换为第二段的头部内容,同时动画也回到开始位置,这样,用户从视觉效果上看是感受不到变化的。

Marquee 组件及其样式的实现代码如下:

// Marquee.tsx
export default function Marquee(props) {
  const { children } = props;

  return (
    <div className={styles.marquee}>
      <div className={styles.ctx}>{children}</div>
      <div className={styles.ctx}>{children}</div>
    </div>
  );
}

// App.tsx
export default function App() {
  return (
    <Marquee>只期待 後來的你 能快樂 那就是 後來的我 最想的</Marquee>
  );
}
// Marquee.module.scss
.marquee {
  width: 40px;
  overflow: hidden;
  position: relative;

  .ctx {
    animation: scroll 4s infinite linear; // 设置滚动动画
  }
}

@keyframes scroll {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-100%);
  }
}

动画.gif

悬停效果

实现鼠标悬停效果,通常有两种方法:

1)声明 isHovered 变量,通过监听 onMouseEnteronMouseLeave 事件来动态修改 isHovered 的值,从而实现悬停效果。

2)使用 :hover 伪类,通过设置 animation-play-state 属性控制动画执行,从而实现悬停效果。

本文使用第二种方法,为整个外层容器设置 :hover 样式。当鼠标悬浮在整个容器上时,让内部元素的动画暂停。在 Marquee.module.scss 中添加如下样式:

// Marquee.module.scss
.marquee:hover {
  .ctx {
    animation-play-state: paused;
  }
}

动画.gif

到此,基本功能已经实现,接下来开始实现表格组件的无限滚动。

表格的无限滚动

首先,实现根据配置动态生成表格的功能。

本文从传入的对象数组中提取所有的键,作为表头内容。

说明:本文实现的表头提取函数考虑了传入可选参数的情况,因此会遍历所有列表项并提取所有存在的键。如果表头属性完全固定,可以使用更简便的方法提取。

成功提取表头后,就可以遍历对象数组中的每一项,并根据对应的键将属性值填入相应的单元格中,逐步构建表格内容。

基于上述分析,表格组件的初步实现如下:

// DataTable.tsx
export default function DataTable<T extends object>({ dataSource }: { dataSource: Array<T> }) {
  if (!Array.isArray(dataSource) || dataSource.length === 0) {
    return null; // 数据源不是数组或数组为空,停止操作
  }

  const labelList = dataSource.reduce((acc: any, cur: any) => {
    const keys = Object.keys(cur);
    for (const key of keys) {
      if (!acc.includes(key)) {
        acc.push(key);
      }
    }
    return acc;
  }, []); // 提取表头

  return (
    <table>
      <thead>
        <tr>
          {labelList.map((key, index) => (
            <th key={index}>{key}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {dataSource.map((data, rowIndex) => (
          <tr key={rowIndex}>
            {labelList.map((key, columnIndex) => (
              <!-- 如果是可选属性,赋予默认值 -->
              <td key={columnIndex}>{data[key] ?? '-'}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

App.tsx 中引入 DataTable 组件并传入 data 数组,就可以看到动态生成的表格。

// App.tsx
import DataTable from '@/components/DataTable';

export default function App() {
  const data = [
    { name: 'Alice', city: 'New York' },
    { name: 'Bob', age: 30, city: 'San Francisco' },
    { name: 'Charlie', age: 35, city: 'Chicago' },
    { name: 'David', age: 40, city: 'Los Angeles', num: 0 },
  ];

  return (
    <DataTable dataSource={data} />
  );
}

image.png

接下来,就是加上无限滚动的效果。

通常来说,可滚动表格只需要一个表头,也就是只需要一个 thead 部分,并且需要固定在顶部。按照上述文字滚动的实现思路,只要在一个 table 中实现两个相同的 tbody 内容,并给它们设置相同的动画即可。

同时,为了限制整体容器的高度,以防用户在表格位置滚动时感知到突变,造成不好的视觉体验,还需要给 table 加一层外部容器。

DataTable 组件的整体实现如下:

// DataTable.tsx
export default function DataTable<T extends object>({ dataSource }: { dataSource: Array<T> }) {
  if (!Array.isArray(dataSource) || dataSource.length === 0) {
    return null; // 数据源不是数组或数组为空,停止操作
  }

  const labelList = dataSource.reduce((acc: any, cur: any) => {
    const keys = Object.keys(cur);
    for (const key of keys) {
      if (!acc.includes(key)) {
        acc.push(key);
      }
    }
    return acc;
  }, []); // 提取表头

  return (
    <div className={styles.container}>
      <table className={styles.table}>
        <thead>
          <tr>
            {labelList.map((key, index) => (
              <th key={index}>{key}</th>
            ))}
          </tr>
        </thead>
        <tbody className={styles.tbody}>
          {dataSource.map((data, rowIndex) => (
            <tr key={rowIndex}>
              {labelList.map((key, columnIndex) => (
                <td key={columnIndex}>{data[key] ?? '-'}</td>
              ))}
            </tr>
          ))}
        </tbody>
        <tbody className={styles.tbody}>
          {dataSource.map((data, rowIndex) => (
            <tr key={rowIndex}>
              {labelList.map((key, columnIndex) => (
                <td key={columnIndex}>{data[key] ?? '-'}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
// DataTable.module.scss
@keyframes scroll {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-100%);
  }
}

.container {
  height: 160px;
  overflow: hidden;

  .table {
    height: 100%;
    overflow: hidden;
    position: relative;
    background-color: #e6e6e6;
  
    .tbody {
      animation: scroll 4s infinite linear;
    }
  }
}

table {
  width: max-content;
  border-spacing: 0;

  thead {
    height: 30px;
    z-index: 99;
    background-color: #adbcaa;
    position: sticky;
    top: 0;

    tr {
      color: #fff;
      font-weight: bold;
    }
  }

  th,
  td {
    width: max-content;
    padding: 0 8px;
    line-height: 30px;
    text-align: center;
  }
}

刷新页面,就可以看到表格的无限滚动效果已经成功实现了。

动画.gif

最后

前端新手一枚,欢迎大家提出问题或建议,一起进步。