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

最终效果
- 🚀 无缝循环滚动,无停顿、无跳跃
 - 🚫 视觉上完全隐藏滚动条,保留滚动功能
 - 🛑 鼠标悬浮自动暂停,离开恢复滚动
 - 🌈 支持状态字段高亮(如不同状态显示不同颜色)
 - 🎨 美观的表格样式,hover 行高亮反馈
 - 🛠 高度可配置(行高、滚动速度、表格高度等)
 
技术栈
- Vue 2 + Element UI(适配 Vue 2 项目,Vue 3 可快速迁移)
 - SCSS(样式模块化,便于维护)
 
实现思路
- 无缝滚动核心:通过「数据拼接」(原数据 + 原数据副本)实现视觉上的无限循环,滚动到原数据末尾时瞬间重置滚动位置,无感知切换
 - 隐藏滚动条:多浏览器兼容 CSS 屏蔽滚动条样式,同时预留滚动条宽度避免内容裁剪
 - 流畅滚动优化 :避免 DOM 频繁重绘,用 
scrollTop控制滚动,关闭平滑滚动避免停顿 - 交互增强: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>