实现无缝滚动无滚动条的 Element UI 表格(附完整代码)

实现无缝滚动无滚动条的 Element UI 表格(附完整代码)

在后台管理系统或数据监控场景中,经常需要实现表格无缝滚动展示数据,同时希望隐藏滚动条保持界面整洁。本文将基于 Element UI 实现一个 无滚动条、无缝循环、hover 暂停、状态高亮 的高性能滚动表格,全程流畅无卡顿,适配多浏览器。

最终效果

  • 🚀 无缝循环滚动,无停顿、无跳跃
  • 🚫 视觉上完全隐藏滚动条,保留滚动功能
  • 🛑 鼠标悬浮自动暂停,离开恢复滚动
  • 🌈 支持状态字段高亮(如不同状态显示不同颜色)
  • 🎨 美观的表格样式,hover 行高亮反馈
  • 🛠 高度可配置(行高、滚动速度、表格高度等)

技术栈

  • Vue 2 + Element UI(适配 Vue 2 项目,Vue 3 可快速迁移)
  • SCSS(样式模块化,便于维护)

实现思路

  1. 无缝滚动核心:通过「数据拼接」(原数据 + 原数据副本)实现视觉上的无限循环,滚动到原数据末尾时瞬间重置滚动位置,无感知切换
  2. 隐藏滚动条:多浏览器兼容 CSS 屏蔽滚动条样式,同时预留滚动条宽度避免内容裁剪
  3. 流畅滚动优化 :避免 DOM 频繁重绘,用 scrollTop 控制滚动,关闭平滑滚动避免停顿
  4. 交互增强:hover 暂停滚动、行 hover 高亮、状态字段颜色区分

配置说明

参数名 类型 默认值 说明
tableData Array \[\] 表格数据源(必传)
columns Array \[\] 列配置(必传,支持 statusConfig 状态样式)
rowHeight Number 36 行高(单位:px)
scrollSpeed Number 20 滚动速度(毫秒 / 像素),值越小越快
scrollPauseOnHover Boolean true 鼠标悬浮是否暂停滚动
tableHeight Number 300 表格高度(父组件配置)

完整代码实现

1. 滚动表格组件(SeamlessScrollTable.vue)

js 复制代码
<template>
  <div class="tableView">
    <el-table
      :data="combinedData"
      ref="scrollTable"
      style="width: 100%"
      height="100%"
      @cell-mouse-enter="handleMouseEnter"
      @cell-mouse-leave="handleMouseLeave"
      :cell-style="handleCellStyle"
      :show-header="true"
    >
      <el-table-column
        v-for="(column, index) in columns"
        v-bind="column"
        :key="index + (column.prop || index)"
        :min-width="column.minWidth || '100px'"
      >
        <template slot-scope="scope">
          <span v-if="column.statusConfig" :class="getColumnStatusClass(column, scope.row)">
            {{ scope.row[column.prop] }}
          </span>
          <span v-else>
            {{ scope.row[column.prop] }}
          </span>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
  export default {
    name: 'SeamlessScrollTable',
    props: {
      tableData: {
        type: Array,
        required: true,
        default: () => [],
      },
      columns: {
        type: Array,
        required: true,
        default: () => [],
      },
      rowHeight: {
        type: Number,
        default: 36,
      },
      scrollSpeed: {
        type: Number,
        default: 20, // 滚动速度(毫秒/像素),20-40ms
      },
      scrollPauseOnHover: {
        type: Boolean,
        default: true,
      },
    },
    data() {
      return {
        autoPlay: true,
        timer: null,
        offset: 0,
        combinedData: [], // 拼接后的数据,用于实现无缝滚动
      }
    },
    computed: {
      // 计算表格可滚动的总高度(仅当数据足够多时才滚动)
      scrollableHeight() {
        return this.tableData.length * this.rowHeight
      },
      // 表格容器可视高度
      viewportHeight() {
        return this.$refs.scrollTable?.$el.clientHeight || 0
      },
    },
    watch: {
      tableData: {
        handler(newVal) {
          // 数据变化时,重新拼接数据
          this.combinedData = [...newVal, ...newVal]
          this.offset = 0
          this.restartScroll()
        },
        immediate: true,
        deep: true,
      },
      autoPlay(newVal) {
        newVal ? this.startScroll() : this.pauseScroll()
      },
    },
    mounted() {
      this.$nextTick(() => {
        // 只有当数据总高度 > 可视高度时,才启动滚动
        if (this.scrollableHeight > this.viewportHeight) {
          this.startScroll()
        }
      })
    },
    beforeDestroy() {
      this.pauseScroll()
    },
    methods: {
      handleMouseEnter() {
        this.scrollPauseOnHover && (this.autoPlay = false)
      },
      handleMouseLeave() {
        this.scrollPauseOnHover && (this.autoPlay = true)
      },
      startScroll() {
        this.pauseScroll()

        const tableBody = this.$refs.scrollTable?.bodyWrapper
        if (!tableBody || this.tableData.length === 0) return

        this.timer = setInterval(() => {
          if (!this.autoPlay) return

          this.offset += 1
          tableBody.scrollTop = this.offset

          // 关键:当滚动到原数据末尾时,瞬间重置滚动位置到开头
          if (this.offset >= this.scrollableHeight) {
            this.offset = 0
            tableBody.scrollTop = 0
          }
        }, this.scrollSpeed)
      },
      pauseScroll() {
        this.timer && clearInterval(this.timer)
        this.timer = null
      },
      restartScroll() {
        this.pauseScroll()
        if (this.scrollableHeight > this.viewportHeight) {
          this.startScroll()
        }
      },
      getColumnStatusClass(column, row) {
        const statusKey = column.statusField || column.prop
        const statusValue = row[statusKey]
        return typeof column.statusConfig === 'function'
          ? column.statusConfig(statusValue, row)
          : column.statusConfig[statusValue] || ''
      },
      handleCellStyle() {
        return {
          padding: '4px 0',
          height: `${this.rowHeight}px`,
          lineHeight: `${this.rowHeight}px`,
        }
      },
    },
  }
