React 实现无限滚动表格

前言

无限滚动效果

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

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

以下代码是 Marquee 组件及其样式的简单实现。

tsx 复制代码
// 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>
  );
}
scss 复制代码
// 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%);
  }
}

悬停效果

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

  1. 声明 isHovered 变量,通过 onMouseEnteronMouseLeave 事件改变 isHovered 的值,从而实现悬停效果。
  2. 使用 :hover 伪类,通过设置 animation-play-state 属性控制动画的运行,从而实现悬停效果。

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

scss 复制代码
// ...
.marquee:hover {
  .ctx {
    animation-play-state: paused;
  }
}

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

表格的无限滚动

首先,实现根据配置动态生成表格的功能。我们从传入的对象数组中提取所有的键,作为表头内容。

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

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

基于上述步骤分析,表格组件的初步实现如下(样式在文章末尾给出):

tsx 复制代码
// 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 数组,就可以看到表格动态生成如下。

tsx 复制代码
// 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} />
  );
}

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

通常来说,可滚动表格只会有一个表头,也就是只有一个 thead 部分,并且此处我们把它固定在顶部。再按照上述文字滚动的实现思路,我们只要在一个 table 中准备两份相同的 tbody 内容,然后给这两份 tbody 设置相同的动画即可。

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

到此,整个分析过程就结束了,DataTable 组件的最终实现如下:

tsx 复制代码
// 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>
  );
}
scss 复制代码
// 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;
  }
}

然后,刷新页面,再看一下页面效果,这样就实现了。

最后

前端新手一枚,如果有什么不对的地方或者更好的建议,欢迎大家提出。

相关推荐
恋猫de小郭30 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端