实现无缝滚动无滚动条的 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>
相关推荐
秋子aria6 小时前
模块的原理及使用
前端·javascript
菜市口的跳脚长颌6 小时前
一个 Vite 打包配置,引发的问题—— global: 'globalThis'
前端·vue.js·vite
小左OvO6 小时前
基于百度地图JSAPI Three的城市公交客流可视化(一)——线路客流
前端
星链引擎6 小时前
企业级智能聊天机器人 核心实现与场景落地
前端
GalaxyPokemon6 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
爱加班的猫6 小时前
深入理解防抖与节流
前端·javascript
自由日记7 小时前
学习中小牢骚1
前端·javascript·css
泽泽爱旅行7 小时前
业务场景-opener.focus() 不聚焦解决
前端
VOLUN7 小时前
Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件
前端·javascript·vue.js