</script>

<style scoped lang="scss">
  .tableView {
    width: 100%;
    height: 100%;
    overflow: hidden;

    ::v-deep .el-table {
      background-color: transparent;
      color: #303133;
      border-collapse: separate;
      border-spacing: 0;

      &::before {
        display: none;
      }

      th.el-table__cell.is-leaf {
        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
        background: transparent !important;
        font-weight: 500;
        color: rgba(0, 0, 0, 0.6);
        padding: 8px 0;
      }

      tr.el-table__row {
        background-color: transparent;
        transition: background-color 0.2s ease;

        &:hover td {
          background-color: rgba(0, 0, 0, 0.02) !important;
        }
      }

      .el-table__cell {
        border: none;
        padding: 4px 0;

        .cell {
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          padding: 0 8px;
        }
      }

      .el-table__body-wrapper {
        height: 100%;
        scroll-behavior: auto;
        &::-webkit-scrollbar {
          display: none !important;
          width: 0 !important;
          height: 0 !important;
        }
        scrollbar-width: none !important;
        -ms-overflow-style: none !important;
      }
    }

    ::v-deep .status-warning {
      color: #e6a23c;
      font-weight: 500;
    }

    ::v-deep .status-danger {
      color: #f56c6c;
      font-weight: 500;
    }

    ::v-deep .status-success {
      color: #67c23a;
      font-weight: 500;
    }

    ::v-deep .status-info {
      color: #409eff;
      font-weight: 500;
    }
  }
</style>

2. 父组件使用示例(TableIndex.vue)

js 复制代码
<template>
  <div class="table-container">
    <h2 class="table-title">设备状态监控表格</h2>
    <div class="table-wrapper" :style="{ height: tableHeight + 'px' }">
      <!-- 配置滚动参数 -->
      <seamless-scroll-table
        :table-data="tableData"
        :columns="columns"
        :row-height="36"
        :scroll-speed="30"
      />
    </div>
  </div>
</template>

<script>
  import SeamlessScrollTable from './SeamlessScrollTable.vue'

  export default {
    name: 'DeviceStatusTable',
    components: { SeamlessScrollTable },
    data() {
      return {
        tableHeight: 300, // 表格高度可配置
        // 表格数据
        tableData: [
          { id: '1001', name: '设备A', type: '温度', state: '待检查' },
          { id: '1002', name: '设备B', type: '压力', state: '已超期' },
          { id: '1003', name: '设备C', type: '湿度', state: '已完成' },
          { id: '1004', name: '设备D', type: '电压', state: '超期完成' },
          { id: '1005', name: '设备E', type: '电流', state: '待检查' },
          { id: '1006', name: '设备F', type: '电阻', state: '已超期' },
          { id: '1007', name: '设备G', type: '功率', state: '已完成' },
        ],
        // 列配置
        columns: [
          { prop: 'id', label: '编号', minWidth: '140px' },
          { prop: 'name', label: '名称', width: '100px' },
          { prop: 'type', label: '设备类型', width: '120px' },
          {
            prop: 'state',
            label: '状态',
            width: '100px',
            statusField: 'state',
            // 状态样式配置(支持对象/函数)
            statusConfig: {
              待检查: 'status-warning',
              已超期: 'status-danger',
              已完成: 'status-success',
              超期完成: 'status-info',
            },
          },
        ],
      }
    },
    methods: {
      getStatusClass(state) {
        const statusMap = {
          待检查: 'status-warning',
          已超期: 'status-danger',
          已完成: 'status-success',
          超期完成: 'status-info',
        }
        return statusMap[state] || ''
      },
    },
  }
</script>

<style scoped lang="scss">
  .table-container {
    width: 100%;
    max-width: 500px;
    margin: 0 auto;
    padding: 20px;
    box-sizing: border-box;
  }

  .table-title {
    color: #303133;
    margin-bottom: 16px;
    font-size: 18px;
    font-weight: 500;
    text-align: center;
    position: relative;
  }

  .table-wrapper {
    background-color: #ffffff;
    border-radius: 8px;
    padding: 16px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
    box-sizing: border-box;
  }
</style>
相关推荐
范什么特西2 分钟前
一些常用名词
开发语言·前端·javascript
晴天163 分钟前
前端打包工具全景解析
前端
晴天165 分钟前
Vite vs Webpack 全方位对比
前端·webpack·node.js
体毛旺盛的猿7 分钟前
HarmonyOS开发面试题
前端·华为·harmonyos
anOnion21 分钟前
构建无障碍组件之Landmarks Pattern
前端·html·交互设计
牧瀬クリスだ23 分钟前
前端三件套基础之JS!!!!
前端·javascript
风骏时光牛马26 分钟前
提示词工程:大模型交互的核心塑造技术
前端
单线程_0129 分钟前
从案例分析 Vue3 Tokenizer+Parser 源码四
前端·javascript·vue.js
31 分钟前
Python 图片处理:裁剪缩放至300×200,格式JPG/PNG/GIF,控制大小≤100KB
linux·前端·python
_阿南_8 小时前
项目中新增给AI制定的代码规范
前端·程序